C ++ 소멸자는 언제 호출됩니까?


118

기본 질문 : 프로그램이 C ++에서 클래스의 소멸자 메서드를 언제 호출합니까? 개체가 범위를 벗어나거나 대상이 될 때마다 호출된다고 들었습니다.delete

더 구체적인 질문 :

1) 객체가 포인터를 통해 생성되고 그 포인터가 나중에 삭제되거나 가리키는 새 주소가 주어진 경우, 가리키는 객체가 소멸자를 호출합니까 (다른 어떤 것도 가리키는 것이 없다고 가정)?

2) 질문 1에 대한 후속 조치로, 객체가 범위를 벗어날 때 정의하는 것은 무엇입니까 (객체가 주어진 {block}을 떠날 때와 관련이 없음). 즉, 연결 목록의 개체에서 소멸자가 언제 호출됩니까?

3) 소멸자를 수동으로 호출하고 싶습니까?


3
특정 질문조차도 너무 광범위합니다. "그 포인터는 나중에 삭제됨"과 "지정할 새 주소가 지정됨"은 상당히 다릅니다. 더 많이 검색 한 다음 (일부는 답이 있음) 찾을 수없는 부분에 대해 별도의 질문을하십시오.
Matthew Flaschen 2012

답변:


74

1) 객체가 포인터를 통해 생성되고 그 포인터가 나중에 삭제되거나 가리키는 새 주소가 주어진 경우, 가리키는 객체가 소멸자를 호출합니까 (다른 어떤 것도 가리키는 것이 없다고 가정)?

포인터 유형에 따라 다릅니다. 예를 들어 스마트 포인터는 개체가 삭제 될 때 개체를 삭제하는 경우가 많습니다. 일반 포인터는 그렇지 않습니다. 포인터가 다른 개체를 가리 키도록 만들 때도 마찬가지입니다. 일부 스마트 포인터는 이전 개체를 파괴하거나 더 이상 참조가 없으면 파괴합니다. 일반 포인터에는 그러한 영리함이 없습니다. 그들은 단지 주소를 보유하고 특별히 그렇게함으로써 그들이 가리키는 객체에 대한 작업을 수행 할 수 있도록합니다.

2) 질문 1에 대한 후속 조치로, 객체가 범위를 벗어날 때 정의하는 것은 무엇입니까 (객체가 주어진 {block}을 떠날 때와 관련이 없음). 즉, 연결 목록의 개체에서 소멸자가 언제 호출됩니까?

그것은 연결 목록의 구현에 달려 있습니다. 일반적인 컬렉션은 소멸시 포함 된 모든 개체를 소멸시킵니다.

따라서 연결된 포인터 목록은 일반적으로 포인터가 가리키는 개체가 아닌 포인터를 파괴합니다. (정확할 수 있습니다. 다른 포인터에 의한 참조 일 수 있습니다.) 그러나 포인터를 포함하도록 특별히 설계된 연결 목록은 자체적으로 개체를 삭제할 수 있습니다.

스마트 포인터의 연결된 목록은 포인터가 삭제 될 때 개체를 자동으로 삭제하거나 더 이상 참조가없는 경우 삭제할 수 있습니다. 원하는 것을 수행하는 조각을 선택하는 것은 모두 당신에게 달려 있습니다.

3) 소멸자를 수동으로 호출하고 싶습니까?

확실한. 한 가지 예는 객체를 동일한 유형의 다른 객체로 바꾸고 싶지만 다시 할당하기 위해 메모리를 해제하지 않으려는 경우입니다. 기존 개체를 제자리에서 파괴하고 새로운 개체를 구성 할 수 있습니다. (그러나 일반적으로 이것은 나쁜 생각입니다.)

// pointer is destroyed because it goes out of scope,
// but not the object it pointed to. memory leak
if (1) {
 Foo *myfoo = new Foo("foo");
}


// pointer is destroyed because it goes out of scope,
// object it points to is deleted. no memory leak
if(1) {
 Foo *myfoo = new Foo("foo");
 delete myfoo;
}

// no memory leak, object goes out of scope
if(1) {
 Foo myfoo("foo");
}

2
마지막 예제에서 함수를 선언했다고 생각합니까? "가장 짜증나는 구문 분석"의 예입니다. (다른 많은 사소한 점은 당신이 의미 추측이다 new Foo()대문자 'F'로.)
스튜어트 Golodetz

1
나는 Foo myfoo("foo")Most Vexing Parse가 아니라고 생각 char * foo = "foo"; Foo myfoo(foo);합니다.
Cosine

