[C++] 8.상속의이해(1)


상속의 문법적인 이해



이번 시간에는 클래스에서 중요한 개념인 상속에 대해서 알아보겠습니다.

상속이 어디에 쓰이면 좋은지는 다음 시간에 알아보도록 하고, 이번에는 문법적인 것만 알아보겠습니다.

상속이라 하면 흔히 재산의 상속을 생각하기 마련이지만,

상속의 대상은 재산뿐만 아니라, 한 사람의 품성이나 특성일 수도 있습니다.

자식이 아버지로부터 좋은 목소리와 큰 키를 물려 받는 것도 상속이라는 것이죠!

클래스의 상속도 이와 유사한데요~

A클래스가 B클래스를 상속하게 되면, A클래스는 B클래스가 지니고 있는 모든 멤버를 물려 받습니다.

예제를 통해 확인해보겠습니다.

class Person //사람 클래스
{
private:
	char name[50];
public:
	Person(char * myname) 
	{
		strcpy(name,myname);
	}
	void WhatYourName() const
	{
		cout<<"My name is"<<name<<endl;
	}
};

//Person 클래스를 상속하는 Student 클래스

class Student : public Person //Person 클래스의 상속을 의미
{
private:
	char major[50]; //전공과목
public:
	Student(char *myname,char *mymager) :Person(myname)
	{
		strcpy(major,mymajor);
	}
	void WhoAreYou() const
	{
		WhatYourName();
		cout<<"My major is"<<major<<endl;
	}
};



위의 예제를 하나하나 다시 확인해보면서 상속을 이해해보겠습니다.


class Student : public Person
{
 ...
}



위의 선언이 의미하는 바는 ‘public 상속’ 입니다.

의미하는 잠시 후에 알아보기로 하고 다른 클래스를 상속할 때 저렇게 선언한다는 것만 알아두세요!




class Person 
{
private:
	char name[50];
public:
	//Person 생성자
	Person(char * myname) 
	{
		strcpy(name,myname);
	}
	...
};
	
	
class Student : public Person 
{
private:
	char major[50]; //전공과목
public: 
	//Student 생성자
	Student(char *myname,char *mymager) :Person(myname)
	{
		strcpy(major,mymajor);
	}
	...
};



클래스 Person의 멤버변수인 name을 클래스 Student가 물려받았기에

Student 생성자는 Person의 멤버변수인 name도 초기화 해야합니다.

그리고 Person의 멤버변수를 초기화 할 때는 Person의 생성자를 호출하여 초기화하는게 더 좋습니다.

더 안정적이기 때문입니다.

위의 예제에서 Student 생성자의 매개변수 myname은 이니셜라이져를 통해

Person 생성자에 전달되어 Person 멤버변수를 초기화합니다.




class Person 
{
	...
public:
	...
	void WhatYourName() const
	{
		cout<<"My name is"<<name<<endl;
	}
};


class Student : public Person 
{
	...
public:
	...
	void WhoAreYou() const
	{
		WhatYourName();
		cout<<"My major is"<<major<<endl;
	}
};



상속을 하게 되면 멤버변수뿐만 아니라 멤버함수도 물려받게 됩니다.

Student에서 선언되지 않은 Person의 멤버함수 WhatYourName을 Student에서 호출이 가능합니다.



이쯤되서 용어를 정리해보겠습니다.


'상속을 한 클래스' <-> '상속을 받은 클래스'

 상위 클래스 <-> 하위 클래스
 기초 클래스 <-> 유도 클래스
 슈퍼 클래스 <-> 서브 클래스
 부모 클래스 <-> 자식 클래스

아까 위에서의 예를 적용하면 클래스 Person은 상속을 한 클래스 기초클래스이고,

클래스 Student는 상속을 받은 유도 클래스가 됩니다.

그 중 기초 클래스와 유도 클래스가 C++에서 가장 일반적으로 사용됩니다. 



유도 클래스의 객체 생성과정



유도 클래스의 객체 생성과정은 매우 중요하기 때문에 조금 더 정확히 이해해보고 넘어가겠습니다.

