Apa yang terjadi pada sampah di C ++?

51

Java memiliki GC otomatis yang sesekali Menghentikan Dunia, tetapi mengurus sampah di tumpukan. Sekarang aplikasi C / C ++ tidak memiliki pembekuan STW ini, penggunaan memori mereka juga tidak bertambah. Bagaimana perilaku ini tercapai? Bagaimana benda mati dirawat?

Ju Shua
sumber
38
Catatan: stop-the-world adalah pilihan implementasi dari beberapa pemulung, tetapi tentu saja tidak semua. Ada GC bersamaan, misalnya, yang berjalan bersamaan dengan mutator (itulah yang disebut pengembang GC program aktual). Saya percaya Anda dapat membeli versi komersial open source IBM JVM J9 yang memiliki kolektor tanpa jeda bersamaan. Azul Zing memiliki kolektor "tanpa jeda" yang tidak benar - benar tanpa jeda tetapi sangat cepat sehingga tidak ada jeda yang terlihat (jeda GC-nya berada pada urutan yang sama dengan sakelar konteks utas sistem operasi, yang biasanya tidak dilihat sebagai jeda) .
Jörg W Mittag
14
Sebagian besar (lama berjalan) C ++ program saya menggunakan lakukan memiliki penggunaan memori yang tumbuh waktu lebih unboundedly. Apakah mungkin Anda tidak terbiasa membiarkan program terbuka selama lebih dari beberapa hari sekaligus?
Jonathan Cast
12
Mempertimbangkan bahwa dengan C ++ modern dan konstruknya Anda tidak perlu lagi menghapus memori secara manual (kecuali Anda mengejar beberapa optimasi khusus), karena Anda dapat mengelola memori dinamis melalui pointer pintar. Jelas, itu menambahkan beberapa overhead untuk pengembangan C ++ dan Anda perlu sedikit lebih berhati-hati, tapi itu bukan hal yang sama sekali berbeda, Anda hanya perlu ingat untuk menggunakan smart pointer construct daripada hanya memanggil manual new.
Andy
9
Perhatikan bahwa masih mungkin terjadi kebocoran memori dalam bahasa yang dikumpulkan sampah. Saya tidak terbiasa dengan Java, tetapi sayangnya kebocoran memori cukup umum di dunia .NET. Objek yang direferensikan secara tidak langsung oleh bidang statis tidak secara otomatis dikumpulkan, event handler adalah sumber kebocoran yang sangat umum, dan sifat pengumpulan sampah yang non-deterministik membuatnya tidak dapat sepenuhnya menghilangkan kebutuhan untuk membebaskan sumber daya secara manual (mengarah ke IDisposable pola). Semua mengatakan, model manajemen memori C ++ yang digunakan dengan benar jauh lebih unggul dari pengumpulan sampah.
Cody Gray
26
What happens to garbage in C++? Bukankah biasanya dikompilasi menjadi executable?
BJ Myers

Jawaban:

100

Programmer bertanggung jawab untuk memastikan bahwa objek yang mereka buat newdihapus melalui delete. Jika suatu objek dibuat, tetapi tidak dihancurkan sebelum pointer terakhir atau referensi untuk itu keluar dari ruang lingkup, ia jatuh melalui celah-celah dan menjadi Memory Leak .

Sayangnya untuk C, C ++ dan bahasa lain yang tidak termasuk GC, ini hanya menumpuk dari waktu ke waktu. Ini dapat menyebabkan aplikasi atau sistem kehabisan memori dan tidak dapat mengalokasikan blok memori baru. Pada titik ini, pengguna harus menggunakan untuk mengakhiri aplikasi sehingga Sistem Operasi dapat merebut kembali memori yang digunakan.

Sejauh mengatasi masalah ini, ada beberapa hal yang membuat hidup seorang programmer jauh lebih mudah. Ini terutama didukung oleh sifat ruang lingkup .

int main()
{
    int* variableThatIsAPointer = new int;
    int variableInt = 0;

    delete variableThatIsAPointer;
}

Di sini, kami membuat dua variabel. Mereka ada di Blok Lingkup , seperti yang didefinisikan oleh {}kurung kurawal. Ketika eksekusi bergerak keluar dari lingkup ini, objek-objek ini akan dihapus secara otomatis. Dalam hal ini, variableThatIsAPointerseperti namanya, adalah penunjuk ke objek di memori. Ketika keluar dari ruang lingkup, pointer dihapus, tetapi objek yang ditunjuknya tetap ada. Di sini, kita deleteobjek ini sebelum keluar dari ruang lingkup untuk memastikan bahwa tidak ada kebocoran memori. Namun kami juga bisa melewati pointer ini di tempat lain dan berharap itu akan dihapus nanti.

Sifat lingkup ini meluas ke kelas:

class Foo
{
public:
    int bar; // Will be deleted when Foo is deleted
    int* otherBar; // Still need to call delete
}

Di sini, prinsip yang sama berlaku. Kami tidak perlu khawatir tentang barkapan Foodihapus. Namun untuk otherBar, hanya pointer yang dihapus. Jika otherBarsatu-satunya penunjuk yang valid untuk objek apa pun yang ditunjukkannya, kita mungkin harus deletedalam Foodestruktor. Ini adalah konsep pendorong di belakang RAII

alokasi sumber daya (akuisisi) dilakukan selama pembuatan objek (khusus inisialisasi), oleh konstruktor, sedangkan alokasi sumber daya (rilis) dilakukan selama penghancuran objek (khusus finalisasi), oleh destruktor. Dengan demikian sumber daya dijamin akan diadakan antara ketika inisialisasi selesai dan finalisasi dimulai (memegang sumber daya adalah kelas invarian), dan ditahan hanya ketika objek hidup. Jadi jika tidak ada kebocoran objek, tidak ada kebocoran sumber daya.

RAII juga merupakan kekuatan pendorong khas di belakang Smart Pointers . Dalam Standard Library C ++, ini std::shared_ptr, std::unique_ptrdan std::weak_ptr; walaupun saya telah melihat dan menggunakan shared_ptr/ weak_ptrimplementasi lain yang mengikuti konsep yang sama. Untuk ini, penghitung referensi melacak berapa banyak pointer ke objek yang diberikan, dan secara otomatis deleteobjek setelah tidak ada lagi referensi untuk itu.

Lebih dari itu, semuanya bermuara pada praktik dan disiplin yang tepat bagi seorang programmer untuk memastikan bahwa kode mereka menangani objek dengan benar.

Thebluefish
sumber
4
dihapus melalui delete- itulah yang saya cari. Luar biasa.
Ju Shua
3
Anda mungkin ingin menambahkan tentang mekanisme pelingkupan yang disediakan dalam c ++ yang memungkinkan sebagian besar yang baru dan hapus dibuat sebagian besar otomatis.
whatsisname
9
@whatsisname bukan itu baru dan hapus dibuat otomatis, itu adalah bahwa mereka tidak muncul sama sekali dalam banyak kasus
Caleth
10
Secara deleteotomatis dipanggil untuk Anda oleh pointer pintar jika Anda menggunakannya sehingga Anda harus mempertimbangkan menggunakannya setiap kali ketika penyimpanan otomatis tidak dapat digunakan.
Marian Spanik
11
@JuShua Perhatikan bahwa saat menulis C ++ modern, Anda seharusnya tidak perlu benar-benar memiliki deletekode aplikasi Anda (dan mulai C ++ 14 dan seterusnya, sama dengan new), tetapi gunakan pointer pintar dan RAII agar objek tumpukan dihapus. std::unique_ptrjenis dan std::make_uniquefungsi adalah langsung, penggantian sederhana newdan deletepada tingkat kode aplikasi.
hyde
82

C ++ tidak memiliki pengumpulan sampah.

Aplikasi C ++ diperlukan untuk membuang sampah mereka sendiri.

Pemrogram aplikasi C ++ harus memahami hal ini.

Ketika mereka lupa, hasilnya disebut "kebocoran memori".

John R. Strohm
sumber
22
Anda tentu memastikan jawaban Anda tidak mengandung sampah, atau boilerplate ...
leftaroundabout
15
@leftaroundabout: Terima kasih. Saya menganggap itu sebagai pujian.
John R. Strohm
1
OK, jawaban bebas-sampah ini memang memiliki kata kunci untuk dicari: kebocoran memori. Akan menyenangkan juga untuk menyebutkan newdan delete.
Ruslan
4
@Ruslan yang sama juga berlaku untuk mallocdan free, atau new[]dan delete[], atau penyalur lainnya (seperti Windows ini GlobalAlloc, LocalAlloc, SHAlloc, CoTaskMemAlloc, VirtualAlloc, HeapAlloc, ...), dan memori yang dialokasikan untuk Anda (misalnya melalui fopen).
user253751
43

Dalam C, C ++ dan sistem lain tanpa Pengumpul Sampah, pengembang ditawarkan fasilitas oleh bahasa dan pustaka untuk menunjukkan kapan memori dapat direklamasi.

Fasilitas paling mendasar adalah penyimpanan otomatis . Sering kali, bahasa itu sendiri memastikan bahwa barang-barang dibuang:

int global = 0; // automatic storage

int foo(int a, int b) {
    static int local = 1; // automatic storage

    int c = a + b; // automatic storage

    return c;
}

Dalam kasus ini, kompiler bertugas mengetahui kapan nilai-nilai itu tidak digunakan dan mengambil kembali penyimpanan yang terkait dengannya.

Saat menggunakan penyimpanan dinamis , dalam C, memori secara tradisional dialokasikan dengan mallocdan direklamasi dengan free. Dalam C ++, memori secara tradisional dialokasikan dengan newdan direklamasi dengan delete.

C tidak banyak berubah selama bertahun-tahun, namun C ++ modern menghindar newdan deletesepenuhnya bergantung pada fasilitas perpustakaan (yang digunakan newdan digunakan dengan deletetepat):

  • pointer cerdas adalah yang paling terkenal: std::unique_ptrdanstd::shared_ptr
  • tapi wadah jauh lebih luas sebenarnya: std::string, std::vector, std::map, ... semua internal mengelola memori dialokasikan secara dinamis transparan

Omong-omong shared_ptr, ada risiko: jika siklus referensi terbentuk, dan tidak rusak, maka kebocoran memori mungkin ada. Terserah pengembang untuk menghindari situasi ini, cara paling sederhana untuk menghindari shared_ptrsama sekali dan yang paling sederhana adalah untuk menghindari siklus di tingkat tipe.

Akibatnya kebocoran memori tidak menjadi masalah dalam C ++ , bahkan untuk pengguna baru, selama mereka tidak menggunakan new, deleteatau std::shared_ptr. Ini tidak seperti C di mana disiplin yang kuat diperlukan, dan umumnya tidak memadai.


Namun, jawaban ini tidak akan lengkap tanpa menyebutkan saudara kembar kebocoran memori: pointer menggantung .

Pointer menggantung (atau referensi menggantung) adalah bahaya yang dibuat dengan menjaga pointer atau referensi ke objek yang mati. Sebagai contoh:

int main() {
    std::vector<int> vec;
    vec.push_back(1);     // vec: [1]

    int& a = vec.back();

    vec.pop_back();       // vec: [], "a" is now dangling

    std::cout << a << "\n";
}

Menggunakan pointer yang menggantung, atau referensi, adalah Perilaku Tidak Terdefinisi . Secara umum, untungnya, ini adalah crash langsung; cukup sering, sayangnya, ini menyebabkan kerusakan memori pertama ... dan dari waktu ke waktu perilaku aneh muncul karena kompiler memancarkan kode yang sangat aneh.

Perilaku tidak terdefinisi adalah masalah terbesar dengan C dan C ++ hingga hari ini, dalam hal keamanan / kebenaran program. Anda mungkin ingin melihat bahasa Rust tanpa Pengumpul Sampah dan Perilaku Tidak Terdefinisi.

