External linkage adalah properti simbol (variabel atau fungsi) yang membuat simbol tersebut dapat dilihat dan diakses oleh translation unit lain saat linking. Tanpa external linkage, setiap file .c harus mendefinisikan ulang simbol yang dibutuhkan—suatu hal yang tidak praktis di proyek berskala menengah. Pada chapter ini kita akan memahami konsep linkage di C, mempraktikkan deklarasi extern di header untuk variabel global lintas file, dan melihat bagaimana linker menyatukan simbol yang didefinisikan di satu .c dengan referensi di .c lain.

Konsep Linkage di C #

Standar ISO/IEC 9899 §6.2.2 mendefinisikan tiga jenis linkage: external, internal, dan no linkage. External linkage berarti simbol dapat dirujuk dari translation unit lain melalui linker. Internal linkage berarti simbol hanya terlihat di dalam translation unit tempat ia didefinisikan. No linkage berarti simbol bersifat lokal (mis. variabel lokal fungsi).

Secara default, fungsi dan variabel global yang didefinisikan di tingkat file memiliki external linkage. Artinya, fungsi int faktorial(int n) yang didefinisikan di math.c otomatis terlihat oleh linker sebagai simbol publik, dan dapat dipanggil dari main.c—cukup dengan deklarasi int faktorial(int n); di header atau langsung di main.c. Keyword extern pada deklarasi variabel adalah penanda eksplisit bahwa variabel tersebut didefinisikan di translation unit lain.

Perbedaan penting antara fungsi dan variabel: deklarasi fungsi (tanpa badan) secara implisit berperilaku extern—kita tidak perlu menulis keyword extern di depan. Untuk variabel, deklarasi int x; di header sebenarnya adalah tentative definition yang bisa menyebabkan multiple definition saat di-include oleh beberapa file .c. Oleh karena itu, deklarasi variabel lintas file wajib diawali extern di header, dan definisi aktual (int x = ...; atau int x;) hanya boleh muncul di satu file .c.

Demonstrasi: Variabel Global Lintas File #

Mari kita bangun proyek kecil dengan file config.c yang mendefinisikan beberapa variabel global publik, dan main.c yang mengakses variabel tersebut. Header config.h berisi deklarasi extern sebagai jembatan antara kedua file.

File config.h:

c
#ifndef CONFIG_H
#define CONFIG_H

/* Deklarasi extern: variable didefinisikan di config.c */
extern int versi_major;
extern int versi_minor;
extern const char *nama_aplikasi;

#endif

File config.c:

c
#include "config.h"

/* Definisi: variable hidup di file ini */
int versi_major = 1;
int versi_minor = 5;
const char *nama_aplikasi = "Aplikasi Demo";

File main.c:

c
#include <stdio.h>
#include "config.h"

void tampilkan_versi(void) {
    /* Mengakses variabel yang didefinisikan di config.c */
    printf("%s v%d.%d\n", nama_aplikasi, versi_major, versi_minor);
}

int main(void) {
    tampilkan_versi();

    /* Bisa juga dimodifikasi dari main.c */
    versi_minor = 6;
    printf("Setelah update: v%d.%d\n", versi_major, versi_minor);
    return 0;
}

Kompilasi dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -I. -o app main.c config.c && ./app

Output yang Dihasilkan:

text
Aplikasi Demo v1.5
Setelah update: v1.6

Perhatikan bahwa main.c dapat membaca dan menulis versi_minor meskipun variabel tersebut didefinisikan di config.c. Setelah baris versi_minor = 6; dijalankan, nilai variabel berubah dan terbaca sebagai 6 pada baris printf berikutnya. Hal ini dimungkinkan karena versi_minor memiliki external linkage.

Mari kita telaah baris-baris kunci di atas:

  1. Pada config.h baris 6-8 (extern int versi_major; ...): deklarasi tanpa inisialisasi. Keyword extern memberitahu compiler bahwa variabel ini didefinisikan di file lain—compiler tidak mengalokasikan memori di sini. Deklarasi hanya memberi tahu compiler tipe variabel agar pemakaian di kode dapat di-type-check.
  2. Pada config.c baris 4-6 (int versi_major = 1; ...): definisi aktual. Di sinilah variabel mendapatkan alamat memori. Inisialisasi (= 1, = 5, = "Aplikasi Demo") bersifat opsional tetapi sangat dianjurkan untuk menghindari uninitialized global. Tanpa inisialisasi, variabel global otomatis bernilai 0 (untuk tipe skalar) atau NULL (untuk pointer).
  3. Pada main.c baris 4 (#include "config.h"): header menyisipkan tiga deklarasi extern. Compiler sekarang tahu tipe ketiga variabel dan mengizinkan main.c mereferensikannya.
  4. Pada main.c baris 7 (printf("%s v%d.%d\n", nama_aplikasi, versi_major, versi_minor);): compiler menghasilkan instruksi mesin yang mereferensikan alamat simbol nama_aplikasi, versi_major, dan versi_minor. Pada tahap kompilasi, alamat ini masih kosong (undefined); linker akan mengisinya nanti.
  5. Pada main.c baris 12 (versi_minor = 6;): penugasan ke variabel external. Karena versi_minor adalah variabel global biasa (bukan const), nilainya dapat diubah dari file mana pun yang melihat deklarasi extern.
  6. Tahap linking: linker melihat main.o mereferensikan simbol versi_minor yang belum terdefinisi. Linker mencari di config.o dan menemukan definisi int versi_minor = 5;. Simbol dicocokkan dan alamatnya diisi ke instruksi mesin di main.o. Hasil akhir: executable yang siap dijalankan.

Bahaya Global Mutable State #

External linkage sangat berguna tetapi juga berbahaya jika disalahgunakan. Variabel global yang dapat dimodifikasi dari mana saja (mutable global state) sulit dilacak di proyek besar—bug yang muncul bisa tidak terduga karena banyak fungsi berbagi state yang sama. Untuk meminimalkan risiko, beberapa praktik disarankan:

  1. Pakai variabel global hanya untuk konstanta (mis. const char *nama_aplikasi = "Demo";).
  2. Untuk state yang dapat diubah, enkapsulasi via fungsi setter/getter (mis. int get_versi_minor(void); void set_versi_minor(int v);) dan simpan variabel aktual sebagai static di file .c.
  3. Hindari meng-expose tipe kompleks (struct besar) sebagai global; gunakan pointer ke handle yang dialokasikan via fungsi factory.