//기초 클래스 base

class base
{
private:
	int baseNum;
public:
	base() : baseNum(20)
	{
		cout<<"생성자 base() 호출"<<endl;
	}
	base(int n) : baseNum(n)
	{
		cout<<"생성자 base(int n) 호출"<<endl;
	}
	void ShowBaseData()
	{
		cout<<baseNum<<endl;
	}
};

//유도 클래스 derived

class derived :public base
{
private: 
	int derivNum;
public:
	derived() : deriveNum(30)
	{
		cout<<"생성자 derived() 호출"<<endl:
	}
	derived(int n) : derivNum(n)
	{
		cout<<"생성자 derived(int n) 호출"<<endl;
	}
	derived(int n1,int n2) : base(n1),derivNum(n2)
	{
		cout<<"생성자 derived(int n1, int n2) 호출"<<endl;
	}
	void ShowDerivData()
	{
		ShowBaseData();
		cout<<derivNum<<endl;
	}
};



case에 따라 base클래스와 derived 클래스에서 각각 어떤 생성자가 호출되는지 확인해보겠습니다.


//case1

int main(void)
{
	derived dr1;
	dr1.ShowDerivData();
	...
}



실행 결과

생성자 base() 호출
생성자 derived() 호출
20
30



위의 case는 dr1을 선언 할 때 아무런 인자도 전달되지 않았으므로,

생성자 derived()가 호출이 되고, 호출하고 보니 base 클래스를 상속하는 걸 알게됩니다.

그래서 기초 클래스의 생성자 호출을 위해 이니셜라이저를 살피게 되고,

아무런 내용도 명시되지 않았기에 base클래스의 void 생성자인 base()를 대신 호출합니다.


//case2

int main(void)
{
	derived dr2(12);
	dr2.ShowDerivedData();
}



실행 결과

생성자 base() 호출
생성자 derived(int n) 호출
20
12



위의 경우는 첫번째와 유사하나 생성자 derived(int n)가 호출됬다는 점에서 다릅니다.


//case3

int main(void)
{
	derived dr3(23,24);;
	dr3.ShowDerivData();
}



//실행결과

생성자 base(int n) 호출
생성자 derived(int n1,int n2) 호출
23
24



위의 경우는 생성자 dr3 선언 시 매개변수가 두 개 이므로,

생성자 derived(int n1,int n2)가 호출이 되고,

base 클래스를 상속하고 있으므로, 이니셜라이저를 통해 base의 생성자 base(int n)을 호출하게 됩니다.



위의 case들을 통해 알 수 있는 것은 유도 클래스의 객체생성 과정에서

기초 클래스의 생성자는 100% 호출된다는 것,

유도 클래스의 생성자에서 기초 클래스의 생성자 호출을 명시하지 않으면,

기초 클래스의 void 생성자가 호출되는 것입니다.

따라서 유도 클래스의 객체생성 과정에는 생성자가 두 번 호출됩니다.



유도 클래스 객체의 소멸과정



유도 클래스의 객체 생성과정에서 생성자가 두 번 호출 되는 것을 통해,

소멸자도 두 번 호출 되는 것을 유추하실 수 있습니다.

class base
{
	...
public:
	...
	~base()
	{
		cout<<"~base()호출"<<endl;
	}
}

class derived :public base
{
	...
public:
	...
	~derived()
	{
		cout<<"~derived()호출"<<endl;
	}
}

int main (void)
{
	derived drv1;
	return ;
}



//실행결과

~derived()호출
~base()호출



위의 예제를 통해 유도 클래스의 객체가 소멸될 때에는, 유도클래스의 소멸자가 실행되고,

그 후에 기초 클래스의 소멸자가 실행되는 것을 볼 수 있습니다.

그러한 특성 때문에 생성자에서 동적 할당한 메모리 공간은 소멸자에서 해제하여야합니다.



protected 선언과 세 가지 형태의 상속



C++의 접근제어 지사자에는 private, protected, public 이렇게 세가지 존재합니다.

