Computer Language/C & C++

[C++] 상속 2) 상속 관계의 생성/소멸자

HONGGG 2023. 12. 19. 02:17

상속 관계 생성자/소멸자

 

상속 받은 클래스는 어떤 생성자/소멸자를 호출하는가?

 

상속 관계가 성립되면 생성자와 소멸자의 작동이 복잡해지며 중요한 측면이 되기도 한다. C++에서는 파생 클래스가 생성되거나 소멸될 때 기본 클래스의 생성자와 소멸자가 어떻게 호출되는지에 대한 규칙이 있다.

 

우선 상속 관계에서 자식 클래스의 생성자가 호출된다면 반드시 부모 클래스의 생성자도 호출되어야 한다. 이를 따로 구현하지 않는다면, 부모 클래스의 기본 생성자를 먼저 호출한다.

 

  1. 부모 클래스의생성자를먼저 호출한다.
  2. 클래스의 모든 멤버 변수가 생성된다. (즉, 멤버 변수들의 생성자가 호출된다.)
  3. 생성자의 본문이 실행된다.

 

상속 관계 생성자 호출
#include <iostream>

using namespace std;

class Base {
public:
    Base() {
        cout << "Base 클래스 생성자 호출" << endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        cout << "Derived 클래스 생성자 호출" << endl;
    }
};

int main() {
    Derived derivedObj;
    return 0;
}
Base 클래스 생성자 호출
Derived 클래스 생성자 호출

 

 

부모가 기본생성자가 없는 경우

그런데 만약 부모 클래스에 기본 생성자가 없다면 어떻게 될까? 그럴 경우 두가지 결과가 나올 수 있다.

 

  1. 기본 생성자 외 다른 생성자가 존재하는 경우, 기본생성자를 호출하면 오류가 발생한다.
  2. 아무 생성자도 없는 경우, C++ 클래스는 암시적 기본 생성자가 있기에 정상 작동한다.

 

부모 클래스에 생성자가 없는 경우
#include <iostream>

using namespace std;

class Base {
public:
    // 부모 클래스에는 기본 생성자도 없고, 다른 생성자도 없음
};

class Derived : public Base {
public:
    // 파생 클래스에서 명시적으로 부모 클래스의 생성자를 호출해야 함
    Derived() {
        cout << "Derived 클래스 생성자 호출" << endl;
    }
};

int main() {
    // Derived 클래스의 객체를 생성하면 Base 클래스의 기본 생성자 호출을 못하는 상황
    Derived derivedObj;
    return 0;
}
Derived 클래스 생성자 호출

 

부모 클래스에 다른 생성자가 있는 경우
#include <iostream>

using namespace std;

class Base {
public:
    // 부모 클래스에는 기본 생성자는 없고, 다른 생성자가 있다.
    Base(int a) {
        cout << a << "를 받았다." << endl; 
    }
};

class Derived : public Base {
public:
    // 파생 클래스에서 명시적으로 부모 클래스의 생성자를 호출해야 함
    Derived() {
        cout << "Derived 클래스 생성자 호출" << endl;
    }
};

