상세 컨텐츠

본문 제목

C++ 상속

C++/C++98

by deulee 2023. 8. 7. 11:35

본문

상속의 개념

상속은 OOP의 중요한 개념 중 하나다. 상속은 기존에 정의된 클래스로부터 새로운 클래스를 만들 때 사용하는 메커니즘이다. 이로 인해 코드의 재사용성과 유지보수성이 향상된다.

 

그리고 클래스간의 계층적 관계를 구성하게 되고 다형성의 문법적 토대가 된다.

 

그럼 상속의 사전적 의미도 자식이 부모가 가진 모든 것을 물려 받는 것을 의미하는데 OOP의 상속도 기본적인 의미는 동일하다.

 

이로 인해 얻는 이점이 크게 3가지가 있다.

 

1. 기존의 클래스를 재활용한다.

 

2. 공통되는 부분을 상위 클래스에 통합하여 반복을 제거하고 유지, 보수를 편리하게 한다.

 

3. 공동의 조상을 가지는 계층을 만듬으로써 객체의 집합에 다형성을 부여한다.

 

즉, 부모에서 정의된 멤버들(private 제외)을 자식에서 그대로 물려서 사용할 수 있게 된다.


상속의 예시

#include <iostream>

// 부모 클래스 정의
class Animal
{
public:
	void eat() {
		std::cout << "Animal is eating" << std::endl;
	}
};

// 자식 클래스 정의, 부모 클래스를 상속한다.
class Dog : public Animal
{
public:
	void bark() {
		std::cout << "Wang Wang" << std::endl;
	}
};

int main(void)
{
	Dog myDog;
	myDog.eat(); // 부모 클래스인 Animal의 멤버 함수를 사용할 수 있다.
	myDog.bark(); // 자식 클래스인 Dog의 멤버 함수를 사용할 수 있다.
	return 0;
}

위의 케이스를 보면 다음과 같다고 보면 된다.

 

Dog 클래스가 Animal로부터 상속받음

 

즉,  'Dog' 클래스는 'Animal' 클래스를 상속하고 있다. 따라서 'Dog' 객체인 'myDog'는 'Animal' 클래스에 정의된 'eat()' 함수를 사용할 수 있다.

 

이것이 C++에서의 상속 개념의 간단한 설명이다. 지금은 멤버 함수만 공유하는 코드를 짰지만 멤버 변수도 'protected'나 'public'으로 지정되어 있으면 공유가 가능하다.


상속의 정보 은폐

 

다음은 상속에서 

#include <iostream>

class A
{
private:
	int priA;
	void funcPriA() {std::cout << "부모 클래스의 private 함수"};
protected:
	int proA;
	void funcProA() {std::cout << "부모 클래스의 protected 함수"};
public:
	int pubA;
	void funcPubA() {std::cout << "부모 클래스의 public 함수"};
};

class B : public A
{
private:
	int priB;
	void funcPriB() {std::cout << "자식 클래스의 private 함수"};
public:
	void funcPubB()
	{
		priB = 0; // 자신의 모든 멤버 액세스 가능
		funcPriB();

		priA = 1; // 에러 : 부모의 private 멤버는 액세스 불가능
		funcPriA();

		proA = 2; // 부모의 protected 멤버는 액세스 가능
		funcProA();

		pubA = 3; // 부모의 public 멤버는 액세스 가능
		funcPubA();
	}
};

int main(void)
{
	B b;

	b.funcPubB(); // 자신의 멤버 함수 호출
	b.funcPubA(); // 부모의 public 멤버 함수 호출
}

위의 예시 코드에 나온대로 상속시 부모의 어떤 멤버에 접근할 수 있는지 알 수 있다.

 

이를 정리해서 표로 나타내면 다음과 같다.

액세스 지정자 클래스 외부 파생 클래스 설명
private 액세스 금지 액세스 금지 무조건 금지
protected 액세스 금지 액세스 허용 파생 클래스만 허용
public 액세스 허용 액세스 허용 무조건 허용

 


상속 액세스 지정

 

자식 클래스를 정의하는 일반적인 문법은 다음과 같다.

 

class Child : { public / protected / private } Parent
{

};

 

'public', 'protected', 'private' 총 세 가지의 상속 액세스 지정자라는 것이 있다.

 

이 지정자는 부모 클래스의 멤버들이 자식 클래스로 상속될 때 액세스 속성이 어떻게 바뀌게 되는가를 지정하는 것이다.

 

정리해보자면 다음과 같다.

 

상속 액세스 지정자 부모 클래스의 액세스 속성 파생 클래스의 액세스 속성
public public public
private 액세스 불가능
protected protected
private public private
private 액세스 불가능
protected private
protected public protected
private 액세스 불가능
protected protected

우선 부모 클래스의 private 멤버는 어떤 경우라도 읽을 수 없다. 따라서 상속은 되지만 파생 클래스에서는 직접 참조할 수 없으므로 액세스 속성이 아예 없다고 할 수 있다.

 

부모 클래스의 public, protected 멤버는 상속 액세스 지정자에 따라 액세스 속성이 변경된다.

 

상속 액세스 지정자public이면 부모 클래스의 액세스 속성이 그대로 유지된다. 즉, 부모의 protected 멤버는 상속된 후의 자식 클래스에서도 여전히 protected이며 부모의 public 멤버는 자식 클래스에서도 외부로 공개된다.

 

상속 액세스 지정자private, protected인 경우는 부모의 모든 멤버가 상속되면서 private, protected로 변경된다.

 

