Mengapa penggunaan 'baru' menyebabkan kebocoran memori?


131

Saya belajar C # pertama, dan sekarang saya mulai dengan C ++. Seperti yang saya mengerti, operator newdi C ++ tidak mirip dengan yang ada di C #.

Bisakah Anda menjelaskan alasan kebocoran memori dalam kode sampel ini?

class A { ... };
struct B { ... };

A *object1 = new A();
B object2 = *(new B());

Jawaban:


464

Apa yang terjadi

Saat Anda menulis, T t;Anda membuat objek bertipe Tdengan durasi penyimpanan otomatis . Ini akan dibersihkan secara otomatis ketika keluar dari ruang lingkup.

Saat Anda menulis, new T()Anda membuat objek bertipe Tdengan durasi penyimpanan dinamis . Itu tidak akan dibersihkan secara otomatis.

baru tanpa pembersihan

Anda harus meneruskan pointer ke sana deleteuntuk membersihkannya:

newing dengan delete

Namun, contoh kedua Anda lebih buruk: Anda mendereferensi pointer, dan membuat salinan objek. Dengan cara ini Anda kehilangan pointer ke objek yang dibuat dengan new, sehingga Anda tidak pernah bisa menghapusnya bahkan jika Anda mau!

baru dengan deref

Apa yang harus Anda lakukan

Anda harus memilih durasi penyimpanan otomatis. Butuh objek baru, cukup tulis:

A a; // a new object of type A
B b; // a new object of type B

Jika Anda memang membutuhkan durasi penyimpanan dinamis, simpan pointer ke objek yang dialokasikan dalam objek durasi penyimpanan otomatis yang menghapusnya secara otomatis.

template <typename T>
class automatic_pointer {
public:
    automatic_pointer(T* pointer) : pointer(pointer) {}

    // destructor: gets called upon cleanup
    // in this case, we want to use delete
    ~automatic_pointer() { delete pointer; }

    // emulate pointers!
    // with this we can write *p
    T& operator*() const { return *pointer; }
    // and with this we can write p->f()
    T* operator->() const { return pointer; }

private:
    T* pointer;

    // for this example, I'll just forbid copies
    // a smarter class could deal with this some other way
    automatic_pointer(automatic_pointer const&);
    automatic_pointer& operator=(automatic_pointer const&);
};

automatic_pointer<A> a(new A()); // acts like a pointer, but deletes automatically
automatic_pointer<B> b(new B()); // acts like a pointer, but deletes automatically

baru dengan automatic_pointer

Ini adalah ungkapan umum yang menggunakan nama RAII yang tidak terlalu deskriptif ( Resource Acquisition Is Inisialisasi ). Saat Anda memperoleh sumber daya yang perlu dibersihkan, Anda menempelkannya pada objek dengan durasi penyimpanan otomatis sehingga Anda tidak perlu khawatir untuk membersihkannya. Ini berlaku untuk sumber daya apa pun, baik itu memori, membuka file, koneksi jaringan, atau apa pun yang Anda suka.

Benda ini automatic_pointersudah ada dalam berbagai bentuk, saya baru saja memberikannya untuk memberi contoh. Kelas yang sangat mirip ada di perpustakaan standar yang disebut std::unique_ptr.

Ada juga yang lama (pra-C ++ 11) bernama auto_ptrtetapi sekarang sudah ditinggalkan karena memiliki perilaku penyalinan yang aneh.

Dan kemudian ada beberapa contoh yang lebih cerdas, seperti std::shared_ptr, yang memungkinkan banyak pointer ke objek yang sama dan hanya membersihkannya ketika pointer terakhir dihancurkan.


4
@ user1131997: senang Anda membuat pertanyaan lain. Seperti yang Anda lihat, tidak mudah untuk menjelaskannya dalam komentar :)
R. Martinho Fernandes

@ R.MartinhoFernandes: jawaban yang bagus. Hanya satu pertanyaan. Mengapa Anda menggunakan pengembalian dengan referensi dalam fungsi operator * ()?
Destructor

@Destructor terlambat membalas: D. Mengembalikan dengan referensi memungkinkan Anda memodifikasi pointee, sehingga Anda dapat melakukan, misalnya *p += 2, seperti yang Anda lakukan dengan pointer normal. Jika itu tidak kembali dengan referensi, itu tidak akan meniru perilaku pointer normal, yang menjadi maksud di sini.
R. Martinho Fernandes

