Kapan destruktor C ++ dipanggil?

118

Pertanyaan Dasar: kapan sebuah program memanggil metode destruktor kelas di C ++? Saya telah diberitahu bahwa itu dipanggil setiap kali sebuah benda keluar dari ruang lingkup atau dikenakandelete

Pertanyaan yang lebih spesifik:

1) Jika objek dibuat melalui pointer dan pointer itu kemudian dihapus atau diberi alamat baru untuk ditunjuk, apakah objek yang ditunjuk untuk memanggil destruktornya (dengan asumsi tidak ada lagi yang menunjuk ke sana)?

2) Menindaklanjuti pertanyaan 1, apa yang mendefinisikan ketika sebuah objek keluar dari ruang lingkup (bukan mengenai kapan sebuah objek meninggalkan {blok} tertentu). Jadi, dengan kata lain, kapan destruktor dipanggil pada objek dalam daftar tertaut?

3) Apakah Anda pernah ingin memanggil destruktor secara manual?

Pat Murray
sumber
3
Bahkan pertanyaan spesifik Anda terlalu luas. "Pointer itu kemudian dihapus" dan "diberi alamat baru untuk ditunjuk" sangat berbeda. Telusuri lebih lanjut (beberapa di antaranya telah dijawab), lalu ajukan pertanyaan terpisah untuk bagian yang tidak dapat Anda temukan.
Matthew Flaschen

Jawaban:

74

1) Jika objek dibuat melalui pointer dan pointer itu kemudian dihapus atau diberi alamat baru untuk ditunjuk, apakah objek yang ditunjuk untuk memanggil destruktornya (dengan asumsi tidak ada lagi yang menunjuk ke sana)?

Itu tergantung pada jenis penunjuknya. Misalnya, penunjuk cerdas sering kali menghapus objeknya saat dihapus. Petunjuk biasa tidak. Hal yang sama juga berlaku saat pointer dibuat untuk menunjuk ke objek yang berbeda. Beberapa petunjuk cerdas akan menghancurkan objek lama, atau akan menghancurkannya jika tidak ada lagi referensi. Petunjuk biasa tidak memiliki kecerdasan seperti itu. Mereka hanya menyimpan alamat dan memungkinkan Anda untuk melakukan operasi pada objek yang mereka tunjuk dengan melakukannya secara khusus.

2) Menindaklanjuti pertanyaan 1, apa yang mendefinisikan ketika sebuah objek keluar dari ruang lingkup (bukan mengenai kapan sebuah objek meninggalkan {blok} tertentu). Jadi, dengan kata lain, kapan destruktor dipanggil pada objek dalam daftar tertaut?

Itu terserah penerapan daftar tertaut. Koleksi tipikal menghancurkan semua objek yang dikandungnya saat dihancurkan.

Jadi, daftar penunjuk yang ditautkan biasanya akan menghancurkan penunjuk tetapi bukan objek yang mereka tunjuk. (Yang mungkin benar. Mereka mungkin referensi oleh penunjuk lain.) Sebuah daftar tertaut yang dirancang khusus untuk berisi penunjuk, bagaimanapun, mungkin menghapus objek pada penghancurannya sendiri.

Daftar penunjuk cerdas yang ditautkan dapat secara otomatis menghapus objek saat penunjuk dihapus, atau melakukannya jika tidak ada lagi referensi. Terserah Anda untuk memilih bagian yang melakukan apa yang Anda inginkan.

3) Apakah Anda pernah ingin memanggil destruktor secara manual?

Tentu. Salah satu contohnya adalah jika Anda ingin mengganti objek dengan objek lain yang berjenis sama tetapi tidak ingin mengosongkan memori hanya untuk mengalokasikannya lagi. Anda dapat menghancurkan objek lama di tempatnya dan membangun yang baru di tempatnya. (Namun, secara umum ini adalah ide yang buruk.)

