소멸자 (C++)

소멸자가 개체가 범위를 벗어나거나 호출에 delete[]의해 명시적으로 제거될 때 자동으로 호출 delete 되는 멤버 함수입니다. 소멸자의 이름은 클래스와 같으며 그 앞에는 타일(~)이 있습니다. 예를 들어 클래스 String의 소멸자는 ~String()으로 선언됩니다.

소멸자를 정의하지 않으면 컴파일러가 기본값을 제공하며 일부 클래스의 경우 이것으로 충분합니다. 클래스가 시스템 리소스에 대한 핸들 또는 클래스 인스턴스가 제거될 때 해제해야 하는 메모리에 대한 포인터와 같이 명시적으로 해제해야 하는 리소스를 기본 때 사용자 지정 소멸자를 정의해야 합니다.

다음 String 클래스 선언을 참조하십시오.

// spec1_destructors.cpp
#include <string> // strlen()

class String
{
    public:
        String(const char* ch);  // Declare the constructor
        ~String();               // Declare the destructor
    private:
        char* _text{nullptr};
};

// Define the constructor
String::String(const char* ch)
{
    size_t sizeOfText = strlen(ch) + 1; // +1 to account for trailing NULL

    // Dynamically allocate the correct amount of memory.
    _text = new char[sizeOfText];

    // If the allocation succeeds, copy the initialization string.
    if (_text)
    {
        strcpy_s(_text, sizeOfText, ch);
    }
}

// Define the destructor.
String::~String()
{
    // Deallocate the memory that was previously reserved for the string.
    delete[] _text;
}

int main()
{
    String str("We love C++");
}

앞의 예제에서 소멸자는 String::~String 연산자를 delete[] 사용하여 텍스트 스토리지에 동적으로 할당된 공간을 할당 취소합니다.

소멸자 선언

소멸자는 클래스와 이름이 같지만 물결표(~)가 앞에 붙어 있는 함수입니다.

몇 가지 규칙이 소멸자의 선언에 적용됩니다. 소멸자는 다음과 같습니다.

  • 인수를 수락하지 마세요.
  • 값(또는 void)을 반환하지 마세요.
  • 또는 volatilestatic로 선언const할 수 없습니다. 그러나 , 또는 static.로 constvolatile선언된 개체의 소멸을 위해 호출할 수 있습니다.
  • 로 선언 virtual할 수 있습니다. 가상 소멸자를 사용하면 해당 형식을 모르고 개체를 삭제할 수 있습니다. 가상 함수 메커니즘을 사용하여 개체에 대한 올바른 소멸자가 호출됩니다. 소멸자는 추상 클래스에 대한 순수 가상 함수로 선언할 수도 있습니다.

소멸자 사용

다음 이벤트 중 하나가 발생하면 소멸자가 호출됩니다.

  • 블록 범위가 있는 로컬(자동) 개체는 범위를 벗어납니다.
  • 를 사용하여 할당된 개체의 할당을 취소하는 데 사용합니다 deletenew. 정의되지 않은 동작의 결과 사용 delete[] .
  • 를 사용하여 할당된 개체의 할당을 취소하는 데 사용합니다 delete[]new[]. 정의되지 않은 동작의 결과 사용 delete .
  • 임시 개체의 수명이 종료됩니다.
  • 프로그램이 종료되고 전역 또는 정적 개체가 존재합니다.
  • 소멸자는 소멸자 함수의 정규화된 이름을 사용하여 명시적으로 호출됩니다.

소멸자는 클래스 멤버 함수를 자유롭게 호출하고 클래스 멤버 데이터에 액세스할 수 있습니다.

소멸자 사용에는 두 가지 제한 사항이 있습니다.

  • 주소를 사용할 수 없습니다.

  • 파생 클래스는 기본 클래스의 소멸자를 상속하지 않습니다.

소멸 순서