어리석은 질문 일 수 있지만 delete myFoo전에 호출 해서는 안 Foo *myFoo = new Foo("foo");됩니까? 아니면 새로 생성 된 개체를 삭제해야합니다.
Matheus Rocha 2013

myFoo앞에는 없습니다 Foo *myFoo = new Foo("foo");. 이 선은라는 새로운 변수를 만들어 myFoo기존 변수 를 숨 깁니다. 이 경우에는 myFoo위 의 내용이 종료 된의 범위에 있으므로 기존 항목이 없습니다 if.
David Schwartz

1
@galactikuh "스마트 포인터"는 개체에 대한 포인터처럼 작동하지만 해당 개체의 수명을보다 쉽게 ​​관리 할 수있는 기능도 포함하고 있습니다.
David Schwartz

20

다른 사람들은 이미 다른 문제를 해결 했으므로 한 가지 사항 만 살펴 보겠습니다. 개체를 수동으로 삭제 하시겠습니까?

대답은 '예'입니다. @DavidSchwartz가 한 가지 예를 제공했지만 상당히 드문 경우입니다. 많은 C ++ 프로그래머가 항상 사용하는 후드 아래에있는 예제를 제공 할 것입니다. std::vector(그리고 std::deque많이 사용되지는 않지만).

대부분의 사람들이 알다시피, std::vector은 (는) 현재 할당이 보유 할 수있는 것보다 더 많은 항목을 추가 할 때 더 큰 메모리 블록을 할당합니다. 그러나 이렇게하면 현재 벡터에있는 것보다 더 많은 객체 를 보유 할 수있는 메모리 블록 이 있습니다.

이를 관리하기 위해 내부적으로하는 vector일은 객체 를 통해 원시 메모리를 할당 하는 Allocator것입니다 (달리 지정하지 않는 한를 사용함을 의미 함 ::operator new). 그런 다음 (예를 들어) push_back를 사용 하여에 항목을 추가하면 vector내부적으로 벡터는 a placement new를 사용하여 메모리 공간의 (이전) 사용되지 않은 부분에 항목을 만듭니다.

이제 erase벡터의 항목이 있으면 어떻게됩니까 ? 그냥 사용할 수는 없습니다 delete. 전체 메모리 블록을 해제합니다. 다른 개체를 파괴하지 않거나 제어하는 ​​메모리 블록을 해제하지 않고 해당 메모리에있는 한 개체를 파괴해야합니다 (예 erase: 벡터에서 push_back5 개 항목 을 즉시 5 개 더 추가 하면 벡터가 재 할당 되지 않음을 보장 합니다. 당신이 그렇게 할 때 기억.

이를 위해 벡터는를 사용 하지 않고 소멸자를 명시 적으로 호출하여 메모리의 객체를 직접 파괴합니다 delete.

만약 다른 사람이 대략 같은 방식 vector(또는 std::deque실제 방식 과 같이 그 변형)을 사용하여 연속 저장소를 사용하여 컨테이너를 작성 한다면 거의 확실하게 동일한 기술을 사용하고 싶을 것입니다.

예를 들어 원형 링 버퍼에 대한 코드를 작성하는 방법을 고려해 보겠습니다.

#ifndef CBUFFER_H_INC
#define CBUFFER_H_INC

template <class T>
class circular_buffer {
    T *data;
    unsigned read_pos;
    unsigned write_pos;
    unsigned in_use;
    const unsigned capacity;
public:
    circular_buffer(unsigned size) :
        data((T *)operator new(size * sizeof(T))),
        read_pos(0),
        write_pos(0),
        in_use(0),
        capacity(size)
    {}

    void push(T const &t) {
        // ensure there's room in buffer:
        if (in_use == capacity) 
            pop();

        // construct copy of object in-place into buffer
        new(&data[write_pos++]) T(t);
        // keep pointer in bounds.
        write_pos %= capacity;
        ++in_use;
    }

    // return oldest object in queue:
    T front() {
        return data[read_pos];
    }

    // remove oldest object from queue:
    void pop() { 
        // destroy the object:
        data[read_pos++].~T();

        // keep pointer in bounds.
        read_pos %= capacity;
        --in_use;
    }
  
~circular_buffer() {
    // first destroy any content
    while (in_use != 0)
        pop();

    // then release the buffer.
    operator delete(data); 
}

};

#endif

표준 컨테이너와 달리 이는 operator newoperator delete직접 사용 합니다. 실제 사용을 위해서는 할당 자 클래스를 사용하고 싶겠지 만, 현재로서는 기여하는 것보다주의를 분산시키는 것이 더 많을 것입니다 (IMO, 어쨌든).


9
  1. 를 사용하여 객체를 만들 때 new를 호출해야 delete합니다. 를 사용하여 객체를 만들면 make_shared결과 shared_ptr는 카운트를 유지 delete하고 사용 횟수가 0이 될 때 호출 합니다.
  2. 범위를 벗어나는 것은 블록을 떠나는 것을 의미합니다. 이것은 객체가 할당 되지 않았다고 가정하여 소멸자가 호출 될 때 입니다 new(즉, 스택 객체).
  3. 소멸자를 명시 적으로 호출해야하는 유일한 경우는 배치new 와 함께 객체를 할당 할 때 입니다.

1
분명히 일반 포인터가 아니지만 참조 카운팅 (shared_ptr)이 있습니다.
Pubby

1
@Pubby : 좋은 지적, 좋은 연습을 장려합시다. 수정 된 답변.
MSalters

6

1) 개체는 '포인터를 통해'생성되지 않습니다. '새로 만들기'개체에 할당 된 포인터가 있습니다. 이것이 의미하는 바라고 가정하면 포인터에서 'delete'를 호출하면 포인터가 역 참조하는 객체를 실제로 삭제 (소멸자를 호출)합니다. 포인터를 다른 개체에 할당하면 메모리 누수가 발생합니다. C ++의 어떤 것도 쓰레기를 수집하지 않습니다.

2) 두 가지 질문이 있습니다. 변수는 선언 된 스택 프레임이 스택에서 튀어 나오면 범위를 벗어납니다. 보통 이것은 당신이 블록을 떠날 때입니다. 힙의 개체는 스택에 대한 포인터가있을 수 있지만 범위를 벗어나지 않습니다. 특히 연결된 목록에있는 개체의 소멸자가 호출된다는 것을 보장하는 것은 없습니다.

3) 그렇지 않습니다. 달리 제안 할 수있는 Deep Magic이있을 수 있지만 일반적으로 '새'키워드를 '삭제'키워드와 일치시키고 소멸자에 모든 것을 넣어 제대로 정리되도록합니다. 이렇게하지 않으면 해당 개체의 리소스를 수동으로 정리하는 방법에 대한 클래스를 사용하는 모든 사람에게 특정 지침과 함께 소멸자에 주석을 달아야합니다.


3

질문 3에 대한 자세한 답변을 제공하려면 예, 특히 dasblinkenlight가 관찰 한 것처럼 소멸자를 명시 적으로 호출 할 수있는 경우가 있습니다.

이에 대한 구체적인 예를 들면 다음과 같습니다.

#include <iostream>
#include <new>

struct Foo
{
    Foo(int i_) : i(i_) {}
    int i;
};

int main()
{
    // Allocate a chunk of memory large enough to hold 5 Foo objects.
    int n = 5;
    char *chunk = static_cast<char*>(::operator new(sizeof(Foo) * n));

    // Use placement new to construct Foo instances at the right places in the chunk.
    for(int i=0; i<n; ++i)
    {
        new (chunk + i*sizeof(Foo)) Foo(i);
    }

    // Output the contents of each Foo instance and use an explicit destructor call to destroy it.
    for(int i=0; i<n; ++i)
    {
        Foo *foo = reinterpret_cast<Foo*>(chunk + i*sizeof(Foo));
        std::cout << foo->i << '\n';
        foo->~Foo();
    }

    // Deallocate the original chunk of memory.
    ::operator delete(chunk);

    return 0;
}

이런 종류의 목적은 객체 생성에서 메모리 할당을 분리하는 것입니다.


2
  1. 포인터 -일반 포인터는 RAII를 지원하지 않습니다. 명시 적이 없으면 delete쓰레기가 있습니다. 다행히 C ++에는 이를 처리하는 자동 포인터 가 있습니다!

  2. 범위 -변수가 프로그램에서 보이지 않게 될 때를 생각해보십시오 . 일반적으로 이것은 {block}당신이 지적했듯이 의 끝에 있습니다.

  3. 수동 파괴 -절대로 시도하지 마십시오. 스코프와 RAII가 당신을 위해 마법을 맡기십시오.


참고 : auto_ptr은 링크에서 언급했듯이 더 이상 사용되지 않습니다.
tnecniv

std::auto_ptrC ++ 11에서는 더 이상 사용되지 않습니다. OP에 실제로 C ++ 11이있는 경우 std::unique_ptr단일 소유자 또는 std::shared_ptr참조 계산 된 여러 소유자에 대해 사용해야 합니다 .
chrisaycock 2012