Terima kasih banyak karena telah menyarankan untuk "menyimpan pointer ke objek yang dialokasikan dalam objek durasi penyimpanan otomatis yang menghapusnya secara otomatis." Andai saja ada cara untuk meminta coder mempelajari pola ini sebelum mereka dapat mengompilasi C ++!
Andy

35

Penjelasan langkah demi langkah:

// creates a new object on the heap:
new B()
// dereferences the object
*(new B())
// calls the copy constructor of B on the object
B object2 = *(new B());

Jadi pada akhir ini, Anda memiliki objek di heap tanpa pointer ke sana, jadi tidak mungkin untuk menghapus.

Sampel lain:

A *object1 = new A();

adalah kebocoran memori hanya jika Anda lupa dengan deletememori yang dialokasikan:

delete object1;

Di C ++ ada objek dengan penyimpanan otomatis, yang dibuat di stack, yang secara otomatis dibuang, dan objek dengan penyimpanan dinamis, di heap, yang Anda alokasikan dengan newdan diharuskan untuk membebaskan diri Anda dengan delete. (Ini semua kira-kira dimasukkan)

Pikirkan bahwa Anda harus memiliki deleteuntuk setiap objek yang dialokasikan new.

EDIT

Kalau dipikir-pikir itu, object2tidak harus menjadi kebocoran memori.

Kode berikut hanya untuk menyampaikan maksud, itu ide yang buruk, jangan pernah suka kode seperti ini:

class B
{
public:
    B() {};   //default constructor
    B(const B& other) //copy constructor, this will be called
                      //on the line B object2 = *(new B())
    {
        delete &other;
    }
}

Dalam hal ini, karena otherdilewatkan oleh referensi, itu akan menjadi objek tepat yang ditunjuk oleh new B(). Oleh karena itu, mendapatkan alamatnya dengan &otherdan menghapus pointer akan membebaskan memori.

Tapi saya tidak bisa cukup menekankan hal ini, jangan lakukan ini. Itu hanya di sini untuk menyampaikan maksud.


2
Saya memikirkan hal yang sama: kita bisa meretasnya agar tidak bocor tetapi Anda tidak ingin melakukannya. object1 tidak harus bocor juga, karena konstruktornya dapat melampirkan dirinya ke beberapa jenis struktur data yang akan menghapusnya di beberapa titik.
CashCow

2
Selalu saja tergoda untuk menulis jawaban "itu mungkin untuk dilakukan tetapi jangan"! :-) Saya tahu perasaannya
Kos

11

Diberi dua "objek":

obj a;
obj b;

Mereka tidak akan menempati lokasi yang sama dalam memori. Dengan kata lain,&a != &b

Menetapkan nilai satu ke yang lain tidak akan mengubah lokasi mereka, tetapi akan mengubah kontennya:

obj a;
obj b = a;
//a == b, but &a != &b

Secara intuitif, penunjuk "objek" bekerja dengan cara yang sama:

obj *a;
obj *b = a;
//a == b, but &a != &b

Sekarang, mari kita lihat contoh Anda:

A *object1 = new A();

Ini menetapkan nilai new A()untuk object1. Nilainya adalah pointer, artinya object1 == new A(), tetapi &object1 != &(new A()). (Perhatikan bahwa contoh ini bukan kode yang valid, ini hanya untuk penjelasan)

Karena nilai pointer dipertahankan, kita dapat membebaskan memori yang ditunjukkannya: delete object1;Karena aturan kami, ini berperilaku sama seperti delete (new A());yang tidak memiliki kebocoran.


Untuk Anda contoh kedua, Anda menyalin objek menunjuk-ke. Nilainya adalah isi dari objek itu, bukan pointer yang sebenarnya. Seperti dalam setiap kasus lainnya &object2 != &*(new A()),.

B object2 = *(new B());

Kami telah kehilangan pointer ke memori yang dialokasikan, dan karenanya kami tidak dapat membebaskannya. delete &object2;mungkin tampak seperti itu akan berhasil, tetapi karena &object2 != &*(new A()), itu tidak setara dengan delete (new A())dan tidak valid.


9

Di C # dan Java, Anda menggunakan yang baru untuk membuat turunan dari kelas apa pun dan kemudian Anda tidak perlu khawatir tentang menghancurkannya nanti.