// 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");
}
David Schwartz
sumber
2
Saya pikir contoh terakhir Anda menyatakan fungsi? Ini adalah contoh dari "parse paling menjengkelkan". (Hal lain yang lebih sepele adalah saya kira yang Anda maksud new Foo()dengan huruf besar 'F'.)
Stuart Golodetz
1
Menurut saya Foo myfoo("foo")bukan Most Vexing Parse, tapi char * foo = "foo"; Foo myfoo(foo);sebenarnya.
Cosine
Ini mungkin pertanyaan yang bodoh, tetapi bukankah seharusnya delete myFoodipanggil sebelumnya Foo *myFoo = new Foo("foo");? Atau Anda akan menghapus objek yang baru dibuat, bukan?
Matheus Rocha
Tidak ada myFoosebelum Foo *myFoo = new Foo("foo");garis. Baris itu membuat variabel baru yang disebut myFoo, membayangi variabel yang sudah ada. Padahal dalam kasus ini, tidak ada yang ada karena hal di myFooatas termasuk dalam ruang lingkup if, yang sudah berakhir.
David Schwartz
1
@galactikuh Sebuah "penunjuk cerdas" adalah sesuatu yang bertindak seperti penunjuk ke suatu objek tetapi juga memiliki fitur yang membuatnya lebih mudah untuk mengelola masa pakai objek itu.
David Schwartz
20

Orang lain telah membahas masalah lain, jadi saya hanya akan melihat satu hal: apakah Anda pernah ingin menghapus objek secara manual.

Jawabannya iya. @DavidSchwartz memberi satu contoh, tapi itu cukup satu tidak biasa. Saya akan memberikan contoh yang ada di balik terpal dari apa yang banyak digunakan programmer C ++ sepanjang waktu: std::vector(dan std::deque, meskipun tidak terlalu banyak digunakan).

Seperti yang diketahui kebanyakan orang, std::vectorakan mengalokasikan blok memori yang lebih besar ketika / jika Anda menambahkan lebih banyak item daripada yang dapat ditampung oleh alokasi saat ini. Namun, ketika melakukan ini, ia memiliki blok memori yang mampu menampung lebih banyak objek daripada yang ada di vektor.

Untuk mengelolanya, apa yang vectordilakukan di bawah selimut adalah mengalokasikan memori mentah melalui Allocatorobjek (yang, kecuali Anda menentukan sebaliknya, berarti digunakannya ::operator new). Kemudian, saat Anda menggunakan (misalnya) push_backuntuk menambahkan item ke vector, secara internal vektor menggunakan a placement newuntuk membuat item di bagian ruang memorinya (yang sebelumnya) tidak terpakai.

Sekarang, apa yang terjadi jika / jika Anda erasemerupakan item dari vektor? Itu tidak bisa begitu saja delete- itu akan melepaskan seluruh blok memorinya; ia perlu menghancurkan satu objek dalam memori itu tanpa merusak yang lain, atau melepaskan salah satu blok memori yang dikendalikannya (misalnya, jika Anda erase5 item dari vektor, lalu segera push_back5 item lagi, dijamin bahwa vektor tidak akan dialokasikan kembali memori saat Anda melakukannya.

Untuk melakukan itu, vektor secara langsung menghancurkan objek dalam memori dengan secara eksplisit memanggil destruktor, bukan dengan menggunakan delete.

Jika, mungkin, ada orang lain yang menulis wadah menggunakan penyimpanan yang berdekatan secara kasar seperti yang vectordilakukan (atau beberapa varian dari itu, seperti yang std::dequesebenarnya), Anda hampir pasti ingin menggunakan teknik yang sama.

Sebagai contoh, mari pertimbangkan bagaimana Anda dapat menulis kode untuk buffer cincin melingkar.

#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

Berbeda dengan wadah standar, ini menggunakan operator newdan operator deletelangsung. Untuk penggunaan nyata, Anda mungkin ingin menggunakan kelas pengalokasi, tetapi untuk saat ini akan melakukan lebih banyak hal untuk mengalihkan perhatian daripada berkontribusi (bagaimanapun juga, IMO).

Jerry Coffin
sumber
9
  1. Saat Anda membuat objek dengan new, Anda bertanggung jawab untuk memanggil delete. Saat Anda membuat objek dengan make_shared, hasilnya shared_ptrbertanggung jawab untuk menghitung dan memanggil deletesaat jumlah penggunaan mencapai nol.
  2. Keluar dari ruang lingkup berarti meninggalkan satu blok. Ini adalah saat destruktor dipanggil, dengan asumsi bahwa objek tersebut tidak dialokasikan dengan new(yaitu, itu adalah objek tumpukan).
  3. Tentang satu-satunya saat Anda perlu memanggil destruktor secara eksplisit adalah saat Anda mengalokasikan objek dengan penempatannew .
dasblinkenlight
sumber
1
Ada penghitungan referensi (shared_ptr), meskipun jelas bukan untuk petunjuk biasa.
Pubby
1
@Pubby: Poin bagus, mari promosikan praktik yang baik. Jawaban diedit.
MSalters
6

1) Objek tidak dibuat 'melalui pointer'. Ada penunjuk yang ditugaskan ke objek apa pun yang Anda 'baru'. Dengan asumsi ini yang Anda maksud, jika Anda memanggil 'hapus' pada penunjuk, itu benar-benar akan menghapus (dan memanggil destruktor pada) objek dereferensi penunjuk. Jika Anda menetapkan pointer ke objek lain, akan ada kebocoran memori; tidak ada di C ++ yang akan mengumpulkan sampah untuk Anda.

