Saat kita menulis fungsi untuk dipakai orang lain—atau bahkan untuk diri kita sendiri tiga bulan ke depan—kita tidak bisa berasumsi pemanggil selalu memberi input yang valid. Pointer bisa NULL, array bisa kosong, string bisa lebih panjang dari buffer, dan angka bisa lebih besar dari batas tipe. Defensive programming adalah filosofi menulis kode yang mengasumsikan hal-hal ini akan terjadi dan menghadapiinya secara eksplisit. Pada chapter ini kita akan mempelajari tiga pilar utama: pengecekan NULL, validasi array bounds, dan validasi isi input. Topik ini melengkapi (bukan menggantikan) assert—assert untuk bug programmer yang tidak boleh terjadi, defensive check untuk input yang sah saja bisa salah.

Pilar 1: Pengecekan NULL Pointer #

Kesalahan paling sering di C adalah dereference pointer NULL. Instruksi *p saat p == NULL memicu segmentation fault (sinyal SIGSEGV) dan program berhenti. Pengecekan if (p == NULL) di awal fungsi adalah bentuk pertahanan paling dasar.

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

/* Versi non-defensif: tidak memeriksa parameter sama sekali.
   Pemanggil yang salah akan menyebabkan crash / undefined behavior. */
int sum_array_naive(int *arr, size_t n) {
    int total = 0;
    for (size_t i = 0; i < n; i++) {
        total += arr[i];
    }
    return total;
}

/* Versi defensif: periksa NULL pointer, nol elemen, dan batas.
   Mengembalikan 0 untuk input yang tidak valid (konvensi sederhana). */
int sum_array_safe(const int *arr, size_t n) {
    if (arr == NULL) {
        fprintf(stderr, "sum_array_safe: arr == NULL\n");
        return 0;
    }
    if (n == 0) {
        return 0;
    }

    int total = 0;
    for (size_t i = 0; i < n; i++) {
        total += arr[i];
    }
    return total;
}

int main(void) {
    int data[] = {10, 20, 30, 40};
    size_t n = sizeof(data) / sizeof(data[0]);

    printf("safe(data, 4)   = %d\n", sum_array_safe(data, n));
    printf("safe(NULL, 4)  = %d\n", sum_array_safe(NULL, n));
    printf("safe(data, 0)  = %d\n", sum_array_safe(data, 0));
    return 0;
}

Output yang Dihasilkan:

text
sum_array_safe: arr == NULL
safe(data, 4)   = 100
safe(NULL, 4)  = 0
safe(data, 0)  = 0

Versi sum_array_safe memeriksa dua kondisi sebelum melakukan loop: arr == NULL dan n == 0. Bila salah satu true, fungsi mencetak pesan diagnostik (untuk NULL) atau mengembalikan nilai default 0 dan keluar lebih awal. Bandingkan dengan sum_array_naive yang akan langsung crash bila dipanggil sum_array_naive(NULL, 4).

Mari kita telaah baris-baris kode di atas:

  1. Baris 14 (if (arr == NULL)): pengecekan pertama dan paling kritis. Tanpa ini, arr[i] di baris 22 akan melakukan dereference NULL + i*sizeof(int), memicu SIGSEGV.
  2. Baris 18 (if (n == 0)): bukan error—array kosong secara sah menjumlahkan 0—tetapi kita keluar lebih awal untuk efisiensi dan menghindari potensi jebakan lain (misal bila ada logika yang mengasumsikan n > 0).
  3. Baris 21-23 (for (size_t i = 0; i < n; i++) total += arr[i];): loop aman karena arr dijamin non-NULL dan n dijamin berada dalam ukuran array (pemanggil yang memberi n salah tetap akan out of bounds, tetapi itu di luar kendali fungsi—lihat pilar 2).
  4. Baris 29 (printf("safe(NULL, 4) = %d\n", sum_array_safe(NULL, n));): pemanggilan dengan NULL. Fungsi mencetak pesan ke stderr dan mengembalikan 0, sehingga yang tercetak adalah safe(NULL, 4) = 0. Program tetap berjalan tanpa crash.

Pilar 2: Validasi Array Bounds #

C tidak melacak ukuran array secara otomatis. Fungsi yang menerima int *arr, size_t n hanya tahu n yang diberikan pemanggil—bukan ukuran sebenarnya. Namun, fungsi yang menerima buffer tujuan char *dst, size_t dst_size wajib menjamin tidak menulis melebihi dst_size. Inilah inti dari banyak vulnerability keamanan klasik seperti buffer overflow.

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

/* Fungsi yang memeriksa array bounds secara defensif.
   Kontrak: salin sebanyak n byte dari src ke dst.
   Garansi: tidak akan membaca/melewati batas yang diberikan pemanggil. */