만약 상속 액세스 지정자가 생략되면 디폴트인 private가 적용된다. 즉, 다음 두 구문은 동일한 문장이다.

 

class D : B
class D : private B

상속의 특성

  1. 상속은 여러 개의 클래스를 파생시킬 수 있다.
    • 동물, 식물, 미생물이 생물의 범주에 있는 것처럼 말이다.
  2. 하나의 클래스로부터 파생될 수 있는 클래스의 개수에 제한이 없다.
    • 생물 -> 동물 -> 포유류 -> 영장류 -> 원숭이 (is a 관계)
    • 부모에 다가갈수록 일반적이고 자식에 다가갈수록 구체적이다.
  3. 두 개의 이상의 클래스로부터 새로운 클래스를 파생시킬 수 있는데 이를 다중 상속이라고 한다.
    • 많이 쓰이진 않는다.

객체의 생성 및 파괴

그럼 상속받은 객체는 어떻게 생성되고 파괴되는 걸까?

 

자식 클래스의 입장에서 부모 클래스의 private 멤버의 영역은 건드리지를 못하니 자식 입장에서도 초기화를 할 수 없고 부모 입장에서도 자식의 멤버에 접근이 안되니 어찌할 도리가 없다.

 

이를 해결하는 방법은 부모 클래스의 public 생성자를 호출하여 상속받은 멤버초기화 할 수 있다. 생성자는 항상 public이므로 누구나 호출할 수 있다.

 

#include <iostream>

class Parent
{
private:
	int x;
public:
	Parent(int _x) : x(_x)
	{
		std::cout << "Parent constructor" << std::endl;
	}
	void printParent(void)
	{
		std::cout << x << std::endl;
	}
};

class Child : public Parent
{
private:
	int y;
public:
	Child(int _x, int _y) : Parent(_x), y(_y)
	{
		std::cout << "Child constructor" << std::endl;
	}
	void printAll(void)
	{
		printParent();
		std::cout << y << std::endl;
	}
};

int main(void)
{
	Child c(1, 2);

	c.printAll();
	return 0;
}

 출력 결과

 

위의 예제를 보면 부모 생성자먼저 호출되고 자식 생성자가 그 다음에 호출된다. 그리고 각각의 멤버가 잘 초기화 한것을 볼 수 있다.

 

이렇게 보면 자식 객체 하나 만들려고 부모 뿐만 아니라 조상 객체들 까지 초기화해야 하는 것이 속도 측면에서 문제가 생길 것 같지만, 생성자는 내부에 정의되는 함수로 인라인 함수이기 때문에 속도 차이가 그렇게 나지는 않는다.

 

하지만 메모리적인 측면에서 과도한 객체 생성은 메모리 낭비가 될 수 있으므로 보통 매개변수로 객체를 넘길 때 call by reference 형식으로 호출하는 것이 일반적이다.

 

그리고 멤버 초기화 리스트를 통해서 생성자를 호출하는 것을 주목해야 한다. 멤버 초기화 리스트는 멤버에 값을 할당하는 것이 아니라 멤버를 초기화 시켜주는 작업을 한다.

 

그렇기 때문에 부모 클래스의 생성자를 호출 할 수 있는 것이다.

 

그럼 만약 멤버 초기화 리스트를 사용하지 않고 자식 생성자 본체에서 생성자를 호출하여 초기화를 진행하면 어떻게 될까?

Child(int _x, int _y)
{
	Parent(_x); // 임시 객체가 생성될 뿐 초기화 작업을 해주지는 않는다.
	std::cout << "Child constructor" << std::endl;
}

위의 코드는 부모 클래스의 디폴트 생성자를 정의하지 않아 컴파일조차 되지 않는다.

 

만약 했다 치고 컴파일이 된다면 위의 코드는 Child 생성자 내부에서 임시 객체가 생성될 뿐 막상 생성자를 호출한 객체의 부모 클래스에는 아무런 영향을 주지 않는다.

 

그럼 자식 클래스에서 멤버 초기화 리스트로 부모 클래스를 호출하지 않으면 어떻게 될까? 이때는 부모의 디폴트 생성자가 호출된다. 이때, 방금 전에도 말했지만 부모 클래스의 디폴트 생성자가 정의되지 않았다면 컴파일 에러가 난다.

 

그렇기 때문에 꼭 꼭 멤버 초기화 리스트를 통해서 생성자를 호출하는 것을 기억하자!!

 

파괴자 호출 순서

그럼 파괴자는 어떻게 호출될까? 파괴자는 스택의 후입선출의 원리를 이용하여 생성자가 호출될 때 부모가 먼저 호출되고 자식이 나중에 호출된 것의 반대로 자식의 파괴자가 먼저 호출되고 부모가 나중에 호출된다.

 

그럼 최종적으로 순서를 정리하도록 하겠다.

 

생성자 호출 순서

1. 부모 생성자

2. 자식 생성자

 

파괴자 호출 순서

1. 자식 파괴자

2. 부모 파괴자


출처

http://www.soen.kr/lecture/ccpp/cpplec.htm

 

C/C++ 강좌

 

www.soen.kr

 

'C++ > C++98' 카테고리의 다른 글

C++ 다중 상속  (0) 2023.08.07
C++ 오버라이딩  (0) 2023.08.07
C++ 다양한 연산자 오버로딩의 예시  (0) 2023.08.05
C++ 전역 연산자 함수  (0) 2023.08.05
C++ 연산자 함수  (0) 2023.08.05

관련글 더보기