2) Ini adalah dua pertanyaan terpisah. Sebuah variabel keluar dari ruang lingkup ketika stack frame yang dideklarasikannya dikeluarkan dari stack. Biasanya ini adalah saat Anda meninggalkan blok. Objek di tumpukan tidak pernah keluar dari ruang lingkup, meskipun mungkin penunjuk mereka di tumpukan. Tidak ada yang secara khusus menjamin bahwa destruktor dari suatu objek dalam daftar tertaut akan dipanggil.

3) Tidak juga. Mungkin ada Sihir Ajaib yang menyarankan sebaliknya, tetapi biasanya Anda ingin mencocokkan kata kunci 'baru' dengan kata kunci 'hapus', dan memasukkan semua yang diperlukan ke dalam destruktor untuk memastikan kata kunci 'baru' itu bersih sendiri. Jika Anda tidak melakukan ini, pastikan untuk mengomentari destruktor dengan instruksi khusus kepada siapa pun yang menggunakan kelas tentang bagaimana mereka harus membersihkan sumber daya objek itu secara manual.

Nathaniel Ford
sumber
3

Untuk memberikan jawaban rinci atas pertanyaan 3: ya, ada (jarang) saat Anda mungkin memanggil destruktor secara eksplisit, khususnya sebagai mitra untuk penempatan baru, seperti yang diamati dasblinkenlight.

Untuk memberikan contoh konkret tentang ini:

#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;
}

Tujuan dari hal semacam ini adalah untuk memisahkan alokasi memori dari konstruksi objek.

Stuart Golodetz
sumber
2
  1. Pointer - Pointer biasa tidak mendukung RAII. Tanpa eksplisit delete, akan ada sampah. Untungnya C ++ memiliki petunjuk otomatis yang menangani ini untuk Anda!

  2. Cakupan - Pikirkan saat variabel menjadi tidak terlihat oleh program Anda. Biasanya ini di akhir {block}, seperti yang Anda tunjukkan.

  3. Penghancuran manual - Jangan pernah mencoba ini. Biarkan scope dan RAII melakukan keajaiban untuk Anda.

chrisaycock
sumber
Catatan: auto_ptr tidak berlaku lagi, seperti yang disebutkan di tautan Anda.
tnecniv
std::auto_ptrtidak digunakan lagi di C ++ 11, ya. Jika OP benar-benar memiliki C ++ 11, ia harus menggunakan std::unique_ptruntuk pemilik tunggal, atau std::shared_ptruntuk beberapa pemilik yang dihitung referensi.
chrisaycock
'Penghancuran manual - Jangan pernah mencoba ini'. Saya sangat sering mengantri penunjuk objek ke utas yang berbeda menggunakan panggilan sistem yang tidak dipahami oleh kompilator. 'Mengandalkan' scope / auto / smart pointers akan menyebabkan aplikasi saya gagal total karena objek dihapus oleh thread pemanggil sebelum dapat ditangani oleh thread konsumen. Masalah ini memengaruhi objek dan antarmuka yang dibatasi cakupan dan dihitung ulang. Hanya pointer dan penghapusan eksplisit yang akan dilakukan.
Martin James
@MartinJames Dapatkah Anda memposting contoh panggilan sistem yang tidak dipahami oleh kompilator? Dan bagaimana Anda menerapkan antrian? Tidak, std::queue<std::shared_ptr>?saya telah menemukan bahwa pipe()antara utas produsen dan konsumen membuat konkurensi jauh lebih mudah, jika penyalinan tidak terlalu mahal.
chrisaycock
myObject = new myClass (); PostMessage (aHandle, WM_APP, 0, LPPARAM (myObject));
Martin James
1

