Dua pertanyaan untuk setiap variabel
Setiap variabel di C punya dua sifat yang terpisah, dan mencampuradukkannya adalah sumber sebagian besar kejutan di halaman ini:
- Scope: di mana nama itu bisa dipakai. Pertanyaan saat kompilasi.
- Umur (lifetime): berapa lama tempat penyimpanannya ada. Pertanyaan saat runtime.
Keduanya biasanya berjalan bersama (variabel lokal biasa terlihat di bloknya dan hidup persis selama itu), tapi static memisahkan keduanya, dan karena itulah kata kunci ini layak dipahami di sini.
Scope blok
Variabel yang dideklarasikan di dalam kurung kurawal terlihat dari deklarasinya sampai kurung kurawal penutup, dan tidak di tempat lain.
Blok dalam bisa melihat outer, karena blok itu bersarang di dalamnya. Blok luar tidak bisa melihat inner. Ini berlaku untuk setiap pasang kurung kurawal: badan fungsi, badan if, badan loop, atau blok polos seperti di atas.
Loop membuatnya lebih nyata. Penghitung yang dideklarasikan di header for adalah milik loop itu:
for (int i = 0; i < 5; i++) {
/* i lives here */
}
/* i does not exist here */
Dan itulah yang kamu inginkan: dua loop di fungsi yang sama masing-masing bisa memakai i tanpa saling mengganggu, dan penghitungnya tidak bisa terbaca tanpa sengaja setelah loop selesai. Kalau kamu butuh nilainya sesudahnya, misalnya indeks tempat pencarian berhenti, deklarasikan sebelum loop.
Parameter fungsi punya scope badan fungsi, jadi perilakunya seperti variabel lokal yang dideklarasikan di bagian atasnya.
C99 mengizinkan deklarasi di mana saja di dalam blok, tidak hanya di bagian atas. Deklarasikan variabel di tempat kamu pertama kali membutuhkannya; variabel dengan scope pendek adalah variabel dengan lebih sedikit cara untuk salah.
Umur: penyimpanan otomatis
Variabel lokal biasa punya penyimpanan otomatis: variabel itu muncul saat kendali memasuki bloknya dan dihancurkan saat kendali meninggalkannya. Memorinya ada di stack.
Kode itu mencetak 1 tiga kali. Setiap pemanggilan mendapat count yang benar-benar baru, diinisialisasi ke 0, lalu dibuang saat fungsi kembali. Karena itu juga setiap frame dari fungsi rekursif punya salinan sendiri untuk setiap variabel lokal.
Ada dua akibat yang perlu dikatakan terus terang. Variabel lokal yang belum diinisialisasi berisi sampah, bukan nol: byte apa pun yang ada di stack. Lalu mengembalikan pointer ke variabel lokal adalah undefined behavior, karena tempat penyimpanannya hilang begitu fungsinya kembali:
/* BUG: buf does not exist after the return */
char *broken(void) {
char buf[64] = "hello";
return buf;
}
Variabel lokal static: ingatan di antara pemanggilan
Tambahkan static pada variabel lokal, maka umurnya menjadi sepanjang program, sementara scope-nya tetap sama persis.
Sekarang penghitungnya mencetak 1, 2, 3. = 0 dijalankan sekali, sebelum main dimulai, bukan di setiap pemanggilan.
Tiga aturan untuk variabel lokal static:
- Nilai awalnya harus berupa ekspresi konstan, karena diterapkan saat program dimulai, bukan saat runtime.
- Tanpa nilai awal, variabel
staticdiinisialisasi ke nol (berbeda dengan variabel lokal otomatis, yang berisi sampah). - Variabelnya tetap privat untuk fungsi itu. Tidak ada kode lain yang bisa menyebut namanya.
Poin terakhir itulah daya tarik utamanya: sebuah fungsi bisa mengingat sesuatu di antara pemanggilan tanpa membuka variabel global yang bisa diubah oleh apa saja. Pakai untuk penghitung pemanggilan, tabel lookup yang di-cache dan dibangun saat pertama kali dipakai, dan flag inisialisasi satu kali.
Harganya, fungsi itu tidak lagi berupa pemetaan murni dari input ke output: argumen yang sama bisa memberi jawaban yang berbeda. Akibatnya fungsi itu lebih sulit diuji dan tidak aman dipanggil dari beberapa thread sekaligus tanpa perlindungan. Pakai variabel lokal static dengan sengaja, bukan sekadar karena praktis.
Scope file: variabel global
Variabel yang dideklarasikan di luar semua fungsi punya scope file. Variabel itu terlihat dari deklarasinya sampai akhir file, di setiap fungsi di bawahnya, dan umurnya sepanjang program.
Variabel global secara bawaan diinisialisasi ke nol, jadi totalOperations dimulai dari 0 meskipun tanpa = 0.
Secara bawaan, variabel global juga punya external linkage: file .c lain di program yang sama bisa menjangkaunya dengan mendeklarasikannya sebagai extern:
/* in stats.c */
int totalOperations = 0; /* the definition - exactly one in the program */
/* in main.c */
extern int totalOperations; /* a declaration: "it exists somewhere" */
Taruh deklarasi extern di file header supaya setiap pemakai melihat deklarasi yang sama. Perhatikan ketidakseimbangannya: deklarasi extern mengatakan variabel itu ada, dan tepat satu file .c harus benar-benar mendefinisikannya.
Kenapa variabel global menggigit
Variabel global adalah cara termudah untuk berbagi data, dan juga cara termudah untuk membuat bug yang tidak bisa kamu lokalisasi. Ada empat masalah konkret:
Apa saja bisa mengubahnya. Saat totalOperations berisi nilai yang salah, pelakunya bisa baris mana pun di file mana pun. Dengan parameter, tersangkanya hanya tempat pemanggilan yang bisa kamu lihat.
Fungsi jadi tidak bisa diuji. Fungsi yang membaca variabel global tidak bisa dipanggil secara terpisah; kamu harus menyiapkan state global dulu, lalu ingat untuk mengaturnya ulang sesudahnya.
Namanya berlaku di seluruh program. Variabel global bernama count atau buffer cepat atau lambat akan bentrok dengan milik orang lain.
Nilai awalnya harus berupa konstanta waktu kompilasi. Di C, variabel global hanya bisa diinisialisasi dengan ekspresi konstan: int limit = readConfig(); tidak akan bisa dikompilasi, dan int b = a * 2; di scope file juga tidak. Apa pun yang dihitung harus diberikan saat runtime oleh suatu fungsi persiapan, dan setiap file yang menyentuh variabel global itu sebelum pemanggilan tersebut berjalan akan melihat 0 tanpa peringatan apa pun.
Alternatifnya hampir selalu tersedia: kirim nilai sebagai parameter, kembalikan hasil, dan gabungkan state yang saling berhubungan ke dalam struct yang dikirim secara eksplisit. Pengecualian yang sah memang ada, seperti objek konfigurasi untuk seluruh program atau handle logging, dan bahkan itu lebih baik dibuat static di scope file dengan fungsi akses, yang dibahas di bagian berikutnya.
static di scope file: privasi
Pada variabel global atau fungsi, static berarti sesuatu yang sama sekali berbeda dari artinya pada variabel lokal: internal linkage. Nama itu menjadi privat untuk file .c-nya sendiri dan tidak bisa dijangkau dari file lain.
/* counter.c */
#include "counter.h"
static int count = 0; /* private to this file - no other file can touch it */
static void validate(void) { /* a private helper, not part of the interface */
if (count < 0) count = 0;
}
void increment(void) { /* public: declared in counter.h */
count++;
validate();
}
int getCount(void) { /* public */
return count;
}
File lain bisa memanggil increment dan getCount, tapi tidak bisa melihat count dan tidak bisa memanggil validate: linker tidak akan menyelesaikan nama-nama itu. Itulah sistem modul C, sejauh yang dimilikinya: header yang mendeklarasikan fungsi publik, dan static untuk semua yang lain.
Ada dua manfaat praktis selain kerapian. Dua file masing-masing boleh punya static void validate(void) tanpa bentrok, yang tidak mungkin kalau fungsinya publik. Lalu compiler tahu fungsi static tidak punya pemanggil di luar file, jadi bisa meng-inline-nya lebih agresif atau memberi peringatan kalau fungsinya tidak dipakai.
Jadi kata kunci ini punya dua arti yang berbeda, ditentukan oleh di mana ia muncul:
static on a LOCAL variable -> lifetime becomes permanent (scope unchanged)
static at FILE scope -> linkage becomes internal (lifetime unchanged)
Shadowing
Mendeklarasikan sebuah nama di scope dalam yang sudah ada di scope luar akan menutupi (shadow) nama luar itu: di dalam scope dalam, nama itu merujuk ke variabel baru dan variabel luarnya tidak bisa dijangkau.
Ini sah, dan kadang memang disengaja. Lebih sering ini kecelakaan, dan mahal akibatnya: fungsi yang seharusnya memperbarui variabel global malah memperbarui variabel lokal bernama sama, dan variabel globalnya tidak pernah berubah. Hal yang sama terjadi saat variabel lokal menutupi parameter, sehingga penugasan yang kamu maksudkan untuk input tidak sampai ke mana-mana.
gcc -Wshadow melaporkan setiap kasusnya. Flag ini tidak termasuk di -Wall, jadi nyalakan secara eksplisit:
gcc -Wall -Wextra -Wshadow program.c -o program
Kebiasaan yang menghindari masalah ini sepenuhnya adalah memberi nama berdasarkan peran, bukan berdasarkan tipe: totalScore dan itemScore tidak bisa saling menutupi, sementara dua variabel yang sama-sama bernama n cepat atau lambat akan saling menutupi.
Referensi singkat
declared inside a block block scope, automatic lifetime, garbage if uninitialised
function parameter block scope of the function body, a copy of the argument
static inside a function block scope, PROGRAM lifetime, zero-initialised, kept between calls
declared outside all functions file scope, program lifetime, zero-initialised, visible to other files
static outside all functions file scope, program lifetime, PRIVATE to this .c file
extern declaration names a variable defined in another file
Pertanyaan yang sering diajukan
Apa itu scope di C?
Scope adalah wilayah kode tempat sebuah nama terlihat. Variabel yang dideklarasikan di dalam sebuah blok (sepasang kurung kurawal) hanya terlihat di blok itu, dan itulah scope blok. Variabel yang dideklarasikan di luar semua fungsi punya scope file dan terlihat dari deklarasinya sampai akhir file.
Apa yang dilakukan static pada variabel lokal di C?
Static mengubah umur variabel tanpa mengubah scope-nya. Variabel lokal static dibuat sekali, diinisialisasi sekali, dan menyimpan nilainya di antara pemanggilan, tapi tetap hanya terlihat di dalam fungsinya. Inilah cara memberi fungsi sebuah ingatan tanpa memakai variabel global.
Apa bedanya static dan global di C?
Variabel global biasa terlihat oleh setiap file di program, dan file lain bisa menjangkaunya dengan extern. Variabel atau fungsi static di scope file bersifat privat untuk satu file .c itu saja: file lain tidak bisa me-link ke sana. static di scope file adalah soal menyembunyikan; static pada variabel lokal adalah soal umur.
Kenapa variabel global itu buruk di C?
Fungsi mana pun bisa mengubahnya, jadi bug bisa disebabkan dari mana saja di program; fungsi yang membacanya tidak bisa diuji secara terpisah; dan namanya memenuhi seluruh program sehingga rawan bentrok. Kirim nilai sebagai parameter dan kembalikan hasilnya, dan di tempat state bersama memang dibutuhkan, jadikan static di scope file supaya hanya satu file yang bisa menyentuhnya.