int main() {
    // Derived 클래스의 객체를 생성하면 Base 클래스의 기본 생성자 호출을 못하는 상황
    Derived derivedObj;
    return 0;
}
main.cpp: In constructor ‘Derived::Derived()’:
main.cpp:16:15: error: no matching function for call to ‘Base::Base()’
   16 |     Derived() {
      |               ^
main.cpp:8:5: note: candidate: ‘Base::Base(int)’
    8 |     Base(int a) {
      |     ^~~~
main.cpp:8:5: note:   candidate expects 1 argument, 0 provided
main.cpp:5:7: note: candidate: ‘constexpr Base::Base(const Base&)’
    5 | class Base {
      |       ^~~~
main.cpp:5:7: note:   candidate expects 1 argument, 0 provided
main.cpp:5:7: note: candidate: ‘constexpr Base::Base(Base&&)’
main.cpp:5:7: note:   candidate expects 1 argument, 0 provided

 

이러한 결과가 생기는 이유는 앞서 다른 포스트에 기록한 생성자의 규칙 때문인데, 간단하게 말하면 기본 생성자는 암시적으로 보이지 않게 반드시 존재하지만, 다른 생성자가 선언되면 암시적 기본 생성자는 오버라이딩되어 제거된다.

 

그렇다면 자식이 기본생성자를 사용한다면 부모 객체도 무조건 기본 생성자를 선언해야 하는가? 이것 또한 아니다.

다음 예제를 보며 설명을 들어보자.

 

 

다른 생성자 호출
#include <iostream>

using namespace std;

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

class Derived : public Base {
public:
    Derived() : Base(0) { // Base(int) 생성자 호출
        cout << "Derived" << endl;
    }
};

int main() {
    Derived derivedObj; // Derived 클래스의 객체 생성
    return 0;
}

 

위 코드에서 자식 클래스의 기본 생성자가 부모의 매개변수를 받는 생성자를 우선적으로 호출하고 있다. 이처럼 자식 클래스가 호출하려는 부모 클래스의 생성자를 지목할 경우 기본 생성자를 찾지 않게 되고 오류가 발생하지 않는다.

 

그렇다면 자식 클래스가 부모 클래스와 동일한 형태의 생성자를 가진다면? 기본 생성자가 오버라이딩 된것과 같이 생성자를 지정하지 않아도 동작할까?

더보기

동일한 조건의 생성자가 있더라도 호출할 생성자를 지목하지 않는다면 모든 생성자는 기본생성자를 호출하려 한다.

 

동일한 형태의 생성자라도 오류 발생
#include <iostream>

using namespace std;

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

class Derived : public Base {
public:
    Derived(int b) { // Base(int) 생성자 호출
        cout << "Derived " << b << endl;
    }
};

int main() {
    Derived derivedObj(1); // Derived 클래스의 객체 생성
    return 0;
}
main.cpp: In constructor ‘Derived::Derived(int)’:
main.cpp:14:20: error: no matching function for call to ‘Base::Base()’
   14 |     Derived(int b) { // Base(int) 생성자 호출
      |                    ^
main.cpp:7:5: note: candidate: ‘Base::Base(int)’
    7 |     Base(int a) {
      |     ^~~~
main.cpp:7:5: note:   candidate expects 1 argument, 0 provided
main.cpp:5:7: note: candidate: ‘constexpr Base::Base(const Base&)’
    5 | class Base {
      |       ^~~~
main.cpp:5:7: note:   candidate expects 1 argument, 0 provided
main.cpp:5:7: note: candidate: ‘constexpr Base::Base(Base&&)’
main.cpp:5:7: note:   candidate expects 1 argument, 0 provided

 


상속 관계의 소멸자

상속 관계의 소멸자는 생성자와 정반대의 실행 순서를 가진다. 자식 클래스는 부모 클래스의 확장한 것이기 떄문에 자식 클래스의 멤버들이 부모 클래스의 상태나 특성에 의존하는 경우가 발생할 수도 있기 때문이다.

 

소멸자는 객체가 소멸될 때 호출된다. 따라서 어떤 클래스의 소멸자가 호출될 때도 자신의 멤버를 소멸하고, 부모의 소멸자를 호출한다. 이는 자식을 포용하고 있는 부모가 먼저 사라지면 두 상속 관계에 연관된 데이터의 의존성이 손상될 수 있기 때문이다.

 

  1. 소멸자의 본문이실행된다.
  2. 클래스의 모든 멤버 변수가 소멸된다. (즉, 멤버 변수들의 소멸자가 호출된다.)
  3. 부모 클래스의 소멸자를 마지막에 호출한다.

 

#include <iostream>

using namespace std;

class Base {
public:
    Base() {
        cout << "Base 클래스 생성자 호출" << endl;
    }

    ~Base() {
        cout << "Base 클래스 소멸자 호출" << endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        cout << "Derived 클래스 생성자 호출" << endl;
    }

    ~Derived() {
        cout << "Derived 클래스 소멸자 호출" << endl;
    }
};

int main() {
    Derived derivedObj; // Derived 클래스의 객체 생성
    return 0;
}
Base 클래스 생성자 호출
Derived 클래스 생성자 호출
Derived 클래스 소멸자 호출
Base 클래스 소멸자 호출

 

 


결론

상속 관계는 복잡하지만 간단하게 이해할 수 있는 메커니즘이 많다. 특히 생성자와 소멸자의 경우 다음과 같이 생각해보자.

 

생성자 호출 순서:

  • 객체 초기화: 객체가 생성될 때, 해당 객체의 모든 부분이 초기화되어야 한다. 부모 클래스에 속한 멤버 변수나 메모리 등이 초기화되어야 하는데, 이를 담당하는 것이 부모 클래스의 생성자이다.
  • 초기화 순서: 자식 클래스의 생성자에서 부모 클래스의 생성자를 호출함으로써, 부모 클래스의 초기화 작업이 먼저 이루어지게 된다. 이것은 객체의 정확한 상태를 보장하고 상속된 멤버들이 올바르게 초기화되도록 한다.

소멸자 호출 순서:

  • 정리 작업: 객체가 소멸될 때, 해당 객체의 자원을 정리하고 해제해야 한다. 이는 주로 소멸자에서 수행된다.
  • 역순 호출: 소멸자 호출 순서는 생성자 호출 순서의 역순으로 이루어진다. 자식 클래스의 소멸자가 먼저 호출되고, 이후에 부모 클래스의 소멸자가 호출된다. 이는 파생 클래스에서 추가된 자원을 먼저 해제하고, 그 후에 부모 클래스의 자원을 정리하기 위함이다.