Menu

Stack vs Heap di C: Masa Hidup, Dangling Pointer, dan Kapan Memakai Masing-masing

Di mana datamu sebenarnya tinggal: penyimpanan otomatis di stack, penyimpanan dinamis di heap, dan penyimpanan statis sebagai wilayah ketiga. Lengkap dengan bug dangling pointer klasik akibat mengembalikan variabel lokal, dan aturan untuk memilih.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Setiap variabel dalam program C tinggal di suatu tempat, dan tempat itu menentukan dua hal yang tidak bisa kamu ubah setelahnya: berapa lama variabel itu bertahan, dan seberapa banyak yang bisa kamu punya. C memberimu tiga wilayah penyimpanan, dan salah memilih berujung pada crash atau kebocoran memori. Halaman ini menjelaskan ketiganya dan menunjukkan bug klasik yang muncul ketika masa hidup data salah dipahami.

Tiga Wilayah

  high addresses
  +---------------------------+
  |  stack                    |  locals, parameters, return addresses
  |    grows downward  |      |  freed automatically on return
  |                    v      |
  +---------------------------+
  |         (unused gap)      |
  +---------------------------+
  |                    ^      |
  |    grows upward    |      |
  |  heap                     |  malloc / calloc / realloc blocks
  +---------------------------+  freed only by free()
  |  static / global data     |  globals and statics, whole run
  +---------------------------+
  |  code (text)              |  the machine code, read-only
  +---------------------------+
  low addresses
  • Penyimpanan otomatis (stack) menampung parameter fungsi dan variabel lokal non-static. Satu blok stack diambil saat fungsi dimasuki dan dilepas saat fungsi kembali. Ukurannya sudah tetap saat kompilasi.
  • Penyimpanan dinamis (heap) menampung semua hasil malloc, calloc, dan realloc. Ukurannya ditentukan saat program berjalan, dan masa hidupnya baru berakhir saat free.
  • Penyimpanan statis menampung variabel global dan apa pun yang dideklarasikan static. Wilayah ini ada selama program berjalan dan diisi nol sebelum main dimulai.

Alamat pada diagram adalah susunan yang umum, bukan jaminan. Standar C menjelaskan masa hidup, bukan tata letak memori.

Penyimpanan Otomatis dalam Praktik

Setiap pemanggilan demo mendapat local dan table yang baru. Tidak ada yang dibebaskan secara manual, tidak ada yang bisa bocor, dan alokasinya hanya butuh satu instruksi yang menggeser stack pointer. Karena itulah variabel lokal biasa sebaiknya jadi pilihan default: inilah penyimpanan tercepat dan teraman yang dimiliki C.

Masalahnya ada di kurung kurawal penutup. Begitu baris itu dijalankan, memori tersebut hilang.

Dangling Pointer

Ini bug yang pasti pernah ditulis setiap programmer C:

/* BROKEN: returns the address of memory that no longer exists */
int *make_counter(void) {
    int count = 0;
    return &count;          /* count dies at this brace */
}

int main(void) {
    int *p = make_counter();
    *p = 5;                 /* writing into a dead stack frame */
    return 0;
}

&count adalah alamat yang sah selama make_counter berjalan. Saat fungsi kembali, ruang stack itu diserahkan ke fungsi apa pun yang dipanggil berikutnya, sehingga p kini menunjuk ke variabel lokal milik fungsi lain. Membacanya menghasilkan nilai sampah, menulisnya merusak data tersebut. GCC dan Clang memberi peringatan untuk pola persis seperti ini (-Wreturn-local-addr), jadi selalu compile dengan peringatan aktif.

Bug yang sama bisa menyamar lewat array, dan di situ peringatannya sering tidak muncul:

Versi rusak dari fungsi itu akan menyusun teksnya di variabel lokal char buf[64] lalu menjalankan return buf;, yaitu mengembalikan alamat buffer yang lenyap pada saat yang sama.

Tiga Cara Memperbaikinya

1. Pemanggil menyediakan buffer (seperti contoh di atas). Tidak ada alokasi, tidak ada pertanyaan soal kepemilikan, dan ini gaya yang paling umum di library C. Fungsinya menerima ukuran buffer supaya tidak menulis melewati batasnya.

2. Kembalikan memori heap, dan jelaskan siapa yang membebaskannya.

Blok heap memang dirancang untuk hidup lebih lama daripada fungsinya, dan itulah inti dari memori dinamis. Harganya adalah komentar soal kepemilikan dan free yang harus dipanggil oleh pemanggil.

3. Pakai penyimpanan statis, kalau satu buffer bersama sudah cukup:

static di dalam fungsi membuat scope variabel tetap lokal tetapi memberinya masa hidup sepanjang program, jadi mengembalikan alamatnya sah. Konsekuensinya, variabel itu hanya ada satu: semua pemanggil memakainya bersama. Pola ini tidak bisa dipakai di kode multithread, dan tetap bisa mengejutkan di kode single-thread ketika dua pemanggil memegang pointer yang sama pada waktu bersamaan.

Ukuran: Saat Stack Habis

Ruang stack kecil dan tetap. Thread utama biasanya mendapat 1 MB di Windows dan 8 MB di Linux, sedangkan thread yang dibuat belakangan sering mendapat jauh lebih sedikit. Heap dibatasi oleh memori yang tersedia di sistem.

