Stack, Static, dan Heap di C ++

160

Saya sudah mencari, tetapi saya tidak mengerti dengan baik ketiga konsep ini. Kapan saya harus menggunakan alokasi dinamis (di heap) dan apa keuntungan sebenarnya? Apa masalah statis dan tumpukan? Bisakah saya menulis seluruh aplikasi tanpa mengalokasikan variabel di heap?

Saya mendengar bahwa bahasa lain menggunakan "pengumpul sampah" sehingga Anda tidak perlu khawatir tentang memori. Apa yang dilakukan oleh pemulung?

Apa yang dapat Anda lakukan dengan memanipulasi memori sendiri yang tidak dapat Anda lakukan dengan menggunakan pengumpul sampah ini?

Pernah seseorang berkata kepada saya bahwa dengan deklarasi ini:

int * asafe=new int;

Saya memiliki "pointer ke pointer". Apa artinya? Ini berbeda dari:

asafe=new int;

?

Hai
sumber
Ada pertanyaan serupa yang diajukan beberapa waktu lalu: Apa dan di mana tumpukan dan tumpukan itu? Ada beberapa jawaban yang benar-benar bagus untuk pertanyaan itu yang seharusnya menjelaskan pertanyaan Anda.
Scott Saad
Kemungkinan duplikat dari Apa dan di mana tumpukan dan tumpukan itu?
Swati Garg

Jawaban:

223

Pertanyaan serupa diajukan , tetapi tidak menanyakan statika.

Ringkasan dari apa itu statis, tumpukan, dan tumpukan memori:

  • Variabel statis pada dasarnya adalah variabel global, bahkan jika Anda tidak dapat mengaksesnya secara global. Biasanya ada alamat untuk itu yang ada di executable itu sendiri. Hanya ada satu salinan untuk seluruh program. Tidak peduli berapa kali Anda masuk ke panggilan fungsi (atau kelas) (dan berapa banyak utas!) Variabel merujuk ke lokasi memori yang sama.

  • Tumpukan adalah sekelompok memori yang dapat digunakan secara dinamis. Jika Anda ingin 4kb untuk suatu objek maka pengalokasi dinamis akan melihat daftar ruang kosong di tumpukan, memilih potongan 4kb, dan memberikannya kepada Anda. Secara umum, pengalokasi memori dinamis (malloc, baru, dll) dimulai pada akhir memori dan bekerja mundur.

  • Menjelaskan bagaimana tumpukan tumbuh dan menyusut sedikit di luar cakupan jawaban ini, tetapi cukuplah untuk mengatakan Anda selalu menambah dan menghapus dari bagian akhir saja. Tumpukan biasanya mulai tinggi dan tumbuh ke alamat yang lebih rendah. Anda kehabisan memori ketika tumpukan memenuhi pengalokasi dinamis di suatu tempat di tengah (tetapi merujuk ke memori fisik dan virtual dan fragmentasi). Beberapa utas akan membutuhkan banyak tumpukan (proses umumnya menyimpan ukuran minimum untuk tumpukan).

Ketika Anda ingin menggunakan masing-masing:

  • Statika / global berguna untuk memori yang Anda tahu akan selalu Anda butuhkan dan Anda tahu bahwa Anda tidak ingin membatalkan alokasi. (Omong-omong, lingkungan tertanam mungkin dianggap hanya memiliki memori statis ... tumpukan dan tumpukan adalah bagian dari ruang alamat yang diketahui digunakan bersama oleh tipe memori ketiga: kode program. Program seringkali akan melakukan alokasi dinamis dari memori statis ketika mereka membutuhkan hal-hal seperti daftar yang ditautkan, tetapi terlepas dari itu, memori statis itu sendiri (buffer) itu sendiri tidak "dialokasikan", tetapi benda-benda lain dialokasikan keluar dari memori yang dimiliki oleh buffer untuk tujuan ini. Anda dapat melakukan ini di non-embedded juga, dan konsol game akan sering menghindari mekanisme memori dinamis yang dibangun dalam mendukung ketat mengontrol proses alokasi dengan menggunakan buffer ukuran yang telah ditetapkan untuk semua alokasi.)

  • Variabel stack berguna ketika Anda tahu bahwa selama fungsi berada dalam ruang lingkup (di stack di suatu tempat), Anda ingin variabel tetap. Tumpukan bagus untuk variabel yang Anda butuhkan untuk kode di mana mereka berada, tetapi yang tidak diperlukan di luar kode itu. Mereka juga sangat bagus ketika Anda mengakses sumber daya, seperti file, dan ingin sumber daya secara otomatis hilang ketika Anda meninggalkan kode itu.

  • Alokasi tumpukan (memori yang dialokasikan secara dinamis) berguna ketika Anda ingin lebih fleksibel daripada yang di atas. Seringkali, suatu fungsi dipanggil untuk merespons suatu peristiwa (pengguna mengklik tombol "buat kotak"). Respons yang tepat mungkin memerlukan mengalokasikan objek baru (objek Box baru) yang harus bertahan lama setelah fungsi keluar, sehingga tidak bisa berada di tumpukan. Tetapi Anda tidak tahu berapa banyak kotak yang Anda inginkan pada awal program, jadi itu tidak bisa statis.

