C tidak memiliki sistem ownership bawaan seperti Rust, tetapi konsep ini tetap berlaku secara konvensi. Setiap blok heap yang dialokasikan oleh malloc/calloc/realloc harus dibebaskan tepat satu kali oleh pemilik yang jelas. Tanpa konvensi ini, kita mudah terjebak dalam dua bug: double free (dua fungsi mengira dirinya pemilik) atau memory leak (tidak ada fungsi yang mengira dirinya pemilik). Pada chapter ini kita akan memetakan pola ownership yang umum di C—single owner, transfer ownership, dan borrowing—dan melihat bagaimana konvensi ini menyelamatkan kode dari bug memori.

Tiga Pola Ownership di C #

Konvensi yang umum dipakai di pustaka C:

  • Single owner — satu fungsi (biasanya yang memanggil malloc) adalah pemilik tunggal dan satu-satunya yang boleh memanggil free. Fungsi lain menerima pointer sebagai “peminjam” (borrowing).
  • Transfer ownership — fungsi mengalokasikan blok lalu mengembalikan pointer; pemanggil menjadi pemilik baru dan wajib memanggil free. Pola ini umum pada “factory function”.
  • Container ownership — sebuah struct (misalnya Vector) memegang pointer; fungsi vector_destroy(&v) adalah satu-satunya yang boleh memanggil free pada pointer tersebut.

Mekanisme kerja di balik layar: standar C tidak melacak siapa pemilik sebuah blok. Allocator glibc hanya tahu alamat dan ukuran blok. Konsep ownership sepenuhnya bersifat dokumentasi dan konvensi antar programmer—compiler tidak bisa memverifikasinya (kecuali dengan tool eksternal seperti Valgrind atau Clang Static Analyzer). Inilah sebabnya konvensi yang konsisten sangat penting: komentar, penamaan fungsi, dan urutan deklarasi harus jelas siapa pemilik.

Pola Single Owner #

Fungsi buat_buffer mengalokasikan dan mengembalikan pointer; main adalah pemilik dan satu-satunya yang memanggil free. Fungsi pakai_buffer hanya meminjam—bukan pemilik.

c
#include <stdio.h>
#include <stdlib.h>

/* Pemilik tunggal: fungsi yang mengalokasikan bertanggung jawab membebaskan */
int *buat_buffer(size_t n) {
    int *buf = malloc(n * sizeof(int));
    if (buf == NULL) return NULL;
    for (size_t i = 0; i < n; i++) buf[i] = (int)i * 5;
    return buf;
}

void pakai_buffer(const int *buf, size_t n) {
    /* PEMBERITAHUAN: buf hanya dipakai, TIDAK boleh di-free */
    if (buf == NULL) return;
    printf("Isi buffer: ");
    for (size_t i = 0; i < n; i++) printf("%d ", buf[i]);
    printf("\n");
}

int main(void) {
    int *b = buat_buffer(4);
    if (b == NULL) return 1;

    pakai_buffer(b, 4);

    /* main adalah pemilik -> main yang memanggil free */
    free(b);
    b = NULL;
    printf("Buffer dibebaskan, b = %p\n", (void*)b);
    return 0;
}

Output yang Dihasilkan:

text
Isi buffer: 0 5 10 15 
Buffer dibebaskan, b = (nil)

Perhatikan beberapa isyarat kontrak ownership dalam kode:

  1. buat_buffer mengalokasikan malloc dan mengembalikan pointer—ini adalah factory function. Pemanggil (main) menjadi pemilik baru.
  2. pakai_buffer menerima const int *buf—keyword const menandakan bahwa fungsi ini tidak akan memodifikasi data, dan secara konvensi tidak akan memanggil free.
  3. main adalah satu-satunya yang memanggil free(b).

Mari kita telaah baris-baris kode di atas:

  1. Baris 5 (int *buat_buffer(size_t n)): nama fungsi buat_* mengikuti konvensi “factory”—fungsi yang mengembalikan sumber daya baru yang harus dikelola pemanggil.
  2. Baris 12 (void pakai_buffer(const int *buf, size_t n)): parameter const int * menyatakan “pointer ke data konstan”—peminjam tidak boleh menulis melalui pointer ini, apalagi memanggil free. Ini adalah kontrak yang ditegakkan compiler.
  3. Baris 22 (int *b = buat_buffer(4);): main menerima ownership. Setelah baris ini, main bertanggung jawab atas blok yang ditunjuk b.
  4. Baris 26 (pakai_buffer(b, 4);): main meminjamkan b ke pakai_buffer. Fungsi itu tidak mengubah kepemilikan—setelah fungsi kembali, main masih pemilik.
  5. Baris 29 (free(b);): main mengembalikan blok ke allocator. Inilah satu-satunya free untuk blok ini.
  6. Baris 30 (b = NULL;): menghapus dangling pointer—praktik wajib setelah free.

Pola Transfer Ownership #

Kadang fungsi mengembalikan pointer ke blok yang dialokasikan, dan pemanggil menjadi pemilik baru. Komentar eksplisit sangat membantu di sini—pemanggil tidak punya cara lain untuk tahu bahwa ia bertanggung jawab memanggil free.

c
#include <stdio.h>
#include <stdlib.h>

/* Transfer ownership: pemanggil menjadi pemilik baru dan wajib free */
int *ambil_kepemilikan(void) {
    int *data = malloc(3 * sizeof(int));
    if (data == NULL) return NULL;
    data[0] = 100; data[1] = 200; data[2] = 300;
    /* Setelah return, pemanggil bertanggung jawab membebaskan data */
    return data;
}

void simpan_ke_log(int *data, size_t n) {
    /* Borrowing: hanya memakai, tidak free */
    printf("Log: ");
    for (size_t i = 0; i < n; i++) printf("%d ", data[i]);
    printf("\n");
}

int main(void) {
    /* main menerima kepemilikan dari ambil_kepemilikan */
    int *mine = ambil_kepemilikan();
    if (mine == NULL) return 1;

    simpan_ke_log(mine, 3);   /* meminjam tanpa transfer */

    free(mine);
    mine = NULL;
    printf("mine dibebaskan\n");
    return 0;
}

Output yang Dihasilkan:

text
Log: 100 200 300 
mine dibebaskan

Kontrak ownership ambil_kepemilikan harus didokumentasikan dengan komentar eksplisit: “pemanggil menjadi pemilik baru dan wajib free”. Tanpa dokumentasi ini, pemanggil mungkin mengira fungsi tersebut memelihara cache internal—dan tidak pernah memanggil free, menyebabkan memory leak.

Beberapa pustaka memakai suffix nama fungsi sebagai penanda ownership: strdup (mengalokasikan dan mengembalikan—pemanggil wajib free), asprintf (sama), getline (sama). Pola penamaan konsisten ini membantu programmer mengenali kontrak tanpa membaca dokumentasi panjang.