C ++ juga memiliki kata kunci "baru" yang membuat objek tetapi tidak seperti di Jawa atau C #, itu bukan satu-satunya cara untuk membuat objek.

C ++ memiliki dua mekanisme untuk membuat objek:

  • otomatis
  • dinamis

Dengan kreasi otomatis, Anda membuat objek dalam lingkungan lingkup: - dalam fungsi atau - sebagai anggota kelas (atau struct).

Dalam suatu fungsi Anda akan membuatnya dengan cara ini:

int func()
{
   A a;
   B b( 1, 2 );
}

Dalam sebuah kelas Anda biasanya membuatnya seperti ini:

class A
{
  B b;
public:
  A();
};    

A::A() :
 b( 1, 2 )
{
}

Dalam kasus pertama, objek dihancurkan secara otomatis ketika blok lingkup keluar. Ini bisa berupa fungsi atau blok ruang lingkup dalam suatu fungsi.

Dalam kasus yang terakhir objek b dihancurkan bersama dengan instance dari A di mana ia adalah anggota.

Objek dialokasikan dengan yang baru ketika Anda perlu mengontrol masa hidup objek dan kemudian membutuhkan penghapusan untuk menghancurkannya. Dengan teknik yang dikenal sebagai RAII, Anda menangani penghapusan objek pada titik yang Anda buat dengan meletakkannya di dalam objek otomatis, dan menunggu penghancur objek otomatis itu berlaku.

Salah satu objek tersebut adalah shared_ptr yang akan memanggil logika "deleter" tetapi hanya ketika semua instance shared_ptr yang berbagi objek dihancurkan.

Secara umum, sementara kode Anda mungkin memiliki banyak panggilan ke yang baru, Anda harus membatasi panggilan untuk dihapus dan harus selalu memastikan ini dipanggil dari destruktor atau "deleter" objek yang dimasukkan ke dalam smart-pointer.

Destruktor Anda juga seharusnya tidak pernah melemparkan pengecualian.

Jika Anda melakukan ini, Anda akan memiliki sedikit kebocoran memori.


4
Ada lebih dari automaticdan dynamic. Ada juga static.
Mooing Duck

9
B object2 = *(new B());

Baris ini adalah penyebab kebocoran. Mari kita selesaikan ini sedikit ..

object2 adalah variabel tipe B, disimpan di alamat say 1 (Ya, saya memilih angka arbitrer di sini). Di sisi kanan, Anda telah meminta B baru, atau pointer ke objek tipe B. Program dengan senang hati memberikan ini kepada Anda dan menetapkan B baru Anda ke alamat 2 dan juga membuat pointer di alamat 3. Sekarang, satu-satunya cara untuk mengakses data di alamat 2 adalah melalui pointer di alamat 3. Selanjutnya, Anda menentukan pointer menggunakan *untuk mendapatkan data yang menunjuk ke pointer (data di alamat 2). Ini secara efektif membuat salinan data itu dan memberikannya ke object2, ditugaskan di alamat 1. Ingat, ini COPY, bukan yang asli.

Sekarang, inilah masalahnya:

Anda tidak pernah menyimpan pointer itu di mana pun Anda bisa menggunakannya! Setelah tugas ini selesai, pointer (memori di address3, yang Anda gunakan untuk mengakses address2) berada di luar jangkauan dan di luar jangkauan Anda! Anda tidak lagi dapat memanggil delete di atasnya dan karenanya tidak dapat membersihkan memori di address2. Yang tersisa adalah salinan data dari address2 di address1. Dua hal yang sama duduk dalam ingatan. Satu Anda dapat mengakses, yang lain Anda tidak dapat (karena Anda kehilangan jalan ke sana). Itu sebabnya ini adalah kebocoran memori.

Saya akan menyarankan datang dari latar belakang C # Anda bahwa Anda banyak membaca tentang bagaimana pointer dalam C ++ bekerja. Mereka adalah topik tingkat lanjut dan perlu waktu untuk dipahami, tetapi penggunaannya akan sangat berharga bagi Anda.


8

Jika itu membuatnya lebih mudah, anggap memori komputer seperti hotel dan program adalah pelanggan yang menyewa kamar ketika mereka membutuhkannya.