Pengumpulan Sampah

Saya telah mendengar banyak akhir-akhir ini tentang betapa hebatnya Pengumpul Sampah, jadi mungkin sedikit suara yang menentang akan sangat membantu.

Pengumpulan Sampah adalah mekanisme luar biasa ketika kinerja bukanlah masalah besar. Saya mendengar GC semakin baik dan semakin canggih, tetapi kenyataannya, Anda mungkin dipaksa untuk menerima penalti kinerja (tergantung pada kasus penggunaan). Dan jika Anda malas, masih mungkin tidak berfungsi dengan baik. Pada saat terbaik, Pengumpul Sampah menyadari bahwa ingatan Anda hilang ketika menyadari bahwa tidak ada lagi referensi untuk itu (lihat penghitungan referensi). Tetapi, jika Anda memiliki objek yang merujuk pada dirinya sendiri (mungkin dengan merujuk ke objek lain yang merujuk kembali), maka penghitungan referensi saja tidak akan menunjukkan bahwa memori dapat dihapus. Dalam hal ini, GC perlu melihat seluruh sup referensi dan mencari tahu apakah ada pulau yang hanya dirujuk sendiri. Begitu saja, saya kira itu menjadi operasi O (n ^ 2), tetapi apa pun itu, itu bisa menjadi buruk jika Anda sama sekali peduli dengan kinerja. (Sunting: Martin B menunjukkan bahwa itu adalah O (n) untuk algoritma yang cukup efisien. Itu masih O (n) terlalu banyak jika Anda khawatir dengan kinerja dan dapat membatalkan alokasi dalam waktu yang konstan tanpa pengumpulan sampah.)

Secara pribadi, ketika saya mendengar orang mengatakan bahwa C ++ tidak memiliki pengumpulan sampah, pikiran saya menandai itu sebagai fitur C ++, tapi saya mungkin berada di minoritas. Mungkin hal yang paling sulit bagi orang untuk belajar tentang pemrograman dalam C dan C ++ adalah pointer dan bagaimana menangani alokasi memori dinamis mereka dengan benar. Beberapa bahasa lain, seperti Python, akan mengerikan tanpa GC, jadi saya pikir turun ke apa yang Anda inginkan dari bahasa. Jika Anda ingin kinerja yang dapat diandalkan, maka C ++ tanpa pengumpulan sampah adalah satu-satunya sisi Fortran yang dapat saya pikirkan. Jika Anda ingin kemudahan penggunaan dan roda pelatihan (untuk menyelamatkan Anda dari tabrakan tanpa mengharuskan Anda mempelajari manajemen memori yang "tepat"), pilih sesuatu dengan GC. Bahkan jika Anda tahu cara mengelola memori dengan baik, itu akan menghemat waktu Anda yang dapat Anda habiskan untuk mengoptimalkan kode lainnya. Sebenarnya tidak ada banyak penalti kinerja lagi, tetapi jika Anda benar-benar membutuhkan kinerja yang dapat diandalkan (dan kemampuan untuk mengetahui apa yang sebenarnya terjadi, kapan, di balik selimut) maka saya akan tetap menggunakan C ++. Ada alasan mengapa setiap mesin gim utama yang pernah saya dengar ada di C ++ (jika bukan C atau assembly). Python, dkk baik-baik saja untuk scripting, tetapi bukan mesin game utama.