void bad(void) {
    int huge[1000000];      /* ~4 MB of stack - likely crashes on entry */
    huge[0] = 1;
}

Tidak ada pesan diagnostik dan tidak ada NULL yang bisa dicek: program langsung mati, biasanya dengan segmentation fault, sebelum baris pertama di badan fungsi dijalankan. Versi heap melaporkan kegagalan dengan benar:

Rekursi yang terlalu dalam juga menghabiskan stack dengan cara yang sama, satu frame demi satu frame. Fungsi rekursif yang tidak berhenti adalah penyebab stack overflow yang paling sering ditemui dalam praktik.

Biaya dan Lokalitas

Alokasi di stack hanyalah satu operasi aritmetika pada sebuah register. Alokasi di heap adalah pemanggilan library yang mencari blok yang cocok, mungkin mengambil lock, dan sesekali meminta memori tambahan dari sistem operasi. Di dalam perulangan yang sering dijalankan, perbedaan itu bisa terukur.

Data di stack juga rapat dan baru saja disentuh, jadi biasanya sudah ada di cache. Blok heap bisa tersebar. Tidak satu pun dari fakta ini boleh menentukan desain sendirian, karena kebenaran masa hidup data harus didahulukan. Tetapi dari dua desain yang sama-sama benar, desain berbasis stack biasanya lebih cepat.

Melihat Wilayah-wilayahnya

Mencetak alamat membuat tata letak ini lebih konkret. Nilai pastinya berbeda setiap kali dijalankan (sistem modern mengacaknya), tetapi pengelompokannya terlihat:

Variabel global dan static letaknya berdekatan, blok heap ada di tempat lain, dan variabel lokal biasanya jauh dari keduanya. Lakukan cast ke void * untuk %p, karena itulah yang diminta format specifier tersebut.

Memilih

Pakai stack jika:

  • ukurannya sudah diketahui saat kompilasi,
  • datanya hanya dibutuhkan di dalam fungsi ini dan fungsi yang dipanggilnya,
  • dan ukurannya kecil, beberapa kilobyte, bukan megabyte.

Pakai heap jika:

  • ukurannya bergantung pada input, file, atau hasil perhitungan,
  • datanya harus hidup lebih lama daripada fungsi yang membuatnya,
  • atau ukurannya cukup besar sampai mengancam batas stack.

Pakai static jika:

  • harus ada tepat satu instance selama program berjalan,
  • dan memakainya bersama oleh semua pemanggil memang benar.

Pilihan default adalah stack. Gunakan heap jika salah satu dari tiga alasannya berlaku, dan saat kamu melakukannya, ikuti aturan kepemilikan di halaman memory leak supaya blok yang kamu beri masa hidup panjang tetap dibebaskan.

Dua Bug yang Saling Mencerminkan

Keduanya layak disebut bersama, karena keduanya adalah pertanyaan masa hidup yang sama dengan dua jawaban berbeda:

  • Dangling pointer: memorinya mati lebih dulu daripada pointernya. Contohnya mengembalikan &local, atau memakai pointer setelah free. Program membaca atau menulis penyimpanan yang kini milik pihak lain.
  • Memory leak: pointernya mati lebih dulu daripada memorinya. Contohnya kehilangan referensi terakhir ke blok malloc. Tidak ada yang langsung rusak, prosesnya hanya terus membesar.

Keduanya berasal dari ketidakcocokan antara berapa lama data harus hidup dan wilayah tempat kamu menaruhnya. Tentukan masa hidupnya dulu, wilayahnya akan mengikuti.

Pertanyaan yang sering diajukan

Apa perbedaan stack dan heap di C?

Stack menyimpan variabel lokal: ukurannya ditentukan compiler, variabel dibuat saat fungsi dimasuki dan dihancurkan saat fungsi kembali, dan alokasinya tidak memakan biaya. Heap menyimpan blok dari malloc: kamu memilih ukurannya saat program berjalan, blok itu bertahan sampai kamu memanggil free, dan alokasinya punya biaya nyata.

Kenapa saya tidak boleh mengembalikan pointer ke variabel lokal di C?

Karena penyimpanan variabel lokal dilepas begitu fungsi kembali. Pointer itu masih memegang alamat tersebut, tetapi memorinya kini milik pemanggilan fungsi berikutnya: membacanya menghasilkan nilai sampah, menulisnya merusak data lain yang tidak berhubungan. Itulah dangling pointer. Kembalikan blok dari malloc, atau biarkan pemanggil menyediakan buffer.

Seberapa besar stack di C?

Biasanya 1 sampai 8 MB untuk thread utama, dan jauh lebih kecil untuk thread tambahan. Cukup kecil sehingga int big[1000000]; sebagai variabel lokal biasanya membuat program crash begitu fungsi dimasuki. Heap dibatasi oleh memori sistem yang tersedia, jadi data besar atau yang ukurannya belum diketahui sebaiknya ditaruh di sana.

Kapan saya sebaiknya memakai heap daripada stack di C?

Ada tiga kasus: ukurannya baru diketahui saat program berjalan, datanya harus hidup lebih lama daripada fungsi yang membuatnya, atau bloknya terlalu besar untuk stack (kira-kira apa pun di atas beberapa ratus kilobyte). Selain itu, pakai variabel lokal biasa: lebih cepat dan tidak mungkin bocor.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai