KoreanFoodie's Study

C++ DevNote : 파생 클래스에서 기반 클래스 생성자 제대로 호출하기 본문

Tutorials/C++ : Expert

C++ DevNote : 파생 클래스에서 기반 클래스 생성자 제대로 호출하기

GoldGiver 2022. 12. 27. 16:52

C++ 에 대해 공부한 것과, 개발하면서 알게 된 것들을 다룹니다

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

사실 파생 클래스가 생성될 때, 기반 클래스 생성자가 호출된다는 건, C++ 을 조금이라도 해 본 사람은 누구나 알고 있는 사실이다. 또한 함수의 생성자/소멸자의 호출 순서가 다음과 같다는 것도, 익히 알려진 사실이다. 즉, A 클래스가 기반 클래스이고, B 클래스가 파생 클래스라고 가정했을 때 B 클래스 변수를 선언하면 호출 순서가 다음과 같이 나올 것이다 :

  1. A 클래스 생성자
  2. B 클래스 생성자
  3. B 클래스 소멸자
  4. A 클래스 소멸자

 

하지만 한 가지 기억해야 할 것은, 파생 클래스를 만들 때 호출되는 기반 클래스 생성자는 '인자가 없는 기반 클래스의 생성자' 이다. 즉, 인자를 받는 형식의 기반 클래스 생성자를 호출해주고 싶으면, 추가적인 작업을 해 주어야 한다는 뜻이다!

다음과 같은 코드를 보자.

#include <iostream>

using namespace std;

class Test
{
public:
    int a;
    
    Test() : a(0) {}
    Test(int i) : a(i) {}
    
    void poo() { cout << "a is " << a << endl; }
};

class A 
{
public:
    Test* test;
    virtual void foo() { test->poo(); }
    
    A(int i) { test = new Test(i); }
    A() { test = new Test(-1); }
    
    virtual ~A() { 
        cout << "A Deconstructor" << endl;        
    }
};

class B : public A
{
public:

    B(int i) { }

    virtual ~B() { 
        cout << "B Deconstructor" << endl;        
    }
};

int main() {
    
    B b(1);
    b.foo();

    return 0;
}

사용자는 B 클래스에 1 을 전달하고, "a is 1" 이라는 출력값을 기대했다고 가정하자. 왜냐하면 기반 클래스 생성자인 A(int i) 가 불릴 것이라고 생각했으므로.

하지만 실제로 불리는 것은 A(int i) 형식이 아닌 A() 형식이 된다.

원하는 목적을 얻기 위해서는, B 의 생성자에서 원하는 A 의 생성자를 잘 호출해 주어야 한다 :

B(int i) : A(i) { }

B 의 생성자에서 위와 같이 A 의 생성자를 잘 호출해 주면, 결과값이 이제 "A is 1" 이 잘 나오게 된다.

 

참고로, 다음과 같이 쓰는 것은 생성자 body 내에서 A 타입의 임시 변수를 만드는 것일 뿐, B 클래스의 생성자에는 아무런 영향을 끼치지 못한다!

B(int i) { A(i); }

 

 

그 외 예시들

그럼 이제 재미있는 예시들을 조금씩 살펴보도록 하자.

 

1. 다중상속 (여러 클래스로부터 상속받을 경우) 의 경우

다음과 같은 코드가 있다고 하면, 생성자와 호출자의 순서는 어떻게 될까?

#include <iostream>

using namespace std;

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

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

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

int main()
{
    C c;
}

C 클래스는 A, B 클래스를 상속받는데, 순서가 B, A 순이다. 이 경우, 상속받은 순서대로 생성자/소멸자가 호출된다(답은 더보기에).

더보기

B 생성
A 생성
C 생성
C 소멸
A 소멸
B 소멸

 

2. 파생 클래스의 생성자에 기반 클래스 변수가 있을 경우

다음과 같은 코드를 보자.

#include <iostream>

using namespace std;

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


int main()
{
    B b;
}

 

이 경우는 출력 결과가 어떻게 될까? 답은 더보기에 있다.

더보기

A 생성
A 생성
B 생성
B 소멸
A 소멸
A 소멸

즉, 먼저 B 를 생성하기 위해 기반 클래스 생성자인 A 의 생성자가 호출되고, B 의 멤버 변수인 a 를 초기화하기 위해 A 의 생성자가 또 불리게 된다. 마지막으로 B 의 생성자가 불린다!

 

다음과 같이 초기화 리스트에서 멤버 변수를 초기화 하면 어떻게 될까?

#include <iostream>

using namespace std;

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

int main()
{
    A a;
    B b(a);
}

결과는 상당히 신기하게 나올 것이다(더보기 참고).

더보기

A 생성
A 생성
B 생성
A 소멸
B 소멸
A 소멸
A 소멸
A 소멸

출력 결과를 보면 생성자는 3번, 소멸자는 5번 호출이 되고 있다. 이유가 무엇일까?

 

사실 나머지 2 건은 복사 생성자 때문에 불린 케이스이다. 코드를 다음과 같이 고치면, 그 이유가 좀 더 명확해진다!

#include <iostream>

using namespace std;

class A {
public:
    A() { cout << "A Con" << endl; }
    A(A const&) { cout << "A Con (Copy)" << endl; }
    virtual ~A() { cout << "A Dec" << endl; }
};
class B : A {
public:
    A a;
    B(A _a) : a(_a) { 
        cout << "B Con" << endl;
    }
    virtual ~B() { cout << "B Dec" << endl; }    
};

int main()
{
    A a;
    B b(a);
}

정답은 접은글을 참고하자.

더보기

A Con               -> main 함수에 있는 a 생성자 호출
A Con (Copy)    -> main 함수에서 복사 생성자 호출. 임시 변수 생성
A Con                -> main 함수에 있는 b 의 기반 클래스 생성자 호출
A Con (Copy)    -> B 생성자에서 멤버 변수인 a 를 초기화 하는데 복사 생성자 호출
B Con               -> main 함수의 b 생성자 호출
A Dec                -> B 생성자에서 복사 생성자로 임시로 만든 _a 에 대한 소멸자 호출
B Dec                -> main 함수에 있는 b 의 소멸자 호출
A Dec                -> B 클래스의 내부에 있는 멤버 변수 a 의 소멸자 호출
A Dec                -> main 함수에 있는 b 의 기반 클래스 소멸자 호출
A Dec                -> main 함수에 있는 a 변수의 소멸자 호출

클래스 내부에 있는 멤버 변수의 소멸자는, 해당 클래스의 소멸자 이후에 불린다! B 에 선언된 'A a;` 변수의 소멸자의 경우, B 의 소멸자 호출 이후 - A 의 소멸자 호출 이전에 불린다!

 

혹여나 정말로 극한까지 테스트를 해 보고 싶다면. 아래 코드를 돌려보자!

#include <iostream>

using namespace std;

class C {
public:
    virtual ~C() { cout << "C Dec" << endl; }    
};

class D {
public:
    virtual ~D() { cout << "D Dec" << endl; }    
};

class A {
public:
    C c;
    A() { cout << "A Con" << endl; }
    A(A const&) { cout << "A Con (Copy)" << endl; }
    virtual ~A() { cout << "A Dec" << endl; }
};
class B : A {
public:
    A a;
    D d;
    B(A _a) : a(_a) { 
        cout << "B Con" << endl;
    }
    virtual ~B() { cout << "B Dec" << endl; }    
};


int main()
{
    A a;
    B b(a);
}

위 코드의 출력값을 보면 생성자와 소멸자의 호출 순서를 다시는 잊지 않을 수 있을 것이다!

 

참고
Comments