Pointer adalah fitur paling kuat sekaligus paling berbahaya di C. Satu kesalahan kecil—mengabaikan inisialisasi, lupa cek NULL, atau melebihi batas array—bisa menyebabkan crash tak terduga, memory leak, atau kerusakan data yang sulit didiagnosis. Bab-bab sebelumnya menunjukkan berbagai varian bahaya pointer: wild, dangling, alias. Pada chapter penutup ini kita akan merangkum praktik terbaik (best practices) yang harus menjadi refleks setiap programmer C: selalu inisialisasi pointer, periksa NULL sebelum dereference, set NULL setelah free, dan lakukan bounds checking pada akses array. Dengan disiplin ini, sebagian besar bug pointer bisa dicegah sejak diketikkan.
Aturan 1: Selalu Inisialisasi #
Pointer yang dideklarasikan tanpa inisialisasi adalah wild pointer—berisi alamat acak dari sampah stack. Aturannya: setiap deklarasi pointer harus disertai inisialisasi, entah dengan alamat variabel yang valid atau dengan NULL. Jika alamat belum diketahui saat deklarasi, gunakan NULL sebagai penanda “belum siap pakai”. Cost-nya cuma satu assignment; manfaatnya deteksi bug lebih awal.
Mekanisme kerjanya: saat compiler melihat int *p = NULL;, ia menyimpan nilai null pointer ke variabel p. Dereference *p langsung memicu segfault yang mudah didiagnosis dengan debugger. Tanpa inisialisasi, p berisi alamat acak yang mungkin valid—dan dereference mungkin “berhasil” tapi merusak data tanpa peringatan. Inisialisasi ke NULL mengubah bug silent corruption menjadi bug crash yang segera terlihat.
#include <stdio.h>
#include <stdlib.h>
/* Helper: aman mengalokasikan dan cek NULL */
int *alokasi_array(int n) {
if (n <= 0) return NULL;
int *p = malloc((size_t)n * sizeof(int));
if (p == NULL) {
return NULL;
}
for (int i = 0; i < n; i++) {
p[i] = i * 10;
}
return p;
}
/* Helper: cetak dengan bounds checking */
void cetak_array(const int *arr, int n) {
if (arr == NULL || n <= 0) {
printf("(array kosong atau NULL)\n");
return;
}
for (int i = 0; i < n; i++) {
printf(" [%d] = %d\n", i, arr[i]);
}
}
int main(void) {
/* 1. Selalu cek NULL sebelum pakai */
int *data = alokasi_array(5);
if (data == NULL) {
printf("Alokasi gagal\n");
return 1;
}
printf("Array berhasil dialokasikan:\n");
cetak_array(data, 5);
/* 2. Set NULL setelah free */
free(data);
data = NULL;
printf("\nSetelah free + NULL: data == NULL? %s\n",
(data == NULL) ? "ya" : "tidak");
/* 3. Fungsi cetak aman menangani NULL */
printf("\nMemanggil cetak_array(NULL, 5):\n");
cetak_array(NULL, 5);
return 0;
}Output yang Dihasilkan:
Array berhasil dialokasikan:
[0] = 0
[1] = 10
[2] = 20
[3] = 30
[4] = 40
Setelah free + NULL: data == NULL? ya
Memanggil cetak_array(NULL, 5):
(array kosong atau NULL)Tiga aturan praktis tampak dalam kode di atas: (1) malloc selalu diikuti cek NULL—alokasi bisa gagal jika memori habis. (2) Setelah free(data), segera set data = NULL agar dereference berikutnya menimbulkan segfault yang jelas, bukan diam-diam merusak data. (3) Fungsi cetak_array memeriksa arr == NULL || n <= 0 di awal dan menangani kasus error dengan pesan yang jelas—pemanggil tidak perlu tahu detail internal.
Mari kita telaah baris-baris kode di atas:
- Baris 6-16 (
int *alokasi_array(int n)): fungsi factory yang mengembalikan pointer atau NULL. Parameterndiperiksa (n <= 0) sebelummalloc. Hasilmallocdiperiksaif (p == NULL). Pola defensif penuh. - Baris 19-29 (
void cetak_array(const int *arr, int n)): fungsi read-only denganconst int *. Cekarr == NULL || n <= 0di awal; jika salah satu true, cetak pesan dan return early—tidak dereference NULL. - Baris 36-37 (
int *data = alokasi_array(5); if (data == NULL) ...): pemanggil wajib cek NULL dari fungsi yang bisa gagal. Tanpa cek, dereference*databisa segfault. - Baris 43-44 (
free(data); data = NULL;): pasanganfree+set NULL. Praktek wajib untuk mencegah use-after-free dan double-free. - Baris 48 (
cetak_array(NULL, 5);): tes fungsi dengan NULL. Fungsi menangani dengan baik—tidak crash.
Aturan 2: Bounds Checking Eksplisit #
C tidak melakukan bounds checking otomatis pada akses array—arr[i] di luar batas tetap dicoba oleh compiler tanpa peringatan. Tanggung jawab ini ada pada programmer. Untuk fungsi yang menerima array dan indeks, selalu periksa idx >= 0 && idx < n sebelum arr[idx]. Kembalikan kode error (misalnya -1) jika indeks tidak valid, alih-alih mengakses memori di luar batas.
#include <stdio.h>
/* Fungsi dengan bounds checking eksplisit */
int akses_amank(const int *arr, int n, int idx, int *out_nilai) {
if (arr == NULL) return -1; /* pointer NULL */
if (idx < 0 || idx >= n) return -2; /* di luar batas */
*out_nilai = arr[idx];
return 0; /* sukses */
}
int main(void) {
int data[4] = {10, 20, 30, 40};
int hasil;
int rc;
rc = akses_amank(data, 4, 2, &hasil);
printf("Akses idx=2: rc=%d, hasil=%d\n", rc, hasil);
rc = akses_amank(data, 4, 10, &hasil);
printf("Akses idx=10: rc=%d (di luar batas)\n", rc);
rc = akses_amank(NULL, 4, 0, &hasil);
printf("Akses NULL: rc=%d (pointer NULL)\n", rc);
return 0;
}Output yang Dihasilkan:
Akses idx=2: rc=0, hasil=30
Akses idx=10: rc=-2 (di luar batas)
Akses NULL: rc=-1 (pointer NULL)Fungsi akses_amank mengembalikan kode error: 0 untuk sukses, -1 jika pointer NULL, -2 jika indeks di luar batas. Pemanggil harus memeriksa kode return sebelum menggunakan nilai di out_nilai. Pola ini mengubah bug silent out-of-bounds menjadi error eksplisit yang bisa ditangani. Library besar seperti glibc dan kernel Linux menerapkan pola defensif serupa pada fungsi yang menerima parameter pointer.