파생 클래스가 원시 동적 메모리를 할당하지 않는 경우 기본 클래스에 가상 소멸자가 필요한 이유는 무엇입니까?


12

다음 코드는 메모리 누수를 유발합니다.

#include <iostream>
#include <memory>
#include <vector>

using namespace std;

class base
{
    void virtual initialize_vector() = 0;
};

class derived : public base
{
private:
    vector<int> vec;

public:
    derived()
    {
        initialize_vector();
    }

    void initialize_vector()
    {
        for (int i = 0; i < 1000000; i++)
        {
            vec.push_back(i);
        }
    }
};

int main()
{
    for (int i = 0; i < 100000; i++)
    {
        unique_ptr<base> pt = make_unique<derived>();
    }
}

클래스 파생은 원시 동적 메모리를 할당하지 않고 unique_ptr이 자체 할당을 해제하므로 나에게별로 의미가 없습니다. 파생 클래스 대신 클래스베이스의 암시 적 소멸자가 호출되지만 여기서 이것이 왜 문제인지 알 수 없습니다. 파생을 위해 명시 적 소멸자를 작성한다면 vec에 대해서는 아무것도 쓰지 않을 것입니다.


4
소멸자가 수동으로 작성된 경우에만 존재한다고 가정합니다. 이 가정은 틀렸다 : 언어는 ~derived()vec의 소멸자를 위임 하는 언어를 제공한다 . 또는 unique_ptr<base> pt파생 된 소멸자를 알고 있다고 가정합니다 . 가상 방법이 없으면 이럴 수 없습니다. unique_ptr에 런타임 표현이없는 템플리트 매개 변수 인 삭제 함수가 제공 될 수 있지만 해당 기능은이 코드에 사용되지 않습니다.
amon

코드를 더 짧게 만들기 위해 같은 줄에 중괄호를 넣을 수 있습니까? 이제 스크롤해야합니다.
laike9m

답변:


14

컴파일러가 의 소멸자 delete _ptr;내부 에서 암시 적 실행을 수행하면 unique_ptr( _ptr포인터가 어디에 저장되어 있는지 unique_ptr) 정확히 두 가지를 알게됩니다.

  1. 삭제할 객체의 주소입니다.
  2. 포인터의 유형입니다 _ptr. 포인터에 있기 때문에 unique_ptr<base>, 그 수단은 _ptr유형이다 base*.

이것은 컴파일러가 아는 전부입니다. 따라서 유형의 객체를 삭제하면 base을 호출 ~base()합니다.

그래서 ... 실제로 가리키는 dervied대상을 파괴하는 부분은 어디에 있습니까? 컴파일러가를 파괴하고 있다는 것을 모른다 , 반드시 파괴되어야한다는 것은 물론 존재하는지도 모릅니다 . 따라서 객체의 절반을 파괴하지 않고 객체를 깨뜨 렸습니다.derivedderived::vec

컴파일러는 할 수없는 가정 모든 것을 base*파괴는 사실입니다 derived*; 결국,에서 파생 된 클래스는 얼마든지있을 수 있습니다 base. 이 유형이 base*실제로 어떤 유형을 가리키는 지 어떻게 알 수 있습니까?

어떤 컴파일러가 수행하는 것은 호출에 대한 올바른 소멸자 알아낼 수있다 (예, derived소멸자가 있습니다. 당신이하지 않는 = delete소멸자, 모든 클래스가 하나 여부를 쓰기 여부, 소멸자있다). 이를 위해서는 base실제 클래스의 생성자가 설정 한 소멸자 코드의 올바른 주소를 얻기 위해 저장된 일부 정보를 사용해야 합니다. 그런 다음이 정보를 사용 base*하여 해당 derived클래스 의 주소 (다른 주소에 있거나 없을 수 있음)에 대한 포인터를 포인터 로 변환해야 합니다 . 그런 다음 소멸자를 호출 할 수 있습니다.

내가 방금 설명한 메커니즘? 일반적으로 "가상 디스패치"라고합니다. 즉, virtual기본 클래스에 대한 포인터 / 참조가있을 때 표시된 함수를 호출 할 때 발생합니다 .

모든 것이 기본 클래스 포인터 / 참조 일 때 파생 클래스 함수를 호출하려면 해당 함수를 선언해야합니다 virtual. 소멸자는 이와 관련하여 근본적으로 다르지 않습니다.


0

계승

상속의 전체 지점은 파생 클래스의 인스턴스가 다른 파생 유형의 다른 인스턴스와 동일하게 처리 될 수 있도록 여러 가지 구현간에 공통 인터페이스와 프로토콜을 공유하는 것입니다.

C ++ 상속에서는 구현 세부 정보를 제공하므로 소멸자를 가상으로 표시 (또는 표시하지 않음)는 구현 세부 정보 중 하나입니다.

함수 바인딩

이제 함수 또는 생성자 또는 소멸자와 같은 특수한 경우를 호출 할 때 컴파일러는 어떤 함수 구현을 의미하는지 선택해야합니다. 그런 다음이 의도를 따르는 기계 코드를 생성해야합니다.

이 작업을 수행하는 가장 간단한 방법은 컴파일 타임에 함수를 선택하고 충분한 코드를 생성하여 값에 관계없이 해당 코드가 실행될 때 항상 함수에 대한 코드를 실행하도록하는 것입니다. 상속을 제외하고는 잘 작동합니다.

함수가있는 기본 클래스가 있고 (생성자 또는 소멸자를 포함한 모든 함수 일 수 있음) 코드에서 함수를 호출하면 이것이 무엇을 의미합니까?

예를 들어 initialize_vector()컴파일러를 호출 한 경우에서 찾은 Base구현 또는에서 구현 을 실제로 호출 할 것인지 결정해야합니다 Derived. 이를 결정하는 두 가지 방법이 있습니다.

  1. 첫 번째는 Base유형 에서 호출했기 때문에 의 구현을 의미한다고 결정하는 것입니다 Base.
  2. 두 번째는에 저장된 값의 실행시의 형태 때문 결정하는 것입니다 Base입력 된 값이 될 수있다 Base, 또는 Derived(가 호출 될 때마다) 호출 될 때 그 결정으로 만들 수있는 호출에, 실행시에해야합니다.

이 시점의 컴파일러는 혼란스럽고 두 옵션 모두 동일하게 유효합니다. 이것은 virtual믹스에 올 때 입니다. 이 키워드가 있으면 컴파일러는 옵션 2를 선택하여 코드가 실제 값으로 실행될 때까지 가능한 모든 구현 간의 결정을 지연시킵니다. 이 키워드가 없으면 컴파일러는 옵션 1을 선택합니다. 그렇지 않으면 정상적인 동작이기 때문입니다.

가상 함수 호출의 경우 컴파일러는 여전히 옵션 1을 선택할 수 있습니다. 그러나 이것이 항상 사실임을 증명할 수있는 경우에만.

생성자와 소멸자

그렇다면 가상 생성자를 지정하지 않는 이유는 무엇입니까?

더 직관적으로 어떻게 컴파일러는 생성자의 동일한 구현에 선택할 것 DerivedDerived2? 이것은 매우 간단합니다. 컴파일러가 실제로 의도 한 것을 배울 수있는 기존의 가치는 없습니다. 생성자의 역할이기 때문에 기존 값은 없습니다.

그렇다면 왜 가상 소멸자를 지정해야합니까?

좀 더 직관적으로 컴파일러는 구현 방식 Base과 구현 방식 중 어느 것을 선택 Derived합니까? 그것들은 단지 함수 호출이므로 함수 호출 동작이 발생합니다. 선언 된 가상 소멸자가 없으면 컴파일러는 Base런타임 값 값에 관계없이 소멸자에 직접 바인딩하기로 결정 합니다.

많은 컴파일러에서 파생 된 데이터 멤버를 선언하지 않거나 다른 형식을 상속하지 않으면의 동작 ~Base()이 적합하지만 보장되지는 않습니다. 아직 발화되지 않은 화염 방사기 앞에 서있는 것처럼 순전히 상황에 따라 작동합니다. 당신은 한동안 괜찮습니다.

C ++에서 기본 또는 인터페이스 유형을 선언하는 유일한 올바른 방법은 가상 소멸자를 선언하여 해당 유형의 유형 계층 구조의 지정된 인스턴스에 대해 올바른 소멸자를 호출하는 것입니다. 이를 통해 인스턴스에 대해 가장 잘 알고있는 함수가 해당 인스턴스를 올바르게 정리할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.