다형성

Ryan Ham·2024년 5월 29일
0

C++

목록 보기
19/26

다형성

""모습(포인터의)은 같은데 형태는 다르다.""
다음 코드를 보면 위의 말을 한번에 이해할 수 있을 것이다.

#include <iostream>
using namespace std;

class First {
public : 
	virtual void SimpleFunc() {
		cout << "First" << endl;
	}
};

class Second : public First{
public:
	virtual void SimpleFunc() {
		cout << "Second" << endl;
	}
};

int main() {
	First* ptr = new First();
    // (1)
	ptr->SimpleFunc();
	delete ptr;

	// (2)
	ptr = new Second();
	ptr->SimpleFunc();
	delete ptr;
	return 0;
}

코드에서 주석 처리된 (1)과 (2)를 잘 보자. 다른 부분들을 다 제껴놓고 이 두 문장만 딱 비교해서 본다면 완전히 동일한 문장이다. 하지만, 두 문장은 서로다른 결과를 내놓는다.

우리가 일반적으로 class를 만들고 상속을 하는 경우를 가정해보자. CPP에서는 부모 포인터로 자식 객체를 가르키는 upcasting이 가능하다. 그리고 이 upcasting은 상속 관계에서 포인터 관리를 하는데 매우매우 유용하게 쓰일 수 있다. (여담이지만, upcasting과 downcasting의 방향이 헷갈릴 때는 부모를 위에, 자식을 아래에 있는 트리 구조를 상상해보자. 또한 downcasting은 나중에 한번 다룰 dynamic_casting과 같이 가는데 이 둘을 DD형제로 기억해보자 ㅎㅎ)

하지만, upcasting을 할때 우리는 객체의 입장에서 멤버 변수와 함수를 부르는 것이 아니라 포인터의 입장에서 바라볼 수 밖에 없다.

코드 예시

이 상황을 타개하기 위해서 virtual이라는 것과 다형성의 개념이 들어오게 된다. 다시 이 글의 첫 번째 코드로 돌아가서, virtual이 정의되지 않은 일반 클래스 상속 구조였다면, (1)과 (2)는 둘다 First* 포인터 타입이므로 First class안에 있는 SimpleFunc() 함수가 불러지게 되어야 한다.

가상 소멸자

가상함수가 없는 일반적인 자식 클래스의 생성과 소멸의 과정을 살펴보면,
부모 클래스 생성자 -> 자식 클래스 생성자 -> 자식 클래스 소멸자 -> 부모 클래스 소멸자
과 같은 과정을 거친다. 이는 스택에 LIFO 형태로 클래스를 넣고 빼는 상상을 하면 쉽게 순서를 이해할 수 있을 것이다.

#include <iostream>
#include <cstring>
using namespace std;

class First {
private:
	char* strOne;
public :
	First(const char* str) {
		strOne = new char[strlen(str) + 1];
		cout << "First 생성자" << endl;
	}
	~First() {
		cout << "First 소멸자" << endl;
		delete[] strOne;
	}
};

class Second : public First {
private:
	char* strTwo;
public :
	Second(const char* str1, const char* str2) : First(str1) {
		strTwo = new char[strlen(str2) + 1];
		cout << "Second 생성자" << endl;
	}
	~Second() {
		cout << "Second 소멸자" << endl;
		delete[] strTwo;
	}
};

int main() {

	First* ptr = new Second("simple", "complex");
	delete ptr;
	return 0;
}

하지만, 다음의 코드를 보자. 동적 할당된 객체(자식)의 주소를 부모 포인터가 받고 있다. 이렇게 upcasting된 상황에서 부모 포인터를 동적 해제 시켜버린다. 이런 경우 생성자와 소멸자의 호출 형태는 어떻게 될까?

우선, new Second()를 할때 부모의 생성자와 자식의 생성자가 차례대로 생성된다. 그 다음, delete ptr에서 소멸자가 실행되게 되는데 여기서 소멸자를 virtual로 선언하지 않으므로 객체의 중심이 아닌 포인터의 중심으로 연산이 진행되게 된다. 따라서 First* 포인터 타입의 ptr이 동적 해제된 경우이므로 First의 소멸자만 호출되게 되고 코드가 끝나게 된다. 결국 Second의 생성자는 호출되었지만 그 소멸자는 호출되지 않았으므로 memory leak이 발생하게 된다.

마찬가지로 이러한 방법을 해결하기 위해서는 단순히 소멸자에 virtual만 붙여주면 된다. 간편한 점은 부모 소멸자에 virtual을 붙이면 자식 소멸자에는 따로 붙이지 않아도 자동으로 virtual이 된다는 사실!!

profile
🏦KAIST EE | 🏦SNU AI(빅데이터 핀테크 전문가 과정) | 📙CryptoHipsters 저자

0개의 댓글