Setiap kali kita memanggil malloc, calloc, atau realloc, kita berutang satu panggilan free untuk mengembalikan blok tersebut ke allocator. Lupa memanggil free menyebabkan memory leak; memanggil free dua kali pada blok yang sama menyebabkan double free; memanggil free lalu mengakses blok lama menyebabkan use-after-free. Ketiganya adalah undefined behavior dan akar dari banyak bug keamanan. Pada chapter ini kita akan mempelajari kontrak free, pola aman “free lalu set NULL”, dan jebakan klasik yang sering dialami pemula. Kita juga akan melihat bahwa free(NULL) adalah no-op yang aman—pola yang membuat pengecekan jauh lebih sederhana.
Kontrak Fungsi free #
Fungsi free dideklarasikan di <stdlib.h> (ISO/IEC 9899 §7.22.3.3):
void free(void *ptr);Kontraknya:
ptrharus menunjuk blok yang sebelumnya dialokasikan olehmalloc,calloc, ataurealloc, atauptrbolehNULL.free(NULL)adalah no-op — tidak melakukan apa-apa dan tidak menyebabkan error. Ini dijamin standar.- Setelah
free, pointer yang dilewatkan tidak boleh dipakai lagi — blok sudah dikembalikan ke allocator dan isinya tak terdefinisi. - Memanggil
freepada pointer yang bukan dari alokator (misalnya pointer ke variabel lokal atau ke tengah blok) adalah undefined behavior. - Memanggil
freedua kali pada blok yang sama adalah undefined behavior (double free).
Mekanisme kerja: free mengembalikan blok ke free list allocator. Untuk alokasi besar (biasanya > 128 KB pada glibc), allocator memanggil munmap untuk benar-benar mengembalikan halaman ke kernel. Untuk alokasi kecil, blok ditandai sebagai “bebas” di metadata internal dan akan dipakai ulang pada malloc berikutnya—tidak benar-benar dikembalikan ke OS. Inilah sebabnya nilai RSS (Resident Set Size) program tidak turun setelah free untuk blok kecil.
Pola Aman: free, lalu Set NULL #
Pola paling penting yang harus selalu kita ikuti: setelah memanggil free(p), segera set p = NULL. Ini mencegah dangling pointer—pointer yang masih menyimpan alamat lama, padahal blok sudah dibebaskan.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main(void) {
int *p = malloc(4 * sizeof(int));
if (p == NULL) {
fprintf(stderr, "malloc gagal\n");
return 1;
}
for (int i = 0; i < 4; i++) {
p[i] = (i + 1) * 11;
}
/* Simpan alamat lama sebagai angka sebelum free */
uintptr_t alamat_lama = (uintptr_t)p;
printf("Sebelum free: p = 0x%lx, *p = %d\n",
(unsigned long)alamat_lama, *p);
free(p);
/* p masih menyimpan alamat lama -- dangling pointer */
p = NULL;
printf("Setelah p=NULL: p = %p\n", (void*)p);
/* Cek NULL sebelum dereference adalah pola aman */
if (p == NULL) {
printf("p NULL: tidak aman dereference, lewati\n");
} else {
printf("p tidak NULL: *p = %d\n", *p);
}
/* free(NULL) diperbolehkan dan no-op */
free(p);
printf("free(NULL) berhasil tanpa efek samping\n");
return 0;
}Output yang Dihasilkan:
Sebelum free: p = 0x55fc6963a2a0, *p = 11
Setelah p=NULL: p = (nil)
p NULL: tidak aman dereference, lewati
free(NULL) berhasil tanpa efek sampingSetelah free(p), variabel p masih menyimpan alamat lama—inilah dangling pointer. Penyetelan p = NULL menghapus alamat lama, sehingga pengecekan if (p == NULL) menjadi true dan kita bisa menghindari dereference yang berbahaya. Karena free(NULL) aman, memanggil free dua kali pada pointer yang sudah di-NULL-kan tidak menyebabkan double free—trik ini menyelamatkan banyak kode dari bug tak terduga.
Mari kita telaah baris-baris kode di atas:
- Baris 8 (
int *p = malloc(4 * sizeof(int));): alokasi 4 elemenint. Bila gagal,padalahNULLdan program keluar dengan kode error. - Baris 15 (
uintptr_t alamat_lama = (uintptr_t)p;): menyimpan alamat sebagai nilai integer. Setelahfree, mengaksespsebagai pointer untuk dicetak bisa memicu peringatan-Wuse-after-freedi GCC modern—uintptr_tmenghindari hal itu. - Baris 22 (
free(p);): mengembalikan blok ke allocator. Setelah baris ini, blok memori di alamatalamat_lamasudah tidak lagi dimiliki program. - Baris 25 (
p = NULL;): menimpa nilaipdenganNULL. Tanpa baris ini,ptetap menyimpan alamat lama—dangling pointer—yang berbahaya bila tanpa sengaja di-dereference. - Baris 28 (
if (p == NULL)): pola aman—selalu cekNULLsebelum dereference. Bilaptidak sengaja tidak di-setNULLsetelahfree, pengecekan ini akanfalsedan program masuk ke cabang yang melakukan*p—use-after-free. - Baris 35 (
free(p);): karenapsekarangNULL,free(NULL)adalah no-op yang aman. Tidak ada double free walaupunfreedipanggil dua kali.
Pola Factory: Alokasi, Pakai, Bebaskan #
Pola umum dalam program nyata: satu fungsi “pabrik” (buat_data) mengalokasikan dan menginisialisasi, fungsi lain (proses_data) menggunakan, dan main membebaskan. Pemilik tunggal memudahkan pelacakan siapa yang bertanggung jawab memanggil free.
#include <stdio.h>
#include <stdlib.h>
void proses_data(int *data, size_t n) {
if (data == NULL) return;
long total = 0;
for (size_t i = 0; i < n; i++) total += data[i];
printf("Total: %ld, rata-rata: %.2f\n", total, (double)total / n);
}
int *buat_data(size_t n) {
int *arr = malloc(n * sizeof(int));
if (arr == NULL) return NULL;
for (size_t i = 0; i < n; i++) arr[i] = (int)i + 1;
return arr;
}
int main(void) {
size_t n = 5;
int *data = buat_data(n);
if (data == NULL) {
fprintf(stderr, "Alokasi gagal\n");
return 1;
}
proses_data(data, n);
free(data);
data = NULL; /* hapus dangling pointer */
if (data == NULL) {
printf("data sekarang NULL, aman untuk pengecekan\n");
}
return 0;
}Output yang Dihasilkan:
Total: 15, rata-rata: 3.00
data sekarang NULL, aman untuk pengecekanFungsi buat_data adalah “factory”—bertanggung jawab mengalokasikan dan mengisi. Fungsi proses_data hanya memakai data dan tidak boleh memanggil free karena ia bukan pemilik. Fungsi main adalah pemilik dan satu-satunya yang memanggil free. Pola ini akan kita perdalam di chapter Ownership dan Lifetime.