pasar
sumber
Ini tidak benar-benar relevan dengan pertanyaan asli (atau banyak sama sekali, sebenarnya), tetapi Anda mendapatkan lokasi tumpukan dan menumpuk ke belakang. Biasanya , tumpukan tumbuh turun dan tumpukan tumbuh (meskipun tumpukan tidak benar-benar "tumbuh", jadi ini adalah penyederhanaan besar yang berlebihan) ...
P Daddy
Saya tidak berpikir bahwa pertanyaan ini mirip atau bahkan duplikat dari pertanyaan lain. yang satu ini khusus tentang C ++ dan apa yang dia maksud hampir pasti adalah tiga durasi penyimpanan yang ada di C ++. Anda dapat memiliki objek dinamis yang dialokasikan pada memori statis, misalnya, membebani op baru.
Johannes Schaub - litb
7
Perlakuan merendahkan Anda terhadap pengumpulan sampah sedikit kurang bermanfaat.
P Ayah 2
9
Seringkali pengumpulan sampah saat ini lebih baik daripada membebaskan memori manual karena itu terjadi ketika ada sedikit pekerjaan yang harus dilakukan, sebagai lawan membebaskan memori yang dapat terjadi tepat ketika kinerja dapat digunakan sebaliknya.
Georg Schölly
3
Hanya sebuah komentar kecil - pengumpulan sampah tidak memiliki kompleksitas O (n ^ 2) (yang tentu saja akan menjadi bencana bagi kinerja). Waktu yang diperlukan untuk satu siklus pengumpulan sampah sebanding dengan ukuran tumpukan - lihat hpl.hp.com/personal/Hans_Boehm/gc/complexity.html .
Martin B
54

Berikut ini tentu saja semua tidak terlalu tepat. Ambillah dengan sebutir garam ketika Anda membacanya :)

Nah, tiga hal yang Anda rujuk adalah durasi penyimpanan otomatis, statis, dan dinamis , yang berkaitan dengan berapa lama objek hidup dan kapan mereka mulai hidup.


Durasi penyimpanan otomatis

Anda menggunakan durasi penyimpanan otomatis untuk data yang berumur pendek dan kecil , yang hanya diperlukan secara lokal dalam beberapa blok:

if(some condition) {
    int a[3]; // array a has automatic storage duration
    fill_it(a);
    print_it(a);
}

Seumur hidup berakhir segera setelah kita keluar dari blok, dan itu dimulai segera setelah objek didefinisikan. Mereka adalah jenis paling lama durasi penyimpanan, dan jauh lebih cepat daripada dalam durasi penyimpanan dinamis tertentu.


Durasi penyimpanan statis

Anda menggunakan durasi penyimpanan statis untuk variabel gratis, yang dapat diakses oleh kode apa saja setiap saat, jika ruang lingkup mereka memungkinkan penggunaan tersebut (ruang lingkup namespace), dan untuk variabel lokal yang perlu memperpanjang masa hidup mereka di keluar dari ruang lingkup mereka (cakupan lokal), dan untuk variabel anggota yang perlu dibagikan oleh semua objek dari kelas mereka (kelas lingkup). Masa hidup mereka tergantung pada ruang lingkup mereka. Mereka dapat memiliki ruang nama dan ruang lingkup lokal dan ruang kelas . Apa yang benar tentang mereka berdua adalah, begitu kehidupan mereka dimulai, kehidupan berakhir pada akhir program . Berikut ini dua contoh:

// static storage duration. in global namespace scope
string globalA; 
int main() {
    foo();
    foo();
}

void foo() {
    // static storage duration. in local scope
    static string localA;
    localA += "ab"
    cout << localA;
}

Program mencetak ababab, karena localAtidak dihancurkan saat keluar dari bloknya. Anda dapat mengatakan bahwa objek yang memiliki cakupan lokal mulai seumur hidup ketika kontrol mencapai definisi mereka . Sebab localA, itu terjadi ketika fungsi tubuh dimasukkan. Untuk objek dalam lingkup namespace, seumur hidup dimulai saat startup program . Hal yang sama berlaku untuk objek statis dari ruang lingkup kelas:

class A {
    static string classScopeA;
};