Matthieu M.
sumber
17
Re: "Menggunakan penunjuk menggantung, atau referensi, adalah Perilaku Tidak Terdefinisi . Secara umum, untungnya, ini adalah crash langsung": Benarkah? Itu sama sekali tidak cocok dengan pengalaman saya; sebaliknya, pengalaman saya adalah bahwa penggunaan pointer yang menggantung hampir tidak pernah menyebabkan crash langsung. . .
ruakh
9
Ya, karena menjadi "menggantung" pointer harus menargetkan memori yang dialokasikan sebelumnya pada satu titik, dan memori itu biasanya tidak mungkin benar-benar belum dipetakan dari proses sehingga tidak lagi dapat diakses sama sekali, karena itu akan menjadi kandidat yang baik untuk digunakan kembali segera ... dalam praktiknya, pointer yang menggantung tidak menyebabkan crash, mereka menyebabkan kekacauan.
Leushenko
2
"Akibatnya kebocoran memori tidak menjadi masalah di C ++," Tentu saja, selalu ada ikatan C ke perpustakaan untuk dikacaukan, serta shared_ptrs rekursif atau bahkan unique_ptrs rekursif, dan situasi lainnya.
Mooing Duck
3
“Bukan masalah dalam C ++, bahkan untuk pengguna baru” - Saya akan memenuhi syarat itu untuk “pengguna baru yang tidak berasal dari bahasa atau C seperti bahasa Jawa ”.
leftaroundabout
3
@leftaroundabout: memenuhi syarat "selama mereka tidak menggunakan new, deletedan shared_ptr"; tanpa newdan shared_ptrAnda memiliki kepemilikan langsung sehingga tidak ada kebocoran. Tentu saja, Anda cenderung memiliki pointer menggantung, dll ... tapi saya khawatir Anda harus meninggalkan C ++ untuk menyingkirkannya.
Matthieu M.
27