그리고 허용하는 접근의 범위는 다음의 관계가 있습니다.

private < protected < public

private와 public은 전에 살펴보았고 protected에 관련된 예제를 보면서,

protected에 대해 알아보겠습니다.

class base
{
private:
	int num1;
protected:
	int num2;
public:
	int num3;
};

class derived :public base
{
public:
	void ShowBaseMember()
	{
		cout<<num1; //컴파일 에러
		cout<<num2; //컴파일 성공
		cout<<num3; //컴파일 성공
	}
};



위의 예제에서 보듯이 protected로 선언된 멤버변수는 이를 상속하는 유도 클래스에서 접근이 가능합니다.



하지만 protected 선언은 private와 public에 비해 그리 많이 사용되지 않습니다.

기초 클래스와 이를 상속하는 유도 클래스 사이에서도 정보은닉은 지켜지는게 좋기 때문입니다.

그 다음으로 세 가지 형태의 상속에 대해 알아보겠습니다.

각각 예제를 들어보겠습니다.


//public 상속

class base
{
private:
	int num1;
protected:
	int num2;
public:
	int num3;
};

class derived : public base
{
	...
};



상속을 하게 되면 base의 멤버변수를 derived가 물려받게 되는데,

public 상속을 하게 되면 다음은 형태로 물려받게 된다고 생각하면 됩니다.

class derived : public base
{
접근불가:
	int num1;
protected:
	int num2;
public:
	int num3;
};



접근 불가라는 키워드는 존재하지 않지만, 멤버변수가 존재는 하되 접근은 불가능함을 의미하기 위해서

이렇게 표현했습니다.

public 상속은 public보다 접근의 범위가 넓은 멤버는

public으로 변경시켜서 상속하겠다는 뜻입니다.


//protected 상속

class derived : protected base
{
	...
};



protected 상속을 하면 다음과 같은 형태로 멤버변수를 물려받게 됩니다.

class derived : protected base
{
접근불가: 
	int num1;
protected:
	int num2;
protected:
	int num3;
};



protected 상속은 protected보다 접근의 범위가 넓은 멤버는

protected로 변경시켜서 상속하겠다는 뜻입니다.




//private 상속

class derived : private base
{
	...
};



private 상속을 하면 다음과 같은 형태로 멤버변수를 물려받게 됩니다.

class derived : private base
{
접근불가:
	int num1;
private:
	int num2;
private:
	int num3;
};



private 상속은 private보다 접근의 범위가 넓은 멤버는

private로 변경시켜서 상속하겠다는 뜻입니다.

세 가지 형태의 상속 방법이 있지만, 주로 public 상속을 많이 사용하기에,

public 상속만 있다고 생각해도 크게 지장은 없습니다~!



상속을 위한 조건



상속으로 클래스의 관계를 구성하기 위해서는 조건이 필요합니다.

1. IS-A 관계의 성립

상속의 기본 문법에서 보이듯이, 유도 클래스는 기초 클래스가 지니는 모든 것을 지니고,

거기에다가 유도 클래스만의 추가적인 특성이 더해집니다.

예를 들어 전화기->무선 전화기를 들어봅시다.

무선전화기는 일종의 전화기입니다. (is a 관계)

따라서 전화기의 특성을 무선 전화기는 모두 가져야합니다.

전화기가 기초클래스가 되고, 무선전화기는 유도 클래스가 되는거죠!

이처럼 상속관계가 성립하려면 기초클래스와 유도 클래스간에 IS-A관계가 성립해야 합니다.




2.HAS-A 관계

소유의 관계(has a 관계)도 상속관계가 성립합니다.

이 경우에 유도클래스는 기초클래스가 지니고 있는 모든 것을 소유합니다.

하지만 소유 관계는 상속이 아닌 다른 방법으로도 충분히 구현 가능하기에

이 경우에는 상속이 자주 사용되지 않습니다.

프로그램 변경에도 많은 제약을 주기도 하기 때문입니다.

따라서 상속관계가 성립하려면 IS-A관계가 성립해야 한다고 생각하셔도 무관합니다.