Cara hotel ini bekerja adalah Anda memesan kamar dan memberi tahu portir ketika Anda pergi.

Jika Anda memprogram memesan kamar dan pergi tanpa memberitahu porter, porter akan berpikir bahwa ruangan itu masih digunakan dan tidak akan membiarkan orang lain menggunakannya. Dalam hal ini ada kebocoran kamar.

Jika program Anda mengalokasikan memori dan tidak menghapusnya (itu hanya berhenti menggunakannya) maka komputer berpikir bahwa memori tersebut masih digunakan dan tidak akan membiarkan orang lain menggunakannya. Ini adalah kebocoran memori.

Ini bukan analogi yang tepat tetapi mungkin bisa membantu.


5
Saya sangat menyukai analogi itu, ini tidak sempurna, tetapi jelas cara yang bagus untuk menjelaskan kebocoran memori kepada orang-orang yang baru mengenalnya!
AdamM

1
Saya menggunakan ini dalam sebuah wawancara untuk seorang insinyur senior di Bloomberg di London untuk menjelaskan kebocoran memori kepada seorang gadis SDM. Saya melalui wawancara itu karena saya benar-benar dapat menjelaskan kebocoran memori (dan masalah threading) kepada seorang non programmer dengan cara yang dia mengerti.
Stefan

7

Saat membuat object2Anda sedang membuat salinan dari objek yang Anda buat dengan yang baru, tetapi Anda juga kehilangan pointer (tidak pernah ditugaskan) (jadi tidak ada cara untuk menghapusnya nanti). Untuk menghindari ini, Anda harus membuat object2referensi.


3
Ini praktik yang sangat buruk untuk mengambil alamat referensi untuk menghapus objek. Gunakan pointer cerdas.
Tom Whittock

3
Latihan yang sangat buruk, eh? Menurut Anda apa yang digunakan pointer pintar di belakang layar?
Blindy

3
@Blindy smart pointer (setidaknya yang diterapkan dengan benar) menggunakan pointer secara langsung.
Luchian Grigore

2
Ya, sejujurnya, seluruh Ide itu tidak terlalu bagus, bukan? Sebenarnya, saya bahkan tidak yakin di mana pola yang dicoba di OP akan benar-benar bermanfaat.
Mario

7

Nah, Anda membuat kebocoran memori jika Anda tidak membebaskan memori yang telah Anda alokasikan menggunakan newoperator dengan mengirimkan pointer ke memori tersebut ke deleteoperator.

Dalam dua kasus Anda di atas:

A *object1 = new A();

Di sini Anda tidak menggunakan deleteuntuk membebaskan memori, jadi jika dan ketika object1pointer Anda keluar dari ruang lingkup, Anda akan memiliki kebocoran memori, karena Anda akan kehilangan pointer dan karenanya tidak dapat menggunakan deleteoperator di dalamnya.

Dan di sini

B object2 = *(new B());

Anda membuang pointer yang dikembalikan oleh new B(), dan karenanya tidak pernah bisa melewatkan pointer itu agar deletememori bisa dibebaskan. Oleh karena itu memori lain bocor.


7

Baris inilah yang segera bocor:

B object2 = *(new B());

Di sini Anda membuat Bobjek baru di tumpukan, lalu membuat salinan di tumpukan. Yang telah dialokasikan pada heap tidak lagi dapat diakses dan karenanya bocor.

Baris ini tidak langsung bocor:

A *object1 = new A();

Akan ada kebocoran jika Anda tidak pernah deleted object1sekalipun.


4
Tolong jangan gunakan tumpukan / tumpukan saat menjelaskan penyimpanan dinamis / otomatis.
Pubby

2
@Pubby mengapa tidak digunakan? Karena penyimpanan dinamis / automaik selalu menumpuk, bukan tumpukan? Dan itu sebabnya tidak perlu detail tentang tumpukan / tumpukan, apakah saya benar?

4
@ user1131997 Heap / stack adalah detail implementasi. Mereka penting untuk diketahui, tetapi tidak relevan dengan pertanyaan ini.
Pubby

2
Hmm saya ingin jawaban yang terpisah untuk itu, yaitu sama dengan milik saya tetapi mengganti tumpukan / tumpukan dengan apa yang Anda pikir terbaik. Saya tertarik untuk mencari tahu bagaimana Anda ingin menjelaskannya.
mattjgalloway
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.