C ++ memiliki hal ini disebut RAII . Pada dasarnya itu berarti sampah dibersihkan saat Anda pergi daripada meninggalkannya di tumpukan dan membiarkan pembersih merapikan setelah Anda. (bayangkan saya di kamar saya menonton sepak bola - ketika saya minum kaleng bir dan membutuhkan yang baru, cara C ++ adalah dengan membawa kaleng kosong ke tempat sampah dalam perjalanan ke lemari es, cara C # adalah membuangnya ke lantai dan menunggu pelayan untuk mengambilnya ketika dia datang untuk membersihkan).

Sekarang dimungkinkan untuk membocorkan memori dalam C ++, tetapi untuk melakukannya mengharuskan Anda meninggalkan konstruksi yang biasa dan kembali ke cara C melakukan sesuatu - mengalokasikan blok memori dan melacak di mana blok itu tanpa bantuan bahasa. Beberapa orang lupa pointer ini sehingga tidak dapat menghapus blokir.

gbjbaanb
sumber
9
Pointer bersama (yang menggunakan RAII) menyediakan cara modern untuk membuat kebocoran. Misalkan objek A dan B mereferensikan satu sama lain melalui pointer bersama, dan tidak ada yang mereferensikan objek A atau objek B. Hasilnya adalah kebocoran. Referensi timbal balik ini adalah non-masalah dalam bahasa dengan pengumpulan sampah.
David Hammen
@ Davidvidam yakin, tetapi dengan biaya melintasi hampir setiap objek untuk memastikan. Contoh Anda tentang pointer pintar mengabaikan fakta bahwa pointer pintar itu sendiri akan keluar dari ruang lingkup dan kemudian objek akan dibebaskan. Anda menganggap pointer cerdas seperti pointer, bukan, itu objek yang dilewatkan di tumpukan seperti kebanyakan parameter. Ini tidak jauh berbeda dengan kebocoran memori yang disebabkan oleh bahasa GC ,. misalnya yang terkenal di mana menghapus event handler dari kelas UI meninggalkannya diam-diam direferensikan dan karenanya bocor.
gbjbaanb
1
@ Gbjbaanb dalam contoh dengan pointer pintar, pointer pintar tidak pernah keluar dari ruang lingkup, itu sebabnya ada kebocoran. Karena kedua objek penunjuk pintar dialokasikan dalam ruang lingkup dinamis , bukan yang leksikal, mereka masing-masing mencoba untuk menunggu yang lain sebelum merusak. Fakta bahwa smart pointer adalah objek nyata dalam C ++ dan bukan hanya pointer yang menyebabkan kebocoran di sini - objek smart pointer tambahan dalam cakupan stack yang juga menunjuk ke objek kontainer tidak dapat membatalkan alokasi ketika mereka merusak diri mereka sendiri karena refcount adalah tidak nol.
Leushenko
2
Cara .NET tidak membuangnya ke lantai. Itu hanya menyimpannya di tempat itu sampai pelayan datang. Dan karena cara. NET mengalokasikan memori dalam praktek (bukan kontrak), tumpukan lebih seperti tumpukan akses-acak. Ini seperti memiliki setumpuk kontrak dan surat-surat, dan sekali-sekali membukanya untuk membuang yang tidak berlaku lagi. Dan untuk membuatnya lebih mudah, yang bertahan setiap buangan dipromosikan ke tumpukan yang berbeda, sehingga Anda dapat menghindari melintasi semua tumpukan sebagian besar waktu - kecuali tumpukan pertama menjadi cukup besar, pelayan tidak menyentuh yang lain.
Luaan
@Luaan itu analogi ... Saya kira Anda akan lebih bahagia jika saya mengatakan itu meninggalkan kaleng-kaleng di atas meja sampai pelayan datang untuk membersihkan.
gbjbaanb
26

Perlu dicatat bahwa, dalam kasus C ++, kesalahpahaman umum bahwa "Anda perlu melakukan manajemen memori manual". Bahkan, Anda biasanya tidak melakukan manajemen memori dalam kode Anda.

Objek ukuran tetap (dengan masa hidup lingkup)

Dalam sebagian besar kasus ketika Anda membutuhkan objek, objek tersebut akan memiliki masa hidup yang ditentukan dalam program Anda dan dibuat di stack. Ini berfungsi untuk semua tipe data primitif bawaan, tetapi juga untuk instance kelas dan struct:

class MyObject {
    public: int x;
};

int objTest()
{
    MyObject obj;
    obj.x = 5;
    return obj.x;
}

Tumpukan objek secara otomatis dihapus ketika fungsi berakhir. Di Jawa, objek selalu dibuat di heap, dan karena itu harus dihapus oleh beberapa mekanisme seperti pengumpulan sampah. Ini bukan masalah untuk objek tumpukan.

Objek yang mengelola data dinamis (dengan cakupan masa pakai)

Menggunakan ruang pada tumpukan berfungsi untuk objek dengan ukuran tetap. Ketika Anda membutuhkan jumlah ruang variabel, seperti array, pendekatan lain digunakan: Daftar ini diringkas dalam objek ukuran tetap yang mengelola memori dinamis untuk Anda. Ini berfungsi karena objek dapat memiliki fungsi pembersihan khusus, penghancur. Dijamin akan dipanggil ketika objek keluar dari ruang lingkup dan melakukan kebalikan dari konstruktor:

class MyList {        
public:
    // a fixed-size pointer to the actual memory.
    int* listOfInts; 
    // constructor: get memory
    MyList(size_t numElements) { listOfInts = new int[numElements]; }
    // destructor: free memory
    ~MyList() { delete[] listOfInts; }
};

int listTest()
{
    MyList list(1024);
    list.listOfInts[200] = 5;
    return list.listOfInts[200];
    // When MyList goes off stack here, its destructor is called and frees the memory.
}

Tidak ada manajemen memori sama sekali dalam kode di mana memori digunakan. Satu-satunya hal yang perlu kita pastikan adalah bahwa objek yang kita tulis memiliki destruktor yang sesuai. Tidak peduli bagaimana kita meninggalkan ruang lingkup listTest, baik itu melalui pengecualian atau hanya dengan kembali dari itu, destruktor ~MyList()akan dipanggil dan kita tidak perlu mengelola memori apa pun.

(Saya pikir ini adalah keputusan desain yang lucu untuk menggunakan operator biner BUKAN~ ,, untuk menunjukkan destruktor. Ketika digunakan pada angka, itu membalikkan bit; dalam analogi, ini menunjukkan bahwa apa yang dilakukan konstruktor terbalik.)

Pada dasarnya semua objek C ++ yang membutuhkan memori dinamis menggunakan enkapsulasi ini. Itu telah disebut RAII ("akuisisi sumber daya adalah inisialisasi"), yang merupakan cara yang cukup aneh untuk mengekspresikan ide sederhana bahwa objek peduli dengan konten mereka sendiri; apa yang mereka peroleh adalah milik mereka untuk dibersihkan.

Objek polimorfik dan masa hidup di luar ruang lingkup

Sekarang, kedua kasus ini untuk memori yang memiliki masa pakai yang jelas: Masa pakai sama dengan ruang lingkup. Jika kita tidak ingin objek kedaluwarsa saat kita meninggalkan ruang lingkup, ada mekanisme ketiga yang dapat mengatur memori untuk kita: pointer cerdas. Pointer pintar juga digunakan ketika Anda memiliki instance objek yang tipenya bervariasi saat runtime, tetapi yang memiliki antarmuka umum atau kelas dasar:

class MyDerivedObject : public MyObject {
    public: int y;
};
std::unique_ptr<MyObject> createObject()
{
    // actually creates an object of a derived class,
    // but the user doesn't need to know this.
    return std::make_unique<MyDerivedObject>();
}

int dynamicObjTest()
{
    std::unique_ptr<MyObject> obj = createObject();
    obj->x = 5;
    return obj->x;
    // At scope end, the unique_ptr automatically removes the object it contains,
    // calling its destructor if it has one.
}

Ada jenis lain dari smart pointer std::shared_ptr,, untuk berbagi objek di antara beberapa klien. Mereka hanya menghapus objek yang terkandung ketika klien terakhir keluar dari ruang lingkup, sehingga mereka dapat digunakan dalam situasi di mana sama sekali tidak diketahui berapa banyak klien akan ada dan berapa lama mereka akan menggunakan objek.

Singkatnya, kami melihat bahwa Anda tidak benar-benar melakukan manajemen memori manual. Semuanya dienkapsulasi dan kemudian dirawat dengan cara yang sepenuhnya otomatis, manajemen memori berbasis ruang lingkup. Dalam kasus di mana ini tidak cukup, pointer pintar digunakan yang merangkum memori mentah.

Ini dianggap praktik yang sangat buruk untuk menggunakan pointer mentah sebagai pemilik sumber daya di mana saja dalam kode C ++, alokasi mentah di luar konstruktor, dan deletepanggilan mentah di luar destruktor, karena mereka hampir mustahil untuk dikelola ketika pengecualian terjadi, dan umumnya sulit digunakan dengan aman.

Yang terbaik: ini bekerja untuk semua jenis sumber daya

Salah satu manfaat terbesar RAII adalah tidak terbatas pada memori. Ini sebenarnya menyediakan cara yang sangat alami untuk mengelola sumber daya seperti file dan soket (membuka / menutup) dan mekanisme sinkronisasi seperti mutex (mengunci / membuka kunci). Pada dasarnya, setiap sumber daya yang dapat diperoleh dan harus dirilis dikelola dengan cara yang persis sama di C ++, dan tidak satu pun dari manajemen ini diserahkan kepada pengguna. Itu semua dirangkum dalam kelas yang memperoleh di konstruktor dan rilis di destruktor.

Misalnya, fungsi mengunci mutex biasanya ditulis seperti ini di C ++:

void criticalSection() {
    std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
    doSynchronizedStuff();
} // myMutex is released here automatically

Bahasa lain membuat ini jauh lebih rumit, baik dengan mengharuskan Anda melakukan ini secara manual (misalnya dalam finallyklausa) atau mereka menelurkan mekanisme khusus yang memecahkan masalah ini, tetapi tidak dengan cara yang sangat elegan (biasanya nanti dalam hidup mereka, ketika cukup banyak orang memiliki menderita karena kekurangannya). Mekanisme seperti ini adalah coba-dengan-sumber daya di Jawa dan pernyataan penggunaan dalam C #, yang keduanya merupakan perkiraan RAII C ++.

Jadi, singkatnya, semua ini adalah akun RAII yang sangat dangkal di C ++, tapi saya harap ini membantu pembaca untuk memahami bahwa memori dan bahkan manajemen sumber daya di C ++ biasanya tidak "manual", tetapi sebenarnya kebanyakan otomatis.

Felix Dombek
sumber
7
Ini adalah satu-satunya jawaban yang tidak memberi informasi yang salah pada orang atau melukis C ++ lebih sulit atau berbahaya daripada sebenarnya.
Alexander Revo
6
BTW, itu hanya dianggap praktik buruk untuk menggunakan pointer mentah sebagai pemilik sumber daya. Tidak ada salahnya menggunakan mereka jika mereka menunjuk ke sesuatu yang dijamin hidup lebih lama dari pointer itu sendiri.
Alexander Revo
8
Aku yang kedua Alexander. Saya bingung melihat jawaban "C ++ tidak memiliki manajemen memori otomatis, lupakan a deletedan Anda mati" meroket di atas 30 poin dan diterima, sementara yang ini memiliki lima. Adakah yang benar-benar menggunakan C ++ di sini?
Quentin
8

Sehubungan dengan C secara khusus, bahasa tidak memberi Anda alat untuk mengelola memori yang dialokasikan secara dinamis. Anda benar-benar bertanggung jawab untuk memastikan setiap orang *allocmemiliki tempat yang sesuai free.

Di mana segala sesuatu menjadi benar-benar buruk adalah ketika alokasi sumber daya gagal di tengah jalan; apakah Anda mencoba lagi, apakah Anda memutar kembali dan memulai dari awal, apakah Anda memutar kembali dan keluar dengan kesalahan, apakah Anda langsung keluar jaminan dan membiarkan OS berurusan dengan itu?

Sebagai contoh, inilah fungsi untuk mengalokasikan array 2D yang tidak berdekatan. Perilaku di sini adalah bahwa jika kegagalan alokasi terjadi di tengah proses, kami memutar semuanya kembali dan mengembalikan indikasi kesalahan menggunakan pointer NULL:

/**
 * Allocate space for an array of arrays; returns NULL
 * on error.
 */
int **newArr( size_t rows, size_t cols )
{
  int **arr = malloc( sizeof *arr * rows );
  size_t i;

  if ( arr ) // malloc returns NULL on failure
  {
    for ( i = 0; i < rows; i++ )
    {
      arr[i] = malloc( sizeof *arr[i] * cols );
      if ( !arr[i] )
      {
        /**
         * Whoopsie; we can't allocate any more memory for some reason.
         * We can't just return NULL at this point since we'll lose access
         * to the previously allocated memory, so we branch to some cleanup
         * code to undo the allocations made so far.  
         */
        goto cleanup;
      }
    }
  }
  goto done;

/**
 * We encountered a failure midway through memory allocation,
 * so we roll back all previous allocations and return NULL.
 */
cleanup:
  while ( i )         // this is why we didn't limit the scope of i to the for loop
    free( arr[--i] ); // delete previously allocated rows
  free( arr );        // delete arr object
  arr = NULL;

done:
  return arr;
}

Kode ini sangat jelek dengan gotoitu, tetapi, jika tidak ada mekanisme penanganan pengecualian terstruktur, ini cukup banyak satu-satunya cara untuk mengatasi masalah tanpa hanya menyerah sepenuhnya, terutama jika kode alokasi sumber daya Anda bersarang lebih banyak lebih dari satu loop. Ini adalah salah satu dari beberapa kali dimana gotosebenarnya merupakan pilihan yang menarik; kalau tidak, Anda menggunakan banyak bendera dan ifpernyataan tambahan .

Anda dapat membuat hidup Anda lebih mudah dengan menulis fungsi pengalokasi / deallocator khusus untuk setiap sumber daya, misalnya

Foo *newFoo( void )
{
  Foo *foo = malloc( sizeof *foo );
  if ( foo )
  {
    foo->bar = newBar();
    if ( !foo->bar ) goto cleanupBar;
    foo->bletch = newBletch(); 
    if ( !foo->bletch ) goto cleanupBletch;
    ...
  }
  goto done;

cleanupBletch:
  deleteBar( foo->bar );
  // fall through to clean up the rest

cleanupBar:
  free( foo );
  foo = NULL;

done:
  return foo;
}

void deleteFoo( Foo *f )
{
  deleteBar( f->bar );
  deleteBletch( f->bletch );
  free( f );
}
John Bode
sumber
1
Ini adalah jawaban yang bagus, bahkan dengan gotopernyataan. Ini adalah praktik yang disarankan di beberapa daerah. Ini adalah skema yang biasa digunakan untuk melindungi terhadap pengecualian di C. Lihat kode kernel Linux, yang penuh dengan gotopernyataan - dan yang tidak bocor.
David Hammen
"tanpa hanya menyerah sepenuhnya" -> dalam keadilan, jika Anda ingin berbicara tentang C, ini mungkin praktik yang baik. C adalah bahasa yang paling baik digunakan untuk menangani blok memori yang datang dari tempat lain, atau membagi-bagikan potongan memori kecil ke prosedur lain, tetapi lebih disukai tidak melakukan keduanya pada saat yang sama dengan cara yang disisipkan. Jika Anda menggunakan "objek" klasik di C, kemungkinan Anda tidak menggunakan bahasa itu untuk kekuatannya.
Leushenko
Yang kedua gotoadalah asing. Akan lebih mudah dibaca jika Anda mengubah goto done;ke return arr;dan arr=NULL;done:return arr;ke return NULL;. Meskipun dalam kasus-kasus yang lebih rumit mungkin memang ada banyak gotos, mulai membuka gulungan pada tingkat kesiapan yang berbeda (apa yang akan dilakukan oleh stack stack unwinding dalam C ++).
Ruslan
2

Saya telah belajar untuk mengklasifikasikan masalah memori ke dalam sejumlah kategori berbeda.

  • Satu kali menetes. Misalkan sebuah program bocor 100 byte pada saat startup, hanya saja tidak pernah bocor lagi. Mengejar dan menghilangkan kebocoran satu kali itu bagus (saya suka memiliki laporan bersih dengan kemampuan deteksi kebocoran) tetapi tidak penting. Terkadang ada masalah yang lebih besar yang perlu diserang.

  • Kebocoran berulang. Fungsi yang disebut berulang-ulang selama masa hidup program yang secara teratur membocorkan memori merupakan masalah besar. Tetes ini akan menyiksa program, dan mungkin OS, sampai mati.

  • Referensi bersama. Jika objek A dan B merujuk satu sama lain melalui pointer bersama, Anda harus melakukan sesuatu yang istimewa, baik dalam desain kelas-kelas tersebut atau dalam kode yang mengimplementasikan / menggunakan kelas-kelas tersebut untuk memutus sirkularitas. (Ini bukan masalah untuk bahasa sampah yang dikumpulkan.)

  • Mengingat terlalu banyak. Ini adalah sepupu jahat sampah / memori yang bocor. RAII tidak akan membantu di sini, juga tidak akan mengumpulkan sampah. Ini masalah dalam bahasa apa pun. Jika beberapa variabel aktif memiliki jalur yang menghubungkannya ke beberapa memori acak, memori acak tersebut bukanlah sampah. Membuat program menjadi pelupa sehingga bisa berjalan selama beberapa hari itu sulit. Membuat program yang dapat berjalan selama beberapa bulan (misalnya, sampai disk gagal) sangat, sangat rumit.

Saya tidak punya masalah serius dengan kebocoran untuk waktu yang sangat lama. Menggunakan RAII di C ++ sangat membantu mengatasi tetesan dan kebocoran itu. (Namun seseorang harus berhati-hati dengan pointer bersama.) Jauh lebih penting saya punya masalah dengan aplikasi yang penggunaan ingatannya terus tumbuh dan tumbuh dan berkembang karena koneksi yang tidak rata ke memori yang tidak lagi digunakan.

David Hammen
sumber
-6

Terserah kepada programmer C ++ untuk mengimplementasikan bentuk pengumpulan sampahnya sendiri jika diperlukan. Kegagalan untuk melakukannya akan menghasilkan apa yang disebut 'kebocoran memori'. Sangat umum untuk bahasa 'tingkat tinggi' (seperti Java) dibangun dalam pengumpulan sampah, tetapi bahasa 'tingkat rendah' ​​seperti C dan C ++ tidak.

xDr_Johnx
sumber