Saat sebuah fungsi mengalokasikan banyak resource—file pointer, buffer memori, socket, lock—dan kemudian gagal di tengah jalan, kita harus membebaskan resource yang sudah dialokasikan sebelum keluar. Tanpa pola yang konsisten, kita mudah lupa: satu cabang error lupa fclose, cabang lain lupa free, dan program akhirnya mengalami resource leak yang sulit dilacak. Pada chapter ini kita akan mempelajari pola goto cleanup—salah satu pemakaian goto yang dianggap sah dan direkomendasikan oleh Linux kernel coding style serta buku-buku referensi C klasik.
Masalah: Banyak Resource, Banyak Cabang Error #
Bayangkan sebuah fungsi yang mengalokasikan tiga resource berturut-turut: buffer memori, array data, dan file handle. Setiap alokasi bisa gagal. Bila kita menulis cleanup di setiap cabang error tanpa pola, kita harus mengulang free/fclose berkali-kali:
#include <stdio.h>
#include <stdlib.h>
/* Versi BURUK: tanpa goto, duplikasi cleanup di setiap cabang error.
Mudah salah - lupa free salah satu resource. */
typedef struct {
char *nama;
int *data;
} context2_t;
int proses_buruk(const char *nama_file, context2_t *out) {
char *buf = malloc(64);
if (buf == NULL) {
return 1;
}
int *data = malloc(sizeof(int) * 10);
if (data == NULL) {
free(buf); /* cleanup buf */
return 2;
}
FILE *fp = fopen(nama_file, "r");
if (fp == NULL) {
free(buf); /* cleanup buf */
free(data); /* cleanup data */
return 3;
}
if (fgets(buf, 64, fp) == NULL) {
free(buf); /* cleanup buf */
free(data); /* cleanup data */
fclose(fp); /* cleanup fp */
return 4;
}
/* Sukses */
out->nama = buf;
out->data = data;
fclose(fp);
return 0;
}
int main(void) {
context2_t ctx = {0};
int rc = proses_buruk("/tmp/tidak_ada_cleanup2", &ctx);
printf("proses_buruk -> rc=%d (cleanup duplikatif di setiap cabang)\n", rc);
if (rc == 0) {
free(ctx.nama);
free(ctx.data);
}
return 0;
}Output yang Dihasilkan:
proses_buruk -> rc=3 (cleanup duplikatif di setiap cabang)Versi ini bekerja secara fungsional, tetapi maintenance-nya merepotkan. Setiap kali kita menambah resource baru, kita harus memperbarui setiap cabang error. Bayangkan bila ada 10 resource dan 8 titik kegagalan—kita akan punya 8 blok free/fclose berbeda, masing-masing dengan urutan yang harus dijaga konsisten. Kesalahan kecil seperti lupa fclose di satu cabang akan menyebabkan file descriptor leak yang baru terdeteksi setelah ribuan iterasi.
Pola goto cleanup: Satu Label, Satu Jalur #
Pola yang lebih baik adalah menggunakan goto ke satu label cleanup: di akhir fungsi. Setiap cabang error cukup goto cleanup tanpa perlu mengulang pemanggilan free. Label cleanup memusatkan seluruh pembebasan resource di satu tempat.
#include <stdio.h>
#include <stdlib.h>
/* Pola cleanup dengan goto: satu label cleanup, satu jalur keluar.
Setiap resource di-Free bila tidak NULL. goto di sini SAH dan
direkomendasikan oleh Linux kernel coding style. */
typedef struct {
char *nama;
int *data;
FILE *log;
} context_t;
int proses(const char *nama_file, context_t *out) {
int rc = 0; /* 0 = sukses, non-zero = kode error */
char *buf = NULL;
int *data = NULL;
FILE *fp = NULL;
buf = malloc(64);
if (buf == NULL) { rc = 1; goto cleanup; }
data = malloc(sizeof(int) * 10);
if (data == NULL) { rc = 2; goto cleanup; }
fp = fopen(nama_file, "r");
if (fp == NULL) { rc = 3; goto cleanup; }
/* Simulasi baca - kalau file kosong, anggap gagal */
if (fgets(buf, 64, fp) == NULL) { rc = 4; goto cleanup; }
/* Sukses: pindahkan ke output */
out->nama = buf;
out->data = data;
out->log = fp;
return 0; /* TIDAK lewat cleanup karena resource dipindahkan */
cleanup:
/* Setiap resource di-free dengan pengecekan NULL terlebih dahulu.
urutan: yang dialokasikan TERAKHIR di-free DULU (LIFO). */
if (fp) fclose(fp);
if (data) free(data);
if (buf) free(buf);
return rc;
}
int main(void) {
context_t ctx = {0};
/* Coba dengan file tidak ada */
int rc = proses("/tmp/file_cleanup_tidak_ada", &ctx);
printf("proses(file_tidak_ada) -> rc=%d\n", rc);
/* Coba dengan file kosong */
FILE *kosong = fopen("/tmp/file_cleanup_kosong", "w");
if (kosong) { fclose(kosong); } /* buat file kosong */
rc = proses("/tmp/file_cleanup_kosong", &ctx);
printf("proses(file_kosong) -> rc=%d\n", rc);
/* Coba dengan file berisi */
FILE *ada = fopen("/tmp/file_cleanup_berisi", "w");
if (ada) { fprintf(ada, "Halo, ini isi file\n"); fclose(ada); }
rc = proses("/tmp/file_cleanup_berisi", &ctx);
printf("proses(file_berisi) -> rc=%d\n", rc);
if (rc == 0) {
printf(" nama='%s'\n", ctx.nama);
free(ctx.nama);
free(ctx.data);
fclose(ctx.log);
}
return 0;
}Output yang Dihasilkan:
proses(file_tidak_ada) -> rc=3
proses(file_kosong) -> rc=4
proses(file_berisi) -> rc=0
nama='Halo, ini isi file
'Tiga skenario uji menghasilkan tiga kode berbeda (3, 4, 0). Untuk rc=3 (file tidak ada), hanya buf yang perlu di-free—data dan fp belum dialokasikan saat error terjadi. Untuk rc=4 (file kosong), ketiga resource sudah dialokasikan dan harus dibebaskan. Untuk rc=0 (sukses), resource dipindahkan ke output struct dan tidak dibebaskan—pemanggil yang bertanggung jawab.
Mari kita telaah baris-baris kode di atas:
- Baris 16-18 (
char *buf = NULL; int *data = NULL; FILE *fp = NULL;): SEMUA resource diinisialisasi keNULL. Inilah kunci polagoto cleanup—free(NULL)danfclose(NULL)tidak sama.free(NULL)aman (tidak melakukan apa-apa menurut standar C §7.22.3.3), tetapifclose(NULL)adalah undefined behavior. Karena itu kita pakaiif (fp)sebelumfclose. - Baris 20-21 (
buf = malloc(64); if (buf == NULL) { rc = 1; goto cleanup; }): alokasi pertama. Bila gagal, setrclalu lompat kecleanup. Dicleanup,if (fp)false (fp masihNULL),if (data)false (data masihNULL),if (buf)false (buf jugaNULLkarena malloc gagal). Tidak ada yang dibebaskan—tepat. - Baris 23-24 (
data = malloc(...); if (data == NULL) { rc = 2; goto cleanup; }): alokasi kedua. Bila gagal,bufsudah dialokasikan dan harus dibebaskan.goto cleanupakan menjalankanif (buf) free(buf)—bufnon-NULL jadi dibebaskan. - Baris 26-27 (
fp = fopen(...); if (fp == NULL) { rc = 3; goto cleanup; }): alokasi ketiga.bufdandatasudah ada.goto cleanupmembebaskan keduanya. - Baris 30 (
if (fgets(buf, 64, fp) == NULL) { rc = 4; goto cleanup; }): kegagalan logis—file berhasil dibuka tetapi kosong. Sekali lagigoto cleanupmenangani ketiga resource. - Baris 33-36 (
out->nama = buf; ... return 0;): jalur sukses. Resource dipindahkan ke output struct dan fungsi langsungreturn 0—TIDAK lewatcleanupkarena kita ingin resource tetap hidup untuk pemanggil. - Baris 38-43 (
cleanup: if (fp) fclose(fp); if (data) free(data); if (buf) free(buf);): labelcleanup. Pembebasan dilakukan dalam urutan LIFO (terakhir dialokasikan pertama dibebaskan). Setiap resource diperiksa!= NULLsebelum dibebaskan.
Skenario Realistis: Parser Konfigurasi #
Mari kita terapkan pola ini di skenario yang lebih realistis: membaca file konfigurasi baris-per-baris, memparse angka, menyimpan ke array yang di-resize secara dinamis. Setiap langkah punya titik kegagalan sendiri.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Skenario realistis: baca konfigurasi dari file, alokasikan buffer,
parse ke array integer. Banyak titik kegagalan. */
typedef struct {
int *nilai;
size_t jumlah;
} hasil_parse_t;
int parse_konfigurasi(const char *path, hasil_parse_t *out) {
int rc = 0;
FILE *fp = NULL;
char *line = NULL;
int *arr = NULL;
size_t kapasitas = 4;
fp = fopen(path, "r");
if (fp == NULL) { rc = 1; goto cleanup; }
line = malloc(256);
if (line == NULL) { rc = 2; goto cleanup; }
arr = malloc(sizeof(int) * kapasitas);
if (arr == NULL) { rc = 3; goto cleanup; }
size_t n = 0;
while (fgets(line, 256, fp) != NULL) {
/* Abaikan baris kosong dan komentar */
if (line[0] == '\n' || line[0] == '#') continue;
/* Parse angka */
int val;
if (sscanf(line, "%d", &val) != 1) {
fprintf(stderr, "Baris tidak bisa di-parse: %s", line);
rc = 4;
goto cleanup;
}
/* Resize bila perlu */
if (n == kapasitas) {
size_t baru = kapasitas * 2;
int *tmp = realloc(arr, sizeof(int) * baru);
if (tmp == NULL) { rc = 5; goto cleanup; }
arr = tmp;
kapasitas = baru;
}
arr[n++] = val;
}
out->nilai = arr;
out->jumlah = n;
/* Sukses: kepemilikan arr dipindah ke out, jangan di-free */
free(line);
fclose(fp);
return 0;
cleanup:
/* LIFO: bebas resource dalam urutan terakhir-dialokasikan-dulu */
if (arr) free(arr);
if (line) free(line);
if (fp) fclose(fp);
return rc;
}
int main(void) {
/* Buat file konfigurasi demo */
FILE *cfg = fopen("/tmp/cfg_demo.txt", "w");
if (cfg) {
fprintf(cfg, "# Konfigurasi contoh\n");
fprintf(cfg, "10\n");
fprintf(cfg, "20\n");
fprintf(cfg, "30\n");
fprintf(cfg, "\n"); /* baris kosong */
fprintf(cfg, "40\n");
fclose(cfg);
}
hasil_parse_t hasil = {0};
int rc = parse_konfigurasi("/tmp/cfg_demo.txt", &hasil);
printf("parse_konfigurasi('/tmp/cfg_demo.txt') -> rc=%d\n", rc);
if (rc == 0) {
printf("jumlah=%zu, nilai:", hasil.jumlah);
for (size_t i = 0; i < hasil.jumlah; i++) {
printf(" %d", hasil.nilai[i]);
}
printf("\n");
free(hasil.nilai);
}
/* Test dengan file tidak valid (string bukan angka) */
FILE *bad = fopen("/tmp/cfg_bad.txt", "w");
if (bad) { fprintf(bad, "10\nbukan_angka\n"); fclose(bad); }
rc = parse_konfigurasi("/tmp/cfg_bad.txt", &hasil);
printf("\nparse_konfigurasi('/tmp/cfg_bad.txt') -> rc=%d (diharapkan 4)\n", rc);
return 0;
}Output yang Dihasilkan:
parse_konfigurasi('/tmp/cfg_demo.txt') -> rc=0
jumlah=4, nilai: 10 20 30 40
parse_konfigurasi('/tmp/cfg_bad.txt') -> rc=4 (diharapkan 4)Stderr:
Baris tidak bisa di-parse: bukan_angkaparse_konfigurasi menggabungkan banyak teknik sekaligus: alokasi file, buffer line, array dinamis dengan realloc, dan parsing baris per baris. Lima titik kegagalan (rc=1 sampai rc=5) semuanya diarahkan ke label cleanup yang sama. Bila sscanf gagal di tengah loop (kasus cfg_bad.txt), array arr yang sudah berisi 10 dibebaskan dengan benar—tidak ada memory leak.