string A::classScopeA;

A a, b; &a.classScopeA == &b.classScopeA == &A::classScopeA;

Seperti yang Anda lihat, classScopeAtidak terikat pada objek tertentu dari kelasnya, tetapi pada kelas itu sendiri. Alamat ketiga nama di atas adalah sama, dan semuanya menunjukkan objek yang sama. Ada aturan khusus tentang kapan dan bagaimana objek statis diinisialisasi, tetapi jangan khawatir tentang itu sekarang. Itu dimaksud dengan istilah kegagalan inisialisasi statis .


Durasi penyimpanan dinamis

Durasi penyimpanan terakhir adalah dinamis. Anda menggunakannya jika Anda ingin memiliki objek hidup di pulau lain, dan Anda ingin meletakkan pointer di sekitar referensi itu. Anda juga menggunakannya jika objek Anda besar , dan jika Anda ingin membuat array ukuran yang hanya diketahui saat runtime . Karena fleksibilitas ini, objek yang memiliki durasi penyimpanan dinamis rumit dan lambat untuk dikelola. Objek yang memiliki durasi dinamis itu mulai seumur hidup ketika doa operator baru yang sesuai terjadi:

int main() {
    // the object that s points to has dynamic storage 
    // duration
    string *s = new string;
    // pass a pointer pointing to the object around. 
    // the object itself isn't touched
    foo(s);
    delete s;
}

void foo(string *s) {
    cout << s->size();
}

Masa pakainya berakhir hanya ketika Anda memanggil hapus untuk mereka. Jika Anda lupa itu, benda-benda itu tidak pernah berakhir seumur hidup. Dan objek kelas yang mendefinisikan konstruktor yang dinyatakan pengguna tidak akan memiliki destruktor yang dipanggil. Objek yang memiliki durasi penyimpanan dinamis memerlukan penanganan manual seumur hidup dan sumber daya memori terkait. Perpustakaan ada untuk memudahkan penggunaannya. Pengumpulan sampah eksplisit untuk objek tertentu dapat dibuat dengan menggunakan pointer pintar:

int main() {
    shared_ptr<string> s(new string);
    foo(s);
}

void foo(shared_ptr<string> s) {
    cout << s->size();
}

Anda tidak perlu peduli dengan panggilan delete: ptr yang dibagikan melakukannya untuk Anda, jika pointer terakhir yang mereferensikan objek keluar dari cakupan. Ptr bersama itu sendiri memiliki durasi penyimpanan otomatis. Jadi masa hidupnya dikelola secara otomatis, memungkinkannya untuk memeriksa apakah ia harus menghapus objek dinamis yang diarahkan ke destruktornya. Untuk referensi shared_ptr, lihat mendongkrak dokumen: http://www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm

Johannes Schaub - litb
sumber
39

Itu sudah dikatakan rumit, sama seperti "jawaban singkat":

  • variabel statis (kelas)
    seumur hidup = runtime program (1)
    visibilitas = ditentukan oleh pengubah akses (pribadi / dilindungi / publik)

  • variabel statis (lingkup global)
    seumur hidup = runtime program (1)
    visibilitas = unit kompilasi yang dipakai dalam (2)

  • heap variable
    lifetime = ditentukan oleh Anda (baru dihapus)
    visibilitas = ditentukan oleh Anda (apa pun yang Anda tetapkan pointer)

  • tumpukan
    visibilitas variabel = dari deklarasi hingga ruang lingkup keluar
    seumur hidup = dari deklarasi hingga ruang lingkup yang menyatakan keluar


(1) lebih tepatnya: dari inisialisasi hingga deinialisasi dari unit kompilasi (yaitu file C / C ++). Urutan inisialisasi unit kompilasi tidak ditentukan oleh standar.

(2) Hati-hati: jika Anda instantiate variabel statis di header, setiap unit kompilasi mendapatkan salinannya sendiri.

Peterchen
sumber
5

Saya yakin salah satu pedant akan memberikan jawaban yang lebih baik segera, tetapi perbedaan utama adalah kecepatan dan ukuran.

Tumpukan

Secara dramatis lebih cepat untuk mengalokasikan. Hal ini dilakukan pada O (1) karena dialokasikan ketika mengatur frame stack sehingga pada dasarnya gratis. Kekurangannya adalah bahwa jika Anda kehabisan ruang tumpukan Anda bertulang. Anda dapat menyesuaikan ukuran tumpukan, tetapi IIRC yang Anda miliki ~ 2MB untuk dimainkan. Juga, segera setelah Anda keluar dari fungsi, semua yang ada di tumpukan dihapus. Jadi bisa bermasalah untuk merujuknya nanti. (Pointer untuk menumpuk objek yang dialokasikan mengarah ke bug.)

Tumpukan

Secara dramatis lebih lambat untuk mengalokasikan. Tapi Anda memiliki GB untuk dimainkan, dan arahkan ke.

Pengumpul sampah

Pengumpul sampah adalah beberapa kode yang berjalan di latar belakang dan membebaskan memori. Ketika Anda mengalokasikan memori pada heap, sangat mudah untuk melupakan untuk membebaskannya, yang dikenal sebagai kebocoran memori. Seiring waktu, memori yang dikonsumsi aplikasi Anda tumbuh dan tumbuh sampai crash. Memiliki pemulung secara berkala membebaskan memori yang tidak lagi Anda perlukan membantu menghilangkan kelas bug ini. Tentu saja ini ada harganya, karena pengumpul sampah memperlambat segalanya.

Chris Smith
sumber
3

Apa masalah statis dan tumpukan?

Masalah dengan alokasi "statis" adalah bahwa alokasi dibuat pada waktu kompilasi: Anda tidak dapat menggunakannya untuk mengalokasikan beberapa jumlah variabel data, jumlah yang tidak diketahui sampai waktu berjalan.

Masalah dengan mengalokasikan pada "tumpukan" adalah bahwa alokasi dihancurkan segera setelah subrutin yang mengembalikan alokasi.

Saya bisa menulis seluruh aplikasi tanpa mengalokasikan variabel di heap?

Mungkin tetapi bukan aplikasi non-sepele, normal, besar (tetapi yang disebut "embedded" program dapat ditulis tanpa heap, menggunakan subset dari C ++).

Apa yang dilakukan oleh pemulung?

Itu terus mengawasi data Anda ("tandai dan sapu") untuk mendeteksi kapan aplikasi Anda tidak lagi merujuknya. Ini nyaman untuk aplikasi, karena aplikasi tidak perlu mendelokasi data ... tetapi pengumpul sampah mungkin mahal secara komputasi.

Pengumpul sampah bukan fitur biasa dari pemrograman C ++.

Apa yang dapat Anda lakukan dengan memanipulasi memori sendiri yang tidak dapat Anda lakukan dengan menggunakan pengumpul sampah ini?

Pelajari mekanisme C ++ untuk alokasi memori deterministik:

  • 'statis': tidak pernah dibatalkan alokasi
  • 'stack': segera setelah variabel "out of scope"
  • 'heap': ketika pointer dihapus (dihapus secara eksplisit oleh aplikasi, atau secara implisit dihapus dalam beberapa atau beberapa subrutin lainnya)
ChrisW
sumber
1

Alokasi memori tumpukan (variabel fungsi, variabel lokal) bisa bermasalah ketika tumpukan Anda terlalu "dalam" dan Anda meluap memori yang tersedia untuk menumpuk alokasi. Heap adalah untuk objek yang perlu diakses dari banyak utas atau sepanjang siklus hidup program. Anda dapat menulis seluruh program tanpa menggunakan heap.

Anda dapat membocorkan memori dengan mudah tanpa pengumpul sampah, tetapi Anda juga dapat menentukan kapan objek dan memori dibebaskan. Saya telah mengalami masalah dengan Java ketika menjalankan GC dan saya memiliki proses waktu nyata, karena GC adalah utas eksklusif (tidak ada yang bisa dijalankan). Jadi, jika kinerja sangat penting dan Anda dapat menjamin tidak ada benda yang bocor, tidak menggunakan GC sangat membantu. Kalau tidak, itu hanya membuat Anda membenci kehidupan ketika aplikasi Anda menghabiskan memori dan Anda harus melacak sumber kebocoran.

Rob Elsner
sumber
1