size_t salin_amen(char *dst, size_t dst_size, const char *src, size_t n) {
    if (dst == NULL || src == NULL) return 0;
    if (dst_size == 0) return 0;

    /* Hitung berapa banyak yang benar-benar bisa disalin */
    size_t batas = n;
    if (batas >= dst_size) {
        batas = dst_size - 1;   /* sisakan ruang untuk '\0' */
    }

    for (size_t i = 0; i < batas; i++) {
        dst[i] = src[i];
        if (src[i] == '\0') {    /* berhenti di terminator */
            return i;
        }
    }
    dst[batas] = '\0';
    return batas;
}

int main(void) {
    char buf[8];
    const char *sumber = "Halo dunia, ini terlalu panjang";

    size_t tersalin = salin_amen(buf, sizeof(buf), sumber, strlen(sumber));
    printf("Tersalin: %zu byte\n", tersalin);
    printf("Isi buf : '%s'\n", buf);
    printf("Panjang : %zu (kapasitas buf = %zu)\n", strlen(buf), sizeof(buf));
    return 0;
}

Output yang Dihasilkan:

text
Tersalin: 7 byte
Isi buf : 'Halo du'
Panjang : 7 (kapasitas buf = 8)

buf berukuran 8 byte, sedangkan string sumber panjangnya 31 karakter. Tanpa perlindungan, strcpy(buf, sumber) akan menulis 32 byte (termasuk \0) ke buffer 8 byte—buffer overflow yang mengkorupsi memori. Fungsi salin_amen mencegahnya: variabel batas dijepit ke dst_size - 1 = 7 agar selalu ada ruang untuk terminator '\0'. Hanya 7 byte pertama yang disalin ("Halo du"), dan terminator ditempatkan di indeks ke-7.

Pilar 3: Validasi Isi Input #

Memeriksa NULL dan ukuran array melindungi dari crash struktural, tetapi tidak menjamin isi input masuk akal. String "12abc" bukan angka yang valid, meskipun pointer-nya non-NULL dan tidak kosong. Pilar ketiga defensive programming adalah memvalidasi isi input, bukan hanya strukturnya.

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

/* Defensif parse integer dari string.
   Memvalidasi input tidak NULL, hasil parse tidak overflow. */
typedef enum {
    PARSE_OK = 0,
    PARSE_NULL = 1,
    PARSE_EMPTY = 2,
    PARSE_NONDIGIT = 3,
    PARSE_OVERFLOW = 4,
} parse_status;

parse_status parse_int(const char *s, int *out) {
    if (s == NULL)  return PARSE_NULL;
    if (*s == '\0') return PARSE_EMPTY;

    int sign = 1;
    const char *p = s;
    if (*p == '-') { sign = -1; p++; }
    else if (*p == '+') { p++; }

    if (*p == '\0') return PARSE_NONDIGIT;

    long nilai = 0;
    while (*p != '\0') {
        if (*p < '0' || *p > '9') return PARSE_NONDIGIT;

        nilai = nilai * 10 + (*p - '0');
        if (nilai > 2147483647L) return PARSE_OVERFLOW;   /* batas INT_MAX */

        p++;
    }

    *out = (int)(sign * nilai);
    return PARSE_OK;
}

int main(void) {
    const char *kasus[] = {
        "123",
        "-456",
        "",
        "12abc",
        "99999999999",
        NULL
    };
    int n = sizeof(kasus) / sizeof(kasus[0]);

    for (int i = 0; i < n; i++) {
        int val;
        parse_status s = parse_int(kasus[i], &val);
        const char *input = kasus[i] ? kasus[i] : "(NULL)";

        if (s == PARSE_OK) {
            printf("parse_int('%s') = %d  [OK]\n", input, val);
        } else {
            const char *alasan = "?";
            switch (s) {
                case PARSE_NULL:     alasan = "input NULL"; break;
                case PARSE_EMPTY:    alasan = "input kosong"; break;
                case PARSE_NONDIGIT: alasan = "karakter non-digit"; break;
                case PARSE_OVERFLOW:  alasan = "overflow"; break;
                default: break;
            }
            printf("parse_int('%s') GAGAL: %s\n", input, alasan);
        }
    }
    return 0;
}

Output yang Dihasilkan:

text
parse_int('123') = 123  [OK]
parse_int('-456') = -456  [OK]
parse_int('') GAGAL: input kosong
parse_int('12abc') GAGAL: karakter non-digit
parse_int('99999999999') GAGAL: overflow
parse_int('(NULL)') GAGAL: input NULL

Fungsi parse_int memvalidasi input di tiga lapisan: (1) NULL di awal, (2) empty string "", (3) karakter non-digit selama parsing, dan (4) overflow batas INT_MAX. Setiap lapisan mengembalikan status berbeda sehingga pemanggil tahu persis apa yang salah—dan bisa memberi pesan error yang spesifik kepada pengguna akhir.