CS/C++

C++ 공부 섹션11 상속 : 홍정모의 따배씨쁠쁠

샤아이인 2022. 1. 16.

 

내돈내고 내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

1) 섹션11

​처음 상속을 배웠는대 약간 복잡성이 더해진 class인것 같다. 뇌를 자극하는 재미가 있는 단원인것 같다.

글을 다 쓰고나니 진짜 내 영혼을 갈아넣은듯한 느낌으로 포스팅 하였다.

누가보든 C++ 이전내용의 기반이 있다면 이해가능하도록 하나하나 서술하였다.

 

◆ 이번시간에는 상속(inheritance)에 대하여 집중적으로 배웠다. 효율적인 class의 사용법이며, 숙련되면 초기부터 디자인할때 상속관계를 생각하면서 구성한다 알려주셨다.

11-1 상속의 기본 (1/2)

#include <iostream>

using namespace std;

class Mother {
	int m_i;
public:
	void setValue(const int& i_in){
		m_i = i_in;
	}

	int getValue() {
		return m_i;
	}
};

class Child : public Mother { // 상속
	// 필요한 내용 구현
};

int main() {
	Mother mother;
	mother.setValue(1024);
	cout << mother.getValue() << endl;

	Child child;
	child.setValue(128);
	cout << child.getValue() << endl;

	return 0;
} 
 

위의 코드를 살펴보자. 가장 먼저 상속을 어떻게 하지는지 처음 확인해볼 수 있었다.

class Child : public Mother
 

Child class는 Mother class를 상속받았으며 이로인해 Child class의 구현 부분이 비어있음에도 Mother에서 사용하던 member function들을 사용할수가 있다.

 

이러한 상속받은 Child class를 derived class라고도 부른는대 이는 Mother class에 있던것을 기본적으로 다사용한다는 개념에서 나온말이다. 또한 부모 class는 generalized class라고 부르기도 한다. 이는 여러 자식 클레스들의 공통점을 추출하여 만든것이기 때문이다.

 

그럼 만약 Mother class로부터 setValue()와 getValue()를 상속받은 상황에서 child 내부에서도 setValue, getValue함수가 있다면 어떻게 될까? Child class에서 호출하는 setValue, getValue함수는 mother class의 것 인가? 아님 child class의 자신의 것 인가?

class Mother {
	int m_i;
public:
	void setValue(const int& i_in){
		m_i = i_in;
	}

	int getValue() {
		return m_i;
	}
};

class Child : public Mother { // 상속
	double m_d;
public:
	void setValue(const double &d_in){
		m_d = d_in;
	}

	int getValue() {
		return m_d;
	}
};
 

우선 답부터 말하면 Child class에서 호출하는 setValue, getValue함수는 child class의 것 이다. 이름이 같다면 Child class에서는 Child class의 함수가 우선순위가 높으며, 자기의 멤버함수를 먼저 호출하는 것이 상식적이다. 따라서 main에서 다음과 같이 매게변수가 double로 확인된다.

setValue() 의 매게변수가 double& 인것을 알수 있다. 하지만 위의 코드처럼 정수 128 (int형)을 인자로 넘겨주고싶다면 어떻게 해야할까? 우리는 다음과 같이 int와 double을 모두 받을 수 있는 함수를 만들어야 한다.

하지만 위의 코드를 보면 2번째 오류가 생긴다. m_i는 mother class의 private 멤버변수라 child에서 접근을 못하고 있는 상황이다. 이를 해결하기 위해 Mother class 에서 멤버변수를 public으로 해주면 급급하게나마 해결이 가능하다. 하지만 이는 캡슐화 라는 class의 이념에 반대되는 행위이다. 이때 사용할 수 있는것이 바로 protected이다.

protected를 사용하면 기존의 private의 속성은 유지하되, child class에서도 접근이 가능해진다. 즉 class 외부에서는 m_i변수를 사용할 수 없지만, 상속받은 Child class나, 자기자신인 Mother class에서는 접근 가능해지는 것 이다.

 

하지만 교수님께서는 private를 유지한 상태로 사용하는 방법을 보여주셨다. 다음과 같다.

위의 코드와 같이 m_i의 값의 초기화를 Mother::setValue(i_in);을 통하여 우회하는 방식으로 하는 것 이다. 또한 main함수에서도 이를 활용하여 초기화 해줄 수 있다.