Bagaimana jika program Anda tidak tahu di muka berapa banyak memori yang dialokasikan (maka Anda tidak dapat menggunakan variabel tumpukan). Katakanlah daftar yang ditautkan, daftar dapat tumbuh tanpa mengetahui di muka berapa ukurannya. Jadi mengalokasikan pada heap masuk akal untuk daftar tertaut ketika Anda tidak menyadari berapa banyak elemen yang akan dimasukkan ke dalamnya.

kal
sumber
0

Keuntungan dari GC dalam beberapa situasi adalah gangguan pada yang lain; ketergantungan pada GC mendorong untuk tidak terlalu memikirkannya. Secara teori, tunggu hingga periode 'idle' atau sampai benar-benar harus, ketika itu akan mencuri bandwidth dan menyebabkan latensi respons di aplikasi Anda.

Tetapi Anda tidak harus 'tidak memikirkannya.' Seperti halnya semua hal lain di aplikasi multithreaded, saat Anda bisa menghasilkan, Anda bisa menghasilkan. Jadi misalnya, di .Net, dimungkinkan untuk meminta GC; dengan melakukan ini, alih-alih mengurangi frekuensi menjalankan GC, Anda dapat memiliki frekuensi menjalankan GC yang lebih pendek, dan menyebarkan latensi yang terkait dengan overhead ini.

Tapi ini mengalahkan daya tarik utama dari GC yang tampaknya "didorong untuk tidak perlu terlalu memikirkannya karena itu auto-mat-ic."

Jika Anda pertama kali terkena pemrograman sebelum GC menjadi lazim dan merasa nyaman dengan malloc / free dan baru / delete, maka bahkan mungkin Anda merasa GC sedikit mengganggu dan / atau tidak dapat dipercaya (karena orang mungkin tidak percaya pada ' optimasi, 'yang memiliki riwayat kotak-kotak.) Banyak aplikasi yang mentoleransi latensi acak. Tetapi untuk aplikasi yang tidak, di mana latensi acak kurang dapat diterima, reaksi umum adalah untuk menghindari lingkungan GC dan bergerak ke arah kode murni yang tidak dikelola (atau dilarang, seni lama sekarat, bahasa assembly.)

Saya memiliki seorang siswa musim panas di sini beberapa waktu lalu, seorang anak magang, anak yang cerdas, yang disapih dengan GC; dia sangat kesal tentang superioritas GC sehingga bahkan ketika pemrograman dalam C / C ++ yang tidak dikelola dia menolak untuk mengikuti malloc / free new / delete model karena, kutipan, "Anda tidak harus melakukan ini dalam bahasa pemrograman modern." Dan kamu tahu? Untuk aplikasi kecil dan berjalan singkat, Anda memang bisa lolos dari itu, tetapi tidak untuk aplikasi yang berjalan lama.

frediano
sumber
0

Stack adalah memori yang dialokasikan oleh kompiler, kapan pun kami mengkompilasi program, secara default kompiler mengalokasikan sebagian memori dari OS (kami dapat mengubah pengaturan dari pengaturan kompiler di IDE Anda) dan OS adalah salah satu yang memberi Anda memori, itu tergantung pada banyak memori yang tersedia pada sistem dan banyak hal lainnya, dan datang untuk menumpuk memori dialokasikan ketika kita mendeklarasikan variabel yang mereka salin (ref sebagai formal) variabel-variabel tersebut didorong untuk menumpuk mereka mengikuti beberapa konvensi penamaan secara default CDECL-nya di Visual studio mis: notasi infiks: c = a + b; tumpukan mendorong dilakukan PUSHING kanan ke kiri, b untuk menumpuk, operator, untuk menumpuk dan hasil dari i, ec to stack. Dalam notasi pre fix: = + cab Di sini semua variabel didorong untuk menumpuk 1 (kanan ke kiri) dan kemudian operasi dibuat. Memori ini dialokasikan oleh kompiler diperbaiki. Jadi mari kita asumsikan 1MB memori dialokasikan untuk aplikasi kita, katakanlah variabel menggunakan 700kb memori (semua variabel lokal didorong untuk menumpuk kecuali mereka dialokasikan secara dinamis) sehingga sisa memori 324kb dialokasikan untuk tumpukan. Dan tumpukan ini memiliki lebih sedikit waktu hidup, ketika ruang lingkup fungsi berakhir tumpukan ini akan dihapus.

raj
sumber