생성/소멸자 실행 순서와 가상 소멸자
- 상속 관계에서 생성자와 소멸자의 작동 과정에 대해 알아보자
생성/소멸자 실행 순서
#include <iostream>
using namespace std;
class Ice {
public:
Ice() { cout << "Ice()" << endl; }
~Ice() { cout << "~Ice()" << endl; }
};
class Pat {
public:
Pat() { cout << "Pat()" << endl; }
~Pat() { cout << "~Pat()" << endl; }
};
class Bingsoo {
public:
Bingsoo() {
cout << "Bingsoo()" << endl;
}
~Bingsoo() {
cout << "~Bingsoo()" << endl;
}
private:
Ice ice;
};
class PatBingsoo : public Bingsoo {
public:
PatBingsoo() {
cout << "PatBingsoo()" << endl;
}
~PatBingsoo() {
cout << "~PatBingsoo()" << endl;
}
private:
Pat pat;
};
int main() {
PatBingsoo *p = new PatBingsoo;
delete p;
}
전에 말했던 것처럼 자식 클래스의 생성자가 호출 될 때 먼저 부모 클래스의 생성자가 호출된다
PatBingsoo를 생성하면
Pat, Bingsoo, Ice의 생성자가 모두 호출될 것이다
- 소멸자도 마찬가지다.
생성자와 소멸자의 순서를 예측해보자
- Ice() // PatBingsoo의 부모 클래스인 Bingsoo의 멤버인 ice가 제일 먼저 생성되었다.
- Bingsoo() // Bingsoo의 멤버 ice가 생성되고 Bingsoo가 생성되었다.
- Pat() // PatBingsoo의 멤버 pat이 생성되었다
- PatBingsoo() // PatBingsoo의 멤버 pat이 생성되고 PatBingsoo가 생성되었다.
- ~PatBingsoo() // 자식 클래스인 PatBingsoo의 소멸자가 가장 먼저 호출된다
- ~Pat() // 자식 클래스의 멤버 변수인 Pat의 소멸자가 그 다음 호출된다
- ~Bingsoo() // 부모 클래스인 Bingsoo의 소멸자가 호출된다
- ~Ice() // 부모 클래스의 멤버 변수인 Ice의 소멸자가 그 다음 호출된다
소멸자는 생성자의 순서 역순이다
생성자의 순서가 왜 이렇게 될까?
팥빙수의 기본이 되는게 빙수이고, 빙수를 만들기 위해서는 얼음이 필요하다
생성자의 목적 자체가 멤버들을 초기화해주는 것이기 때문에, 멤버들이 먼저 존재한 다음에 그 멤버들을 초기화해줘야 한다
그래서 멤버들이 먼저 생성된 다음에 해당 클래스의 생성자가 호출되는 것이다.
얼음 -> 빙수, 팥-> 팥빙수
소멸자의 순서는 왜 이렇게 될까?
예를 들어 PatBingsoo의 멤버가 Pat을 가리킨다고 해보자
class PatBingsoo : public Bingsoo {
public:
PatBingsoo() {
cout << "PatBingsoo()" << endl;
pat = new Pat;
}
~PatBingsoo() {
cout << "~PatBingsoo()" << endl;
delete pat;
}
private:
Pat *pat;
};
pat을 먼저 delete를 해준 다음 포인터를 삭제해야 한다.
포인터가 먼저 삭제되면 동적할당 된 메모리를 해제할 수 없다
즉, 멤버가 사라지기 전에 소멸자가 호출되어 메모리 해제를 해준 뒤 멤버가 사라져야 한다.
헷갈림 주의!) 동적할당 된 메모리를 해제하는 것과, 멤버가 사라지는 것은 다르다.
- 멤버 변수가 사라지는 것은 해당 멤버 변수의 범위를 벗어나서 사라지는 것이다. 특정 함수 안에서 작동하는 지역 변수를 생각해보면 된다
그래서 파생된 것들을 먼저 삭제해준 다음 Base로 올라간다
Bingsoo의 ice를 동적할당 해보자
class Bingsoo {
public:
Bingsoo() {
cout << "Bingsoo()" << endl;
ice = new Ice;
}
virtual ~Bingsoo() {
cout << "~Bingsoo()" << endl;
delete ice;
}
private:
Ice *ice; // ice 객체가 아니기 때문에 ice 생성자가 호출되지 않음. ice = new Ice에서 호출됨.
};
ice라는 포인터가 만들어지는 시점은 Bingsoo() 생성자 호출 전이다
하지만 ice는 Ice타입 객체가 아니라, Ice 타입을 가리키는 Ice* 포인터이기 때문에, Ice 생성자가 호출되지 않는다
- 그래서 Bingsoo() 생성자가 호출된 후 Ice 객체를 생성하여 동적할당 할 때 Ice 생성자가 호출된다
가상 소멸자
소멸자도 가상 함수가 될 수 있다
이해를 돕기 위해 예시를 들어보겠다
- 위 예시에서 Bingsoo 포인터가 PatBingsoo 인스턴스를 가리키고, Bingsoo의 Ice와 PatBingsoo의 Pat이 동적할당을 한다고 해보자
#include <iostream>
using namespace std;
class Ice {
public:
Ice() { cout << "Ice()" << endl; }
~Ice() { cout << "~Ice()" << endl; }
};
class Pat {
public:
Pat() { cout << "Pat()" << endl; }
~Pat() { cout << "~Pat()" << endl; }
};
class Bingsoo {
public:
Bingsoo() {
cout << "Bingsoo()" << endl;
ice = new Ice;
}
~Bingsoo() {
cout << "~Bingsoo()" << endl;
delete ice;
}
private:
Ice *ice; // ice 객체가 아니기 때문에 ice 생성자가 호출되지 않음. ice = new Ice에서 호출됨.
};
class PatBingsoo : public Bingsoo {
public:
PatBingsoo() {
cout << "PatBingsoo()" << endl;
pat = new Pat;
}
~PatBingsoo() {
cout << "~PatBingsoo()" << endl;
delete pat; // 멤버 변수가 소멸하기 전에 소멸자를 호출해야 함. Patbingsoo -> pat
}
private:
Pat *pat;
};
int main() {
Bingsoo *p = new PatBingsoo;
delete p;
}
- 실행 결과
- Bingsoo()
- Ice()
- PatBingsoo()
- Pat()
- ~Bingsoo()
- ~Ice()
문제가 발생한다.
~PatBingsoo() 소멸자가 호출이 안되었다. 왜 이런 일이 발생했을까?
p는 Bingsoo 타입을 가리키는 포인터이다.
delete p; 를 하면, 컴파일러는 p가 어떤 객체를 가리키는지 정확히 할 수 없기 때문에 정적 바인딩이 일어나고, 따라서 ~PatBingsoo()의 소멸자가 호출되지 않은 것이다.
따라서 이 문제를 해결하려면 소멸자에서도 동적 바인딩이 일어나도록 해주면 된다.
Bingsoo의 소멸자 ~Bingsoo() 또한 virtual로 선언해주면 된다
소멸자 동적 바인딩이 발생함
런타임에 포인터가 PatBingsoo 타입의 인스턴스를 가리킴을 확인하고 PatBingsoo의 소멸자를 호출함
class Bingsoo {
public:
Bingsoo() {
cout << "Bingsoo()" << endl;
ice = new Ice;
}
virtual ~Bingsoo() {
cout << "~Bingsoo()" << endl;
delete ice;
}
private:
Ice *ice; // ice 객체가 아니기 때문에 ice 생성자가 호출되지 않음. ice = new Ice에서 호출됨.
};
- 코드를 올바르게 수정한 후 실행 결과
- Bingsoo()
- Ice()
- PatBingsoo()
- Pat()
- ~PatBingsoo()
- ~Pat()
- ~Bingsoo()
- ~Ice()
- 소멸자는 왠만하면 가상으로 선언해주는 것이 좋다
내용을 제대로 이해하셨다면, 코드를 보고 호출되는 생성자/소멸자 순서를 바로 파악하실 수 있을거에요
참조) 두들낙서 C/C++ 강좌
'Programming Language > C++' 카테고리의 다른 글
[C++] 상속에서의 형변환 - 다운캐스팅 (0) | 2024.06.27 |
---|---|
[C++] 상속에서의 형변환 - 업캐스팅(Upcasting) (0) | 2024.06.27 |
[C++] 순수 가상 함수와 추상 클래스 (0) | 2024.06.27 |
[C++] 상속이 필요한 이유(2) (0) | 2024.06.27 |
[C++] 가상 함수(Virtual Function)와 동적 바인딩(Dynamic Binding) (0) | 2024.06.27 |