Setiap kali Anda menggunakan "baru", yaitu, melampirkan alamat ke penunjuk, atau mengatakan, Anda mengklaim ruang di heap, Anda perlu "menghapusnya".
1. ya, saat Anda menghapus sesuatu, destruktor dipanggil.
2. Saat destruktor dari daftar tertaut dipanggil, destruktor objeknya dipanggil. Tetapi jika itu adalah pointer, Anda perlu menghapusnya secara manual. 3. saat ruang diklaim "baru".

angsa berawan
sumber
0

Ya, destruktor (alias dtor) dipanggil saat sebuah objek keluar dari ruang lingkup jika berada di tumpukan atau saat Anda memanggil deletepenunjuk ke sebuah objek.

  1. Jika pointer dihapus melalui deletemaka dtor akan dipanggil. Jika Anda menetapkan ulang pointer tanpa memanggil deleteterlebih dahulu, Anda akan mendapatkan kebocoran memori karena objek tersebut masih ada di memori di suatu tempat. Dalam contoh terakhir, dtor tidak dipanggil.

  2. Implementasi daftar tertaut yang baik akan memanggil dtor dari semua objek dalam daftar ketika daftar sedang dihancurkan (karena Anda memanggil beberapa metode untuk menghancurkannya atau keluar dari ruang lingkup itu sendiri). Ini bergantung pada implementasi.

  3. Saya meragukannya, tetapi saya tidak akan terkejut jika ada beberapa keadaan aneh di luar sana.

tnecniv
sumber
1
"Jika Anda menetapkan ulang pointer tanpa memanggil delete terlebih dahulu, Anda akan mendapatkan kebocoran memori karena objek tersebut masih ada di memori di suatu tempat." Belum tentu. Itu bisa saja dihapus melalui penunjuk lain.
Matthew Flaschen
0

Jika objek dibuat tidak melalui pointer (misalnya, A a1 = A ();), destruktor dipanggil saat objek dihancurkan, selalu saat fungsi tempat objek itu berada selesai. Misalnya:

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


destruktor dipanggil ketika kode dieksekusi ke baris "selesai".

Jika objek dibuat melalui pointer (misalnya, A * a2 = new A ();), destruktor dipanggil ketika pointer dihapus (hapus a2;). Jika titik tidak dihapus oleh pengguna secara eksplisit atau diberi alamat baru sebelum menghapusnya, terjadi kebocoran memori. Itu adalah bug.

Dalam daftar tertaut, jika kita menggunakan std :: list <>, kita tidak perlu peduli dengan desctructor atau kebocoran memori karena std :: list <> telah menyelesaikan semua ini untuk kita. Dalam daftar tertaut yang kita tulis sendiri, kita harus menulis desctructor dan menghapus penunjuk secara eksplisit, jika tidak, akan menyebabkan kebocoran memori.

Kami jarang memanggil destruktor secara manual. Ini adalah fungsi yang menyediakan sistem.

Maaf untuk bahasa Inggris saya yang buruk!

wyx
sumber
Tidak benar bahwa Anda tidak dapat memanggil destruktor secara manual - Anda dapat (lihat kode di jawaban saya, misalnya). Yang benar adalah bahwa sebagian besar waktu Anda tidak boleh :)
Stuart Golodetz
0

Ingatlah bahwa Pembuat suatu objek dipanggil segera setelah memori dialokasikan untuk objek itu dan sedangkan destruktor dipanggil tepat sebelum mengalihkan memori objek itu.

Cerah Khandare
sumber