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):

c
void free(void *ptr);

Kontraknya:

  • ptr harus menunjuk blok yang sebelumnya dialokasikan oleh malloc, calloc, atau realloc, atau ptr boleh NULL.
  • 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 free pada pointer yang bukan dari alokator (misalnya pointer ke variabel lokal atau ke tengah blok) adalah undefined behavior.
  • Memanggil free dua 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.

c
#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:

text
Sebelum free: p = 0x55fc6963a2a0, *p = 11
Setelah p=NULL: p = (nil)
p NULL: tidak aman dereference, lewati
free(NULL) berhasil tanpa efek samping

Setelah 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:

  1. Baris 8 (int *p = malloc(4 * sizeof(int));): alokasi 4 elemen int. Bila gagal, p adalah NULL dan program keluar dengan kode error.
  2. Baris 15 (uintptr_t alamat_lama = (uintptr_t)p;): menyimpan alamat sebagai nilai integer. Setelah free, mengakses p sebagai pointer untuk dicetak bisa memicu peringatan -Wuse-after-free di GCC modern—uintptr_t menghindari hal itu.
  3. Baris 22 (free(p);): mengembalikan blok ke allocator. Setelah baris ini, blok memori di alamat alamat_lama sudah tidak lagi dimiliki program.
  4. Baris 25 (p = NULL;): menimpa nilai p dengan NULL. Tanpa baris ini, p tetap menyimpan alamat lama—dangling pointer—yang berbahaya bila tanpa sengaja di-dereference.
  5. Baris 28 (if (p == NULL)): pola aman—selalu cek NULL sebelum dereference. Bila p tidak sengaja tidak di-set NULL setelah free, pengecekan ini akan false dan program masuk ke cabang yang melakukan *p—use-after-free.
  6. Baris 35 (free(p);): karena p sekarang NULL, free(NULL) adalah no-op yang aman. Tidak ada double free walaupun free dipanggil 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.

c
#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:

text
Total: 15, rata-rata: 3.00
data sekarang NULL, aman untuk pengecekan

Fungsi 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.