하지만 위와 같은 방식은 진정한 초기화라 할수가 없다. 초기화란 변수선언과 동시에 값을 그즉시 바로 담아주는 과정이지만, 위와 같은 방식에서는 메모리를 먼저 할당을 받은후 뒤이어 자식클레스 에서 1024를 복사해서 대입해주는 것 이기에 엄밀히 초기화가 아니다.

 

따라서 Child class의 생성자를 만들어야 할 것이다.

우선 기존에 하던방식대로 생성자를 만들며 초기화하려드니 다시 에러가 발생하였다. 이번에는 또 뭐가 문제란 말인가??

혹 이전 강의들에서 Class는 instance(object)생성시에 메모리를 할당받는다는 교수님의 말씀을 기억하는가? 이것이 문제이다. m_i는 Mother class의 멤버변수인대 Child class의 인스턴스(object)를 생성할 당시에는 m_i는 메모리를 갖고있지 않다. 없는공간을 값으로 초기화하려 드는 것 이다. 추후 11-3장 유도된 클레스의 생성순서에서 더 자세한 이유를 다룬다.

 

따라서 다음과 같이 {} 안에서 초기화 해줘야 한다.

.

여기까지 하면 Child Class의 생성자는 만들어 줬다. 하지만 아직 Mother class의 생성자는 만들지 않았다. 만약 main함수에서

Mother mother(1024)와 같이 초기화를 해주고 싶을수도 있지 않은가? 그리고 기본적으로 생성자는 class의 기본요소 아닌가?

따라서 위의 코드와 같이 생성자를 만들었지만 다음과 같은 오류가 나타난다.

Mother class에 default constructor가 없다는 것 이다. 예전에 배웠듯 생성자는 프로그래머가 직접 만들어 주지 않으면 컴파일러가 compile할때 default constructor를 삽입해 주지만, 우리가 생성자를 직접 만든경우 default constructor는 자동생성되지 않는다. 하지만 이것이 직접적인 원인은 아니다. 더 큰 문제는 Child class 생성시 mother class의 default constructor 알아서 호출해주는대, 우리가 직접 생성자를 구현했기때문에 default constructor가 없어 호출되지 못했고 따라서 Child 가 상속받을때 가지고올 default constructor가 없어서 오류가 나는 것 이다.

 

이를 해결하기 위해 가장 간단한 방법으로는 default constructor를 만들어줄수 있을것 이다.

하지만 이보다 더 세련된 방식을 알려주셨다.

 

Child class가 들어온 int값(i_in) 을 Mother class에 넣어주면서 생성자를 호출하는 것 이다.

 

다음 코드로 확인해보면 이해갈 것 이다.

이렇게 된 경우 Child class에서 위에서 우리가 직접만든 생성자를 호출함으로써 부모의 default 생성자를 호출하지 않게되는 것 이다.

 

 11-2 상속의 기본 (2/2)

선생님이건 학생이건 둘다 공통적으로 사람이라는 속성을 갖고있다. 따라서 Person.h를 따로 만들어 사람의 속성을 구현하며, 이를 선생님 과 학생 각각이 상속하여 사용한다면 더욱 효과적일 것 이다.

 

<주의!!>

헤더파일을 작성할때는 using namespace std;를 맨 위쪽에 선언해주면 안된다. 왜냐하면 이 헤더를 포함하는 다른 cpp파일에서 자칫 잘못 std를 사용할수 있기 때문이다. 따라서 헤더파일 안에서는 귀찮더라도 std::cout, std::endl 과 같이 직접 타이핑 해줘야 한다.

 

원래는 학생과 선생님 class에서 각각 자기만의 m_name라는 멤버변수를 갖고있었다(코드생략). 하지만 이름은 사람을 공통속성으로 Person class안에 포함시켜도 될 내용이다.

#pragma once
#include <iostream>
#include <string>

class Person {
	std::string m_name;
public:
	Person(const std::string &name_in)
		: m_name(name_in) {}
};
 

아래의 코드는 이렇게 만든 Person.h 를 Student.h에서 include하고 상속받고있는 모습이다.

하지만 위의 코드를 보면 에러가 발생하고 있다. m_name에 접근하여 인자로 들어온 name_in을 넘겨주고 싶지만, m_name은 Person의 private 멤버변수이기에 에러가 발생한다. 11-1 에서 말했듯 Student의 인스턴스를 생성할때는 아직 m_name은 메모리를 할당받지 못한 상황이며, 더 나아가 m_name이라는 멤버변수의 1차적 책임은 Person에 있기에, 직접 변수에 접근하는 것이 아닌 Student에서 Person의 생성자를 호출하여 간접적으로 초기화 하는 것 이다. 즉, 생성자는 Person에게 있어야 한다.

 

따라서 student의 생성자 를 통하여 Person의 생성자를 간접적으로 호출해줘야 한다.

하지만 또다시 오류가 나타난다.

위의 코드를 보면 Student의 생성자에서 name_in을 No_name으로 default값을 지정하고있다. 즉, Student class에서는 default생성자가 정해진 것이다. 하지만 아직 Person class에 default생성자가 없는것 이다. 위에서도 말했듯 자식class의 생성자를 호출할때 자동적으로 부모의 생성자를 불러오게 된다했으니, 불러올 default생성자가 없으면 문제가될 것이다. 요약하면, Person class에서 상속해줄 default 생성자가 없어 오류가 나타난다. 따라서 위에서 했듯 default생성자를 만들어 주거나, 초기값을 지정해주면 된다.

#pragma once
#include <iostream>
#include <string>

class Person {
	std::string m_name;
public:
	Person(const std::string &name_in = "No Name") // 이부분 에서 default값 설정
		: m_name(name_in) {}
};
 

위의 코드와 같이 default값을 지정해주면 된다.

 

이제 다른 오류가 남아있다. 우선 다음 코드를 보자

Student의 멤버함수에서도 m_name에 접근하는대 private 변수이기에 외부에서 접근할수 없으며, 상속되었어도 접근할수가 없다.

Person.h에서 멤버변수인 m_name을 public으로 바꾸면 일단 급급하게 사용은 가능하다. 하지만 이는 class의 캡슐화에 어긋난다.

 

교수님이 말씀해 주시길 : 상속받는 class에서 부모의 멤버 변수에 접근하여 변경이 가능하다면, 코드를 수정해야하는 경우 자식 class에서 부모의 멤버변수에 접근하는 부분을 전부다 하나하나 수정해줘야 하는 문제가 생긴다. 또한 Person class를 상속받은 student나 teacher 클래스의 멤버 함수들이 m_name을 바꾸려는 시도가 많이지게 된어 기능이 겹친다.

 

따라서 이를 그냥 Person.h 로 옮기는것이 깔끔하다 말해주셨다. 어짜피 student와 teacher 양쪽에 공통적으로 필요한 기능이니까!!

Person.h

#pragma once
#include <iostream>
#include <string>

class Person {
	std::string m_name;
public:
	Person(const std::string &name_in)
		: m_name(name_in) {}

	void setName(const std::string& name_in) {
		m_name = name_in;
	}

	std::string getName() {
		return m_name;
	}
};
 

setName과 getName함수가 Person class안으로 들어가 있는것을 확인할 수 있다. 추가로 m_intel변수의 경우 각 학생마다의 지적능력의 지표이다. 이는 학생이 갖는 속성이기에 Person class로 옮겨서는 안된다.

 

teacher.h

Teacher class안에서 부모class인 Person의 멤버함수인 getName()을 가져다가 사용할수가 있다.

 

마지막으로 연산자 오버로딩을 조금 손봐줘야 한다.

outstream 연산자 오버로딩에서 const 참조로 student인자를 받고있는데, 그 student에 대해 getName 함수를 사용하고 있어서 이름은 얻을수 있지만 변경할수 없는 상황이다. 따라서 getName함수에 const를 붙여 이름을 반환만 하고 바꾸지 않도록 해준다.

추가로 Student class에는 study()를, Teacher class에서는 teach()를 구현해 줄 수 있다. 중요한것은 이 함수들은 Person의 공통속성이 아니라는 점 이다. 따라서 위의 두 멤버 함수들은 각각의 자신의 class안에 있어야되며, 공통속성이 오는 Person으로 옮기면 안된다.

 

 11-3 유도된 클래스들의 생성 순서

Child class는 Mother class를 상속받고있다. 따라서 Child class에서도 m_i라는 부모의 멤버변수를 사용할수있다(public임). 위의 코드를 보면 this->m_i = 10; 과 같이 사용중 인것을 볼 수 있다. 하지만 m_i(1024)로 초기화 하려들면 에러나 나타난다. 왜그럴까?? 이는 앞에서도 말한적이 2번 있지만, 이번시간에 더욱 깊게 알아볼 것 이다.

 

이를 이해하기 위해서는 부모와 자식 class의 생성자 호출 순서에 대하여 알아야 한다. 우선 다음 코드를 보자.

#include <iostream>

using namespace std;

class Mother {
public:
	int m_i;
    // 기본생성자
	Mother() : m_i(1) {
		cout << "Mother construction " << endl;
	}
};

class Child : public Mother {
public:
	double m_d;
    // 기본생성자
	Child() : m_d(1.0) {
		cout << "Child construction " << endl;
	}
};

int main()
{
	Child c1;

	return 0;
}
 

Child의 생성자는 m_d만을 초기화 하고있는 것 "처럼" 보인다. Mother class의 생성자와는 무관해 보인다 이말이다. 이를 확인해 보기 위해서 실행해보면 다음과 같이 뜬다.

Mother construction?? 난 분명 Child만을 호출했는대 왜 부모의 생성자가 호출되는것 이지?? 의구심이 들 것이다.

즉, Child class에 대한 인스턴스(object)를 만들때 우리가 의도적으로 Mother class의 생성자를 호출하지 않아도 자동으로 Mother의 default생성자를 호출한다. 이렇게 부모의 생성자가 먼저 실행된 후에 Child class의 생성자가 호출되는 것 이다.

 

이를 디버깅을 통해 하나하나 확인해 보자.

실행하면 우선 Child class 안의 생성자로 이동한다. 하단의 m_d의 Value를보면 아직 쓰레기 값이 들어있음을 알 수 있다.

 

step into를 눌러 다음 코드로 진행하니, 갑자기 Mother class로 이동하였다!! 위의 사진에 보이듯 아직 m_i값은 쓰레기 값이다.

 

한번더 step into를 누르니 m_i값이 1로 초기화 되고 Mother construction이라는 출력문 출력하였다.

 

마지막으로 한번더 step into를 누르니 다시 Child class로 돌아와 Child의 생성자를 실행하고 출력문을 출력한후 종료되었다.

Child class만 봐서는 Child의 생성자만 호출하고 사용할 것 같지만, 실상은 달랐다. 이는 컴파일러가 편의를 위해 보여주지 않을뿐 내부적으로는 상속의 대상인 Mother class의 생성자를 호출하는 것 이다. 사실 이는 어찌보면 당연하다. 우리는 Mother의 것들을 재사용 하기위해 상속을 한 것이다. 그렇다면 Mother의 것을 사용하기전에 생성자를 통한 초기화를 한후 Child에서 상속하여 사용하는 것 이 타당한 생각이 된다.

 

자 다시 원래의 문제점으로 돌아가 보자. m_i(1024)로 초기화 하려들면 에러나 나타난다. 왜그럴까?? 에 대해 답할 준비가 완료됬다. 이유는 Mother class가 초기화 되기전(= Mother의 멤버변수가 공간을 확보하기전)에 Mother의 멤버변수에 접근하여 자식 class에서 초기화를 할수가 없기때문이다. 즉 순서가 뒤바뀐 것 이다.

 

위에서 처럼 m_i(1024)처럼 직접 초기화할수는 없지만, 아래와 같이 Mother class를 거친뒤에 다시 돌아온 Child class의 생성자 내부에서 값의 대입이 가능해진다.

위의 코드에서 m_d(1.0)과 같이 initialization list를 통해 초기화하지 못하는 것은, m_d(1.0)라 적힌 줄에서는 아직 Mother의 생성자가 호출되기 이전이라 initialization list 를 건드릴수가 없는것 이다. 따라서 먼저 부모쪽으로 이동후 생성자를 호출한 후에야 다시 child class 내부로 돌아와서야 접근이 가능해진다.

 

또 한가지 숨어있는 비밀이 있는데, 우리가 적던, 적지않던 다음과 같이 코드가 숨어있다는 점이다.

이렇게 Mother의 생성자 호출이 숨어있던 것 이다. 이를 활용하여 Child class의 생성자에서 Mother생성자 호출시 초기값을 넘겨줄수도 있어진다.

위의 코드와 같이 Child 에서 Mother()이 호출될때, i_in으로 1024가 전달되어 바로 m_i를 초기화 할수있다. 인자가 없다면 기본값인 0으로 초기화 되며, 이는 default constructor 에 해당된다.

 

또 하나 재미있었던 내용이 있는대 만약 initialization list의 순서를 바꿔 m_d(1.0), Mother(1024)가 되면 어떻게 될까? m_d(1.0)이 먼저 초기화 될까?

 

답은 아니오 이다. 순서를 바꿔도 항상 Mother class의 생성자가 먼저 실행된다.

 

마지막으로 연속적인 상속과정을 관찰해보자.

#include <iostream>

using namespace std;

class A
{
public:
	A(){
		cout << "A constructor" << endl;
	}
};

class B : public A
{
public:
	B(){
		cout << "B constructor" << endl;
	}
};

class C : public B
{
public:
	C(){
		cout << "C constructor" << endl;
	}
};

int main()
{
	C c;

	return 0;
}
 

이전의 내용을 잘 숙지했다면 다음과 같은 결과는 어렵지 않게 받아들여진다.

C 생성자 진입 -> B 생성자 진입 -> A생성자 진입 -> A생성자 실행 -> B생성자 실행 -> C생성자 실행 순 일것이다.

 

 11-4 유도된 클래스들의 생성과 초기화

#include <iostream>

using namespace std;

class Mother {
public:
	int m_i;

	Mother(const int &i_in = 0) : m_i(i_in) {
		cout << "Mother construction " << endl;
	}
};

class Child : public Mother {
public:
	float m_d;
public:
	Child() : Mother(1024), m_d(1.0f) {
		m_i = 1024;
		cout << "Child construction " << endl;
	}
};

int main()
{
	Child c1;

	cout << sizeof(Mother) << endl;
	cout << sizeof(Child) << endl;

	return 0;
}
 

이전의 예제를 조금만 변형 하여 재사용하자. Child class의 멤버변수는 float형 m_d하나만 코드상으로 보이지만 사실 Mother class의 m_i도 생각해줘야 한다. 이들 두 class의 사이즈를 출력해보면 명확히 확인이 가능해진다.

Mother의 경우 int형 하나라 4바이트가 나오고, Child의 경우 int(4바이트)와 float(4바이트)가 합쳐저서 8바이트가 출력된다. main에서 Child 클레스의 인스턴스(object)를 만들때는 Child class의 멤버변수 + Mother class의 멤버변수 의 사이즈와 같거나 그 이상의 메모리를 할당한다. 그 이상이라 말한 이유는 패딩값이 포함되어 있기 때문이다. 이는 예전에 C에서 배운적 있던 내용이다.

 

그다음으로는 소멸자의 호출 순서에 대하여 배웠다.

#include <iostream>

using namespace std;

class A
{
public:
	A(int a){
		cout << "A: " << a << endl;
	}

	~A(){
		cout << "Destructor A " << endl;
	}
};

class B : public A
{
public:
	B(int a, double b) : A(a) {
		cout << "B: " << b << endl;
	}

	~B(){
		cout << "Destructor B " << endl;
	}

};

class C : public B
{
public:
	C(int a, double b, char c) : B(a, b) {
		cout << "C: " << c << endl;
	}

	~C(){
		cout << "Destructor C " << endl;
	}
};

int main()
{
	C c(1024, 3.14, 'a');

	return 0;
}
 

일단 결과를 먼저보자!

소멸자의 경우 생성자의 역순으로 호출되는것을 확인할 수 있었다. 소멸자의 경우 만약 동적할당받은 메모리가 있다면 이를 반환하는 역할을 할것이다. 이는 명품 C++에서도 배웠던 내용이다.

 

 11-5 상속과 접근 지정자

상속에서 이부분이 전 가장 어려웠습니다............

부모 클래스의 멤버 변수/함수의 접근 지정자와
상속 접근 지정자를 비교해서 더 엄격한 쪽을 따른다!

이점을 명심하고 다음 예제들을 같이 확인해 보자.

Derived class의 상속 접근 지정자가 public이다. 그렇다면 Base class의 m_public과 비교해보면 누가 더 엄격한가? 둘다 동일한 public이다 따라서 m_public 변수는 Derived class 안에서도 public으로 작용한다.

 

그렇다면 Base class의 m_protected와 비교해보면 누가 더 엄격한가? protected가 더 엄격하다. 따라서 m_protected는 protected 로 Derived class안에서 작용한다.

 

변수 m_private의 경우 애당초 부모 class에서 private이라서 자식인 Derived calss에서 접근할수가 없다.

이제 상속 접근자가 protected로 바뀌었다.

 

Derived class의 상속 접근 지정자가 protected이다. 그렇다면 Base class의 m_public과 비교해보면 누가 더 엄격한가? protected가 더 엄격하다. 따라서 Derived class안에서 m_public변수는 protected로 작용한다. main에서 접근하지 못하고 있는 상황이다.

 

그렇다면 Base class의 m_protected와 비교해보면 누가 더 엄격한가? 둘다 같은 protected니까 protected가된다. 중요한 내용이 있는데 Derived class"안"에서 protected인것에 주목해야 한다. 무슨말인가? 할것이다. 위의 코드를 보면 Derived 안에서 m_protected가 123으로 대입되고 있는데, 이는 Derived class안에서 protected가 됬지만 같은class 안에서는 protected 멤버에 접근이 가능하기에 대입이 가능하다. 또한 Derived class안에서 protected가 되었다는 말은 Derived class를 상속받는 또다른 class에서 접근이 가능하다는 것 이다.

 

위의 내말을 이해했다면 다음 코드(private)에서 왜 m_protected변수가 Derived class에서 접근가능한거지? 라는 의문이 해결될 것 이다.

이전 주황글씨의 내말을 이해하지 못했다면 위의 코드를 보고 다음과 같은 생각이 들 것이다. Derived의 상속 서식지정자가 private이니 Base에서 protected인 m_protected와 비교시 private가 더 강하다. 따라서 m_protected는 private가 될것이고 따라서 123이 대입불가능할 것 이다. 이는 정확한 오답이다! m_protected는 Derived class안의 private변수가 되지 않았는가? 같은 Derived class안에서는 접근이 가능하다!!

 

◆ 11-6 유도된 클래스에 새로운 기능 추가하기

Derived class(자식클래스)에 새로운 기능의 함수를 추가할때, Base의 멤버변수에 접근하여 값을 바꾸고 싶다면 어떻게 해야할까?

Derived class에서 setValue함수가 Base의 멤버변수인 m_value를 바꾸려 시도하고 있다. m_value는 private라 접근이 불가능 한 상황이다. 이럴때는 멤버변수의 접근 지정자를 protected로 바꾸면 접근이 가능해진다.

 

하지만 한가지 의문이 든다? m_value의 값만 건드리는 함수라면 Base class에 구현하는 것이 옳바른 이치 아니겠는가? 그리고 이는 맞는 말이다. 정말 m_value의 값"만" 변경한다면 Base안에 구현하는것이 더 타당하다.

 

그럼 굳이 Derived class안에 구현한 이유는 무엇인가 말이다? 이는 m_value와 Derived의 멤버변수 둘다 상호작용하여 같이 사용해야하는경우 하위 class에서 접근하여 사용해야하는 것 이다.

이렇게 protected를 사용하는 방법 말고도 Base class안에서 getValue함수를 구현하고, 이를 Derived class안에서 사용하여 값을 이용할수도 있다. 하지만 이는 상위 class를 한번은 거쳐야 하는 방식이기에 효율면에서 떨어진다.

 

 11-7 상속받은 함수를 오버라이딩 하기

#include <iostream>

using namespace std;

class Base {
protected:
	int m_i;
public:
	Base(int value) : m_i(value) {}

	void print() {
		cout << "I'm Base" << endl;
	}
};

class Derived : public Base {
	double m_d;
public:
	Derived(int value) : Base(value) {}
};

int main()
{
	Base base(5);
	base.print(); // I'm Base 출력

	Derived derived(7);
	derived.print(); // I'm Base 출력

	return 0;
}
 

Base class나 이를 상속받는 Derived class나 모두 I'm Base를 출력한다. 같은 print()함수를 사용중이다.

 

만약 Derived에서 자기만의 출력 문을 만들고 싶다면, Derived 안에 같은 이름의 함수를 만들어줄 수 있을것이다.

class Derived : public Base {
	double m_d;
public:
	Derived(int value) : Base(value) {}

	void print() { // 새로 추가된 출력문
		cout << "I'm derived" << endl;
	}
};
 

결과로는

I'm Base

I'm derived 이라고 출력된다.

 

만약 같은 이름을 두고 부모 class에서도 함수를 만들어 기능을 사용하고싶고, 자식 class에서도 기능을 만들어 사용하고싶다면 위의 예제에서 Base와 Derived의 print()를 구분해줘야 한다. (이름을 동일하게 쓰는 이유는 다형성 때문이라 하셨다. 바로 다음 12쳅터에서 배운다)

class Derived : public Base {
	double m_d;
public:
	Derived(int value) : Base(value) {}

	void print() {
		Base::print();  // 추가된 부분 (Base의 함수임을 명시)
		cout << "I'm derived" << endl;
	}
};
 

위의 코드를 보면 Derived class 안의 print()에서 Base::print()를 호출하고, 자신의 I'm derived문도 출력하고 있다. 여기서 만약 잘못하고 앞에 Base를 Base::print()에서 빼먹고 print()만을 적어주면 무한loop에 빠지게 된다. 사용자가 생각으로는 부모 class의 print()를 호출할려 한거지만 실제로는 자식의 print()문이 loop를 돌게되는 것 이다. 위의 예제의 결과는 다음과 같다.

I'm Base <Base에서 호출>

I'm Base <Derived에서 호출>

I'm derived <Derived에서 호출>

 

출력 연산자의 오버로딩 또한 배웠다. 다음 코드를 같이 확인해 보자.

class Base {
protected:
	int m_i;
public:
	Base(int value) : m_i(value) {}

	void print() {
		cout << "I'm Base" << endl;
	}

	friend ostream& operator << (ostream& out, const Base& b) {
		out << "This is base output" << endl;
		return out;
	}
};

class Derived : public Base {
	double m_d;
public:
	Derived(int value) : Base(value) {}

	void print() {
		Base::print();
		cout << "I'm derived" << endl;
	}

	friend ostream& operator << (ostream& out, const Derived& b) {
		out << "This is derived output" << endl;
		return out;
	}
};

int main()
{
	Base base(5);
	cout << base;

	Derived derived(7);
	cout << derived;

	return 0;
}
 

위의 코드에서 Derived class의 print()호출시 Base::print()를 실행시킨후 자식 class의 I'm derived문이 출력되듯, << 연산자 오버로딩에서도 부모 class의 안의 << 오버로딩된 출력문을 먼저 출력한후, 자신의 오버로딩 된 출력을 하고싶다면 어떻게 해야할까?

 

참고로 << 연산자 오버로딩 함수는 Base의 friend함수이지, Base class의 일부분이 아니다. 이를 해결하기 위해서는 일종의 기술이 필요하다.

결과로 나오는 값

위의 코드를 보면 static_cast<Base>를 하고있는것을 볼 수 있다. 아직 감이 오지 않을 수 있다. 자식 class는 부모class로부터 상속을 받았고 따라서 내부적으로 부모class에 대한 정보를 메모리에 포함하고 있음을 기억하는가? 참고로 부모class의 정보가 메모리 앞주소에 먼저온다. 다음 나의 그림을 보면 조금더 이해갈 수 있다. (엄밀할 그림은 아니니 이해용도로만 사용해주길 바란다)

앞부분만의 메모리를 casting하여 사용하는 것 이다.

 

 11-8 상속 받은 함수를 감추기

#include <iostream>

using namespace std;

class Base {
protected:
	int m_i;
public:
	Base(int value) : m_i(value) {}

	void print() {
		cout << "I'm Base" << endl;
	}
};

class Derived : public Base {
	double m_d;
public:
	Derived(int value) : Base(value) {}

	using Base::m_i;
};

int main()
{
	Derived derived(7);
	derived.m_i = 1024;

	return 0;
}
 

위의 코드를 보면 Derived에서 Base를 상속한다. 하지만 변수 m_i는 protected이기 때문에 main에서 생성한 instance인 derived객체 에서는 접근할수가 없다. 이때 사용가능한 것이 Derived class안의 public범위에 using Base::m_i; 를 선언해주는 것 이다. 이것을 사용하면 Derived 범위 안에서는 m_i가 public이 되어버린다. 이처럼 상속받은 범위 지정자를 Derived class에서 바꿀수 있게된다.

 

위의 코드에서는 Derived class 내부에는 print함수가 없는 상태이다. 하지만 Base에 있는 print함수를 상속받아 사용이 가능한것은 이전시간에 배워 익히 알고있다. 이번에는 그것을 막는방법에 대하여 배웠다. 방법은 간단하다.

class Derived : public Base {
	double m_d;
public:
	Derived(int value) : Base(value) {}
	
	using Base::m_i;
private:
	using Base::print;  // private 범위안에서 선언됨
};
 

위의 코드와 같이 private범위 안에 using Base::print; 라고 선언해주면 된다. 참고로 이부분에서 print()처럼 괄호"()"를 붙혀서는 안된다. 괄호를 붙이는 것은 함수를 호출하는 행위이다. 함수이름 즉, 주소값만 필요하다.

derived.print()에서 빨간줄로 경고가 나타나고있음을 확인할 수 있다. 즉 Base에서는 print() 사용이 가능하지만 Derived에서는 사용 불가능해진 것 이다.

 

마지막으로 부모에서는 접근할 수 있지만, 자식에서는 접근할 수 없도록 막는 방법이 하나 더 있다.

바로 delete를 사용하는 것 이다. 이 delete는 동적할당된 메모리를 반환하는 delete가 아님에 주의해야함을 이전에 배운적 있다.

 

 11-9 다중 상속

#include <iostream>
using namespace std;

class USBDevice
{
private:
	long m_id;

public:
	USBDevice(long id) : m_id(id) {} //초기화

	long getID() { return m_id; }

	void plugAndPlay() {}
};

class NetworkDevice
{
private:
	long m_id;

public:
	class NetworkDevice(long id) : m_id(id) {}

	long getID() { return m_id; }

	void networking() {}
};

class USBNetworkDevice : public USBDevice, public NetworkDevice
{
public:
	USBNetworkDevice(long usb_id, long net_id)
		:USBDevice(usb_id), NetworkDevice(net_id) {}
};

int main()
{
	USBNetworkDevice my_device(3.14, 6.022);

	my_device.networking();
	my_device.plugAndPlay();

	my_device.USBDevice::getID();
	my_device.NetworkDevice::getID();

	return 0;
}
 

USBNetworkDevice는 USBDevice와 NetworkDevice를 모두 상속받고 있다. 이를 다중상속이라 한다. 다중상속받은 class는 생성자의 초기화 과정이 조금 까다로울 수 있다. 일반적으로 2개의 input을 받아 각 class의 생성자의 인자로 전달하여 우외적으로 초기화를 한다.

 

하지만 위의 코드를 보면 USBDevice와 NetworkDevice 모두 getID라는 함수가 있어 이름이 겹치게된다. 이러한 문제는 간단하게 my_device.USBDevice::getID()과 같은 형태로 해결할 수 있다.

 

또한 다이아몬드 상속에 대해서도 배웠다.

사진에 보이는 그대로 C가 B1과 B2를 상속받고 있으며, B1과 B2도 A를 상속받고 있다. 이러한 다이아 모양의 상속을 피해야 하는 방법중 하나이다. 무조건 기피하라는것은 아니라 하셨다. 우리가 사용하는 cin과 cout도 이러한 다이아 상속이라 알려주셨다.

 

2) 나의 현황

진짜 상속을 명확히 이해하기 위해 누가 읽든 이전 강의를 차분히 들어왔다면 내글만 보고도 다 점검 가능하도록 하나하나 상세히 풀어적었다. 디버깅 과정까지 하나하나 사진찍으며 올리고 나니 나 스스로의 이해도 또한 한층 높아졌다. 남은건 LearnCPP한번 쭉 읽어주기만 하면 될거같다.

이글의 모든 사진과 내용의 출처는 홍정모 교수님께 있습니다.

홍정모 교수님 블로그:

 

홍정모 연구소 : 네이버 블로그

안녕하세요! 홍정모 블로그에 오신 것을 환영합니다. 주로 프로그래밍 관련 메모 용도로 사용합니다. 강의 수강하시는 분들은 홍정모 연구소 카페로 오세요.

blog.naver.com

 

댓글