Programming Language/C++

[C++] 생성/소멸자 실행 순서와 가상 소멸자

lumana 2024. 6. 27. 23:13

생성/소멸자 실행 순서와 가상 소멸자

  • 상속 관계에서 생성자와 소멸자의 작동 과정에 대해 알아보자

생성/소멸자 실행 순서

#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의 생성자가 모두 호출될 것이다

      • 소멸자도 마찬가지다.
  • 생성자와 소멸자의 순서를 예측해보자

  1. Ice() // PatBingsoo의 부모 클래스인 Bingsoo의 멤버인 ice가 제일 먼저 생성되었다.
  2. Bingsoo() // Bingsoo의 멤버 ice가 생성되고 Bingsoo가 생성되었다.
  3. Pat() // PatBingsoo의 멤버 pat이 생성되었다
  4. PatBingsoo() // PatBingsoo의 멤버 pat이 생성되고 PatBingsoo가 생성되었다.
  5. ~PatBingsoo() // 자식 클래스인 PatBingsoo의 소멸자가 가장 먼저 호출된다
  6. ~Pat() // 자식 클래스의 멤버 변수인 Pat의 소멸자가 그 다음 호출된다
  7. ~Bingsoo() // 부모 클래스인 Bingsoo의 소멸자가 호출된다
  8. ~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;  
}
  • 실행 결과
  1. Bingsoo()
  2. Ice()
  3. PatBingsoo()
  4. Pat()
  5. ~Bingsoo()
  6. ~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에서 호출됨.
};
  • 코드를 올바르게 수정한 후 실행 결과
  1. Bingsoo()
  2. Ice()
  3. PatBingsoo()
  4. Pat()
  5. ~PatBingsoo()
  6. ~Pat()
  7. ~Bingsoo()
  8. ~Ice()
  • 소멸자는 왠만하면 가상으로 선언해주는 것이 좋다

내용을 제대로 이해하셨다면, 코드를 보고 호출되는 생성자/소멸자 순서를 바로 파악하실 수 있을거에요

참조) 두들낙서 C/C++ 강좌