C++/C++ 기초

[C++] 파생 클래스와 기본 클래스의 생성자/소멸자 호출 관계 + 가상 소멸자

Song 컴퓨터공학 2023. 7. 17. 18:19
 

[C++] 생성자와 소멸자 ( Constructor and Destructor )

포스팅의 목차 / 링크 1. 생성자 (Constructor) 2. 디폴트 생성자 (Default Constructr) 3. 복사 생성자 (Copy Constructor) 4. 소멸자 (Destructor) 5. 생성자/소멸자 실행 순서 클래스를 통해 객체를 생성하면, 해당 객

songsite123.tistory.com

클래스는 생성자와 소멸자가 최소 1개씩 있으며, 객체의 생성과 호출에 생성자와 소멸자가 각각 실행되는 걸 위 포스팅에서 배웠습니다. 오늘 포스팅은 상속에 초점을 맞추어 파생 클래스의 객체가 생성되거나 소멸 될 때, 기본 클래스의 생성자와 소멸자가 호출되는지, 파생 클래스의 생성자와 소멸자가 호출되는지, 그 사이 관계성을 알아보는 내용을 다룹니다.

 


이에 대한 내용의 개념 체크에 유명한 2개의 질문이 있습니다.

 

Q1. 파생 클래스 객체가 생성될 때 파생클래스의 생성자와 기본 클래스의 생성자가 모두 실행되는가?

Q2. 실행된다면 기본 클래스의 생성자와 파생 클래스의 생성자 중 어떤 것이 먼저 실행되는가?

 

이에 대한 정답은 파생 클래스 객체가 생성될 때 파생 클래스의 생성자와 기본 클래스의 생성자가 모두 호출되며, 기본 클래스의 생성자가 먼저 실행되고 파생 클래스의 생성자가 실행됩니다. 이는 아주 간단한 코드로 확인해볼 수 있습니다.

#include <iostream>
using namespace std;

class A {
public:
	A() { cout << "생성자 A" << endl; }
	~A() { cout << "소멸자 A" << endl; }
};

class B : public A {
public:
	B() { cout << "생성자 B" << endl; }
	~B() { cout << "소멸자 B" << endl; }
};

class C : public B {
public:
	C() { cout << "생성자 C" << endl; }
	~C() { cout << "소멸자 C" << endl; }
};

int main() {
	C object;
}

기본 클래스부터 생성자가 실행되고, 가장 마지막에 생성자가 실행된 객체부터 소멸자가 실행되는 것을 알 수 있습니다.

 

그러면 파생 클래스보다 기본 생성자의 호출이 먼저 되는 것일까요? 그건 또 아닙니다.

 

가장 먼저 파생 클래스의 생성자를 호출하게 됩니다. 그런데 컴파일러가 생성자를 컴파일할 때, 만약 상속 받은 클래스가 있다면 자신이 실행되기 전에 상위 클래스의 생성자를 호출한다 라고 번역합니다. 따라서 실행은 A→B→C 지만 실상 호출은 C부터 호출되고, C가 실행되기 전에 B가 호출되고, B가 실행되기 전에 A가 호출되고, A는 상속받은 클래스가 없으므로 A가 실행되고, B가 실행되고, C가 실행되는 것입니다. 이런 관계를 잘 알아놔야 밑의 생성자 매개변수 전달도 이해할 수 있습니다.

 

이 과정을 이렇게 암기하듯 생각하지 않아도 파생 클래스에서는 기본 클래스의 멤버를 사용할 수 있는데, 당연히 초기화가 먼저 일어나야 하므로 기본 클래스의 생성자가 먼저 실행된다고 생각해도 됩니다.

 

소멸자의 경우 파생 클래스의 소멸자를 컴파일 할 때 파생 클래스의 소멸자 코드를 먼저 실행하고 상속 받은 클래스의 소멸자를 호출하도록 컴파일되기 때문에 생성자의 실행 순서와 반대로 실행 됩니다. 소멸자는 호출 순서와 실행 순서가 같고, 생성자는 호출 순서와 실행 순서가 다릅니다. 이는 컴파일러의 컴파일 과정이 다르기 때문에 발생하는 일입니다.

 

 

 파생 클래스에서 기본 클래스의 생성자 호출

 

그런데 맨 위의 포스팅에서 기본 생성자만 사용하지 않고 함수 오버로딩을 통해 여러 매개변수를 주거나 디폴트 매개변수로 초기값까지 지정해주는 것도 배웠습니다. 그렇다면 파생 클래스에서 기본 클래스의 생성자 중 특정 매개변수를 가지는 생성자는 어떻게 호출할까요?

 

원래 기본적으로 파생 클래스의 생성자를 작성할 때 함께 실행할 기본 클래스의 생성자를 지정 해야 합니다. 지정하지 않으면 자동적으로 컴파일러가 기본 생성자를 호출하도록 컴파일 해주던 겁니다. 파생 클래스의 생성자에서 기본 클래스의 생성자를 명시하는 방법은 상속받는 방법과 동일합니다.

 

파생클래스생성자(매개변수) : 기본클래스생성자(매개변수) {}

 

위 방식을 통해 기본 클래스 생성자의 호출을 명시적으로 선택할 수 있습니다. 원래 아무것도 사용하지 않으면 컴파일러가 : 기본클래스이름() 이라는 기본 생성자를 호출했던 겁니다. 배열에서 매개변수가 있는 생성자를 선언하면 기본 생성자가 생성되지 않는 것처럼 기본 클래스에서 생성자를 선언할 때 기본 생성자를 호출할 일이 있다면 따로 선언해주거나 디폴트 매개변수를 통해 기본생성자를 만들어놔야 파생 클래스에서 호출할 때 오류가 발생하지 않습니다.

 

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

class TV {
	int size;
public:
	TV() { size = 20; }
	TV(int size) { this->size = size; }
	int getSize() { return size; }
};

class WideTV : public TV {
	bool videoln;
public:
	WideTV(int size, bool videoln) :TV(size) {
		this->videoln = videoln;
	}
	bool getVideoln() { return videoln; }
};

class SmartTV : public WideTV {
	string ipAddr;
public:
	SmartTV(string ipAddr, int size) : WideTV(size, true) {
		this->ipAddr = ipAddr;
	}
	string getipAddr() { return ipAddr; }
};

int main() {
	SmartTV htv("192.0.1", 32);
	cout << "size=" << htv.getSize() << endl;
	cout << "videoln=" << boolalpha << htv.getVideoln() << endl;
	cout << "ipAddr=" << htv.getipAddr() << endl;
}

클래스의 생성자를 위주로 봐주시면 됩니다. 바깥으로 빼서 선언하는 경우 원형에서는 명시하지 않고 선언부에서 명시적으로 상위 클래스의 생성자를 명시해주면 됩니다.

class TV {
	int size;
public:
	TV() { size = 20; }
	TV(int size);
	int getSize() { return size; }
};

TV::TV(int size){ this->size = size; }

class WideTV : public TV {
	bool videoln;
public:
	WideTV(int size, bool videoln);
	bool getVideoln() { return videoln; }
};

WideTV::WideTV(int size, bool videoln) :TV(size) {
	this->videoln = videoln;
}

class SmartTV : public WideTV {
	string ipAddr;
public:
	SmartTV(string ipAddr, int size);
	string getipAddr() { return ipAddr; }
};

SmartTV::SmartTV(string ipAddr, int size) : WideTV(size, true) {
	this->ipAddr = ipAddr;
}

 

 

 

 가상 소멸자

 

[C++] 가상 함수와 오버라이딩 ( Virtual function and overriding )

오버라이딩이란, 부모 클래스로부터 상속받은 메소드의 내용을 재정의(변경) 해야 하는 것을 오버라이딩이라고 합니다. 다른 말로 파생 클래스에서 기본 클래스에 작성된 가상 함수를 재작성하

songsite123.tistory.com

위 포스팅에서 가상 함수가 무엇인지에 대해 배웠는데요, 기본적으로 기본 클래스의 소멸자는 가상 함수로 선언하는 것이 좋습니다. 그 이유는 파생 클래스의 객체가 기본 클래스에 대한 포인터로 delete 되는 상황, 즉 업 캐스팅 된 상황에서 메모리를 해제할 때의 상황에서도 정상적인 소멸이 되게 하기 위함입니다.

Base *p = new Derived();
delete p;

위 같은 코드를 수행하면 p가 Base 형 포인터이므로 컴파일러는 ~Base() 소멸자를 호출하도록 컴파일 됩니다. 그렇게 되면 ~Base() 만 수행되고, ~Derived() 는 실행되지 않습니다. 만약 이 때 ~Base() 가 virtual 키워드로 선언된 가상함수라면, ~Base()에 대한 호출이 동적 바인딩에 의해 ~Derived()에 대한 호출로 변해 ~Derived() 가 실행됩니다. 그리고 기본적으로 위에서도 배웠듯 파생 클래스의 소멸자는 자신이 실행된 후 기본 클래스의 소멸자를 호출하도록 컴파일 되기 때문에 ~Derived() 코드 실행 후에 ~Base() 코드가 실행되어 기본 클래스와 파생클래스의 소멸자가 모두 순서대로 실행됩니다.

 

#include <iostream>
using namespace std;

class Base {
public:
	~Base() { cout << "~Base()" << endl; }
};

class Derived : public Base {
public:
	~Derived() { cout << "~Derived()" << endl; }
};

int main() {
	Derived* dp = new Derived();
	Base* bp = new Derived();	// 업 캐스팅

	delete dp;	// Derived 포인터로 소멸
	delete bp;	// Base 포인터로 소멸
}

vitrual 키워드를 사용하지 않는 경우, Derived 포인터로 Derived 객체를 delete 하는 경우에는 2개의 소멸자가 모두 호출되지만, 업 캐스팅한 Base 포인터로 delete 하는 경우는 ~Base() 만 실행됩니다. 여기에서 ~Base() 함수만 가상 소멸자로 대체하면

#include <iostream>
using namespace std;

class Base {
public:
	virtual ~Base() { cout << "~Base()" << endl; } // 가상 소멸자
};

class Derived : public Base {
public:
	~Derived() { cout << "~Derived()" << endl; }
};

int main() {
	Derived* dp = new Derived();
	Base* bp = new Derived();	// 업 캐스팅

	delete dp;	// Derived 포인터로 소멸
	delete bp;	// Base 포인터로 소멸
}

 

Base 포인터로 delete 를 해도 정상적으로 Derived 와 Base 의 소멸자가 모두 호출되게 됩니다. 따라서 기본 클래스의 소멸자는 가상 소멸자로 선언하는 것이 좋습니다.