'수동 파괴-절대 시도하지 마십시오'. 컴파일러가 이해하지 못하는 시스템 호출을 사용하여 다른 스레드에 대한 개체 포인터를 대기열에 넣는 경우가 많습니다. 범위 / 자동 / 스마트 포인터에 '의존'하면 소비자 스레드에서 처리 할 수 ​​있기 전에 호출 스레드에 의해 개체가 삭제 되었기 때문에 내 앱이 비극적으로 실패 할 수 있습니다. 이 문제는 범위 제한 및 refCounted 개체 및 인터페이스에 영향을줍니다. 포인터와 명시 적 삭제 만 가능합니다.
Martin James

@MartinJames 컴파일러가 이해하지 못하는 시스템 호출의 예를 게시 할 수 있습니까? 대기열을 어떻게 구현하고 있습니까? 아니 std::queue<std::shared_ptr>?내가 찾은 그 pipe()훨씬 쉽게, 복사가 너무 비용이 경우 생산자와 소비자 스레드 메이크업 동시성 사이.
chrisaycock 2012

myObject = new myClass (); PostMessage (aHandle, WM_APP, 0, LPPARAM (myObject));
Martin James

1

"new"를 사용할 때, 즉 포인터에 주소를 첨부하거나 힙에 공간을 요구할 때마다 "삭제"해야합니다.
1. 예, 무언가를 삭제하면 소멸자가 호출됩니다.
2. Linked List의 소멸자가 호출되면 해당 객체의 소멸자가 호출됩니다. 그러나 포인터 인 경우 수동으로 삭제해야합니다. 3. "신규"에 의해 공간이 요구되는 경우.


0

예, 소멸자 (일명 dtor)는 개체가 스택에있는 경우 범위를 벗어나거나 delete개체에 대한 포인터를 호출 할 때 호출 됩니다.

  1. 포인터 delete가을 통해 삭제 되면 dtor가 호출됩니다. delete먼저 호출하지 않고 포인터를 다시 할당하면 개체가 메모리 어딘가에 여전히 존재하기 때문에 메모리 누수가 발생합니다. 후자의 경우 dtor가 호출되지 않습니다.

  2. 좋은 연결 목록 구현은 목록이 소멸 될 때 목록에있는 모든 개체의 dtor를 호출합니다 (일부 메서드를 호출하여 삭제하거나 범위를 벗어 났기 때문). 이것은 구현에 따라 다릅니다.

  3. 의심 스럽지만 이상한 상황이 있어도 놀라지 않을 것입니다.


1
"삭제를 먼저 호출하지 않고 포인터를 다시 할당하면 개체가 메모리 어딘가에 여전히 존재하기 때문에 메모리 누수가 발생합니다." 반드시 그런 것은 아닙니다. 다른 포인터를 통해 삭제되었을 수 있습니다.
Matthew Flaschen 2012

0

객체가 포인터를 통하지 않고 생성 된 경우 (예 : A a1 = A ();), 객체가 소멸 될 때 소멸자가 호출되며, 항상 해당 객체가있는 함수가 완료 될 때 호출됩니다.

void func()
{
...
A a1 = A();
...
}//finish


소멸자는 코드가 "finish"줄에 실행될 때 호출됩니다.

포인터를 통해 객체가 생성 된 경우 (예 : A * a2 = new A ();), 포인터가 삭제 될 때 소멸자가 호출됩니다 (delete a2;). 사용자가 명시 적으로 포인트를 삭제하지 않거나 새 주소를 삭제하기 전에 메모리 누수가 발생합니다. 그것은 버그입니다.

연결 목록에서 std :: list <>를 사용하면 std :: list <>가이 모든 작업을 완료했기 때문에 desctructor 또는 메모리 누수에 대해 신경 쓸 필요가 없습니다. 우리 자신이 작성한 연결 목록에서 desctructor를 작성하고 포인터를 명시 적으로 삭제해야합니다. 그렇지 않으면 메모리 누수가 발생합니다.

소멸자를 수동으로 호출하는 경우는 거의 없습니다. 시스템에 제공하는 기능입니다.

불쌍한 영어에 대해 죄송합니다!


수동으로 소멸자를 호출 할 수 없다는 것은 사실이 아닙니다. 가능합니다 (예를 들어 내 답변의 코드 참조). 어떤 사실이다 것은 당신이 :) 안 대부분의 시간
스튜어트 Golodetz

0

객체의 생성자는 해당 객체에 메모리가 할당 된 직후에 호출되고 소멸자는 해당 객체의 메모리 할당을 해제하기 직전에 호출됩니다.

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