개체가 범위에서 벗어나거나 삭제될 때 완전한 소멸에서 발생하는 이벤트 시퀀스는 다음과 같습니다.

  1. 클래스의 소멸자가 호출되고 소멸자 함수의 본문이 실행됩니다.

  2. 비정적 멤버 개체의 소멸자가 클래스 선언에 나타나는 순서와 반대로 호출됩니다. 이러한 멤버의 생성에 사용되는 선택적 멤버 초기화 목록은 생성 또는 소멸 순서에 영향을 주지 않습니다.

  3. 가상이 아닌 기본 클래스에 대한 소멸자는 선언의 역순으로 호출됩니다.

  4. 가상 기본 클래스의 소멸자가 선언과 반대 순서로 호출됩니다.

// order_of_destruction.cpp
#include <cstdio>

struct A1      { virtual ~A1() { printf("A1 dtor\n"); } };
struct A2 : A1 { virtual ~A2() { printf("A2 dtor\n"); } };
struct A3 : A2 { virtual ~A3() { printf("A3 dtor\n"); } };

struct B1      { ~B1() { printf("B1 dtor\n"); } };
struct B2 : B1 { ~B2() { printf("B2 dtor\n"); } };
struct B3 : B2 { ~B3() { printf("B3 dtor\n"); } };

int main() {
   A1 * a = new A3;
   delete a;
   printf("\n");

   B1 * b = new B3;
   delete b;
   printf("\n");

   B3 * b2 = new B3;
   delete b2;
}
A3 dtor
A2 dtor
A1 dtor

B1 dtor

B3 dtor
B2 dtor
B1 dtor

가상 기본 클래스

가상 기본 클래스의 소멸자는 방향이 있는 비순환 그래프(깊이 우선, 왼쪽에서 오른쪽으로, 후위 운행법)에서 표시되는 역순으로 호출됩니다. 다음 그림은 상속 그래프를 보여 줍니다.

Inheritance graph that shows virtual base classes.

A~E라는 5개의 클래스가 상속 그래프에 정렬됩니다. 클래스 E는 B, C 및 D의 기본 클래스입니다. C 및 D 클래스는 A 및 B의 기본 클래스입니다.

다음은 그림에 표시된 클래스에 대한 클래스 정의를 나열합니다.

class A {};
class B {};
class C : virtual public A, virtual public B {};
class D : virtual public A, virtual public B {};
class E : public C, public D, virtual public B {};

E 형식 개체의 가상 기본 클래스를 파괴하는 순서를 결정하기 위해 컴파일러는 다음 알고리즘을 적용하여 목록을 빌드합니다.

  1. 그래프에서 가장 깊은 지점에서 시작하여 왼쪽으로 그래프를 이동합니다(이 경우 E).
  2. 모든 노드를 방문할 때까지 왼쪽으로 이동을 수행합니다. 현재 노드의 이름입니다.
  3. 이전 노드(아래 및 오른쪽으로)를 다시 방문하여 기억된 노드가 가상 기본 클래스인지 확인합니다.
  4. 기억된 노드가 가상 기본 클래스인 경우 목록을 검색하여 이미 입력되었는지를 확인합니다. 가상 기본 클래스가 아닌 경우 무시합니다.
  5. 기억된 노드가 목록에 아직 없으면 목록 맨 아래에 추가합니다.
  6. 그래프를 위로 이동하고 다음 경로를 따라 오른쪽으로 이동합니다.
  7. 2단계로 이동합니다.
  8. 마지막 상향 경로가 모두 사용되면 현재 노드의 이름을 참고합니다.
  9. 3단계로 이동합니다.
  10. 아래쪽 노드가 다시 현재 노드가 될 때까지 이 프로세스를 계속합니다.

따라서 E 클래스의 경우 소멸의 순서는 다음과 같습니다.

  1. 가상이 아닌 기본 클래스입니다 E.
  2. 가상이 아닌 기본 클래스입니다 D.
  3. 가상이 아닌 기본 클래스입니다 C.
  4. 가상 기본 클래스 B.
  5. 가상 기본 클래스 A.

이 프로세스는 고유 항목의 순서 지정된 목록을 만듭니다. 클래스 이름은 두 번 표시되지 않습니다. 목록이 생성되면 역순으로 안내되고 마지막부터 첫 번째 클래스까지 목록의 각 클래스에 대한 소멸자가 호출됩니다.

생성 또는 소멸 순서는 한 클래스의 생성자 또는 소멸자가 먼저 생성되는 다른 구성 요소에 의존하거나 더 오래 지속되는 경우 주로 중요합니다. 예를 들어 코드 B 가 실행될 때 소멸자가 A 계속 존재하거나 그 반대의 경우도 마찬가지입니다.

나중에 파생된 클래스가 생성과 소멸의 순서를 변경할 수 있는 가장 왼쪽 경로를 변경할 수 있기 때문에 상속 그래프에서 클래스 사이의 이러한 상호 의존성은 본질적으로 위험합니다.

가상이 아닌 기본 클래스

가상이 아닌 기본 클래스에 대한 소멸자는 기본 클래스 이름이 선언되는 역순으로 호출됩니다. 다음과 같은 클래스 선언을 생각해 보세요.

class MultInherit : public Base1, public Base2
...

위의 예제에서 Base2의 소멸자가 Base1의 소멸자보다 먼저 호출됩니다.

명시적 소멸자 호출

대부분의 경우 소멸자를 명시적으로 호출할 필요가 없지만 절대 주소에 있는 개체를 정리할 때는 도움이 됩니다. 이러한 개체는 일반적으로 배치 인수를 사용하는 사용자 정의 new 연산자를 사용하여 할당됩니다. 이 연산자는 delete 무료 저장소에서 할당되지 않으므로 이 메모리의 할당을 취소할 수 없습니다(자세한 내용은 새 연산자 및 삭제 연산자 참조). 그러나 소멸자를 호출하면 적절한 정리를 수행할 수 있습니다. s 클래스의 String 개체에 대해 소멸자를 명시적으로 호출하려면 다음 문 중 하나를 사용하세요.

s.String::~String();     // non-virtual call
ps->String::~String();   // non-virtual call

s.~String();       // Virtual call
ps->~String();     // Virtual call

앞에 나온 대로 소멸자를 정의하는 형식에 관계없이 소멸자를 명시적으로 호출하기 위한 표기법을 사용할 수 있습니다. 그러면 형식에 대해 소멸자가 정의되었는지 여부를 몰라도 명시적인 호출이 가능합니다. 소멸자가 정의되지 않은 경우 명시적 호출은 아무 효과가 없습니다.

강력한 프로그래밍

클래스는 리소스를 획득하는 경우 소멸자가 필요하며 리소스를 안전하게 관리하려면 복사 생성자 및 복사 할당을 구현해야 할 수 있습니다.

이러한 특수 함수가 사용자가 정의하지 않은 경우 컴파일러에서 암시적으로 정의됩니다. 암시적으로 생성된 생성자 및 할당 연산자는 단순 멤버 복사를 수행하며 개체가 리소스를 관리하는 경우 거의 틀립니다.

다음 예제에서 암시적으로 생성된 복사 생성자는 포인터 str1.textstr2.text 를 만들고 동일한 메모리를 참조하며, 반환 copy_strings()할 때 해당 메모리는 정의되지 않은 동작인 두 번 삭제됩니다.

void copy_strings()
{
   String str1("I have a sense of impending disaster...");
   String str2 = str1; // str1.text and str2.text now refer to the same object
} // delete[] _text; deallocates the same memory twice
  // undefined behavior

소멸자, 복사 생성자 또는 복사 할당 연산자를 명시적으로 정의하면 이동 생성자 및 이동 할당 연산자의 암시적 정의를 방지할 수 있습니다. 이 경우 일반적으로 이동 작업을 제공하지 못하는 경우 복사 비용이 많이 드는 경우 최적화 기회를 놓칠 수 있습니다.

참고 항목

복사 생성자 및 복사 할당 연산자
이동 생성자 및 이동 할당 연산자