Ders 07 / 13
Değişkenler, Kapsam ve Ömür
Bir adın nereden göründüğünün derlemede, bir değerin ne kadar yaşadığının koşumda karara bağlanması; aynı adın iki birimde iki ayrı nesneye düşüp düşmediğinin bağlayıcı tarafından çözülmesi.
Yayın Güncelleme
Bu derste
- Bir adın kapsamının derlemede, bir değerin ömrünün koşumda belli olduğunu ayırt eder
- static ve extern belirteçlerinin hangi aşamayı etkilediğini örnekle gösterir
- Aynı adın iki birimde iki ayrı nesneye düşebildiğini bağlayıcı üzerinden ölçer
İçindekiler
Veri Tipleri dersi, zaten var olan bir adın taşıdığı tipin nasıl karara bağlandığını ölçtü. Bu ders bir adım geriye gidiyor: bir adın kendisi nereden görünür ve o adın işaret ettiği değer ne kadar yaşar? Bu iki soru aynı görünse de aynı aşamada yanıtlanmıyor — biri kaynağın yapısına, öbürü koşumun akışına ait. Bir üçüncü soru da bunlara eklenecek: bir program birden çok birimden kurulduğunda, aynı adın iki birimde aynı nesneye mi yoksa iki ayrı nesneye mi düştüğü de ayrı bir aşamada, ayrı bir kurala göre çözülüyor.
Programlama Temelleri kursu (M01/K02) kapsam ve ömür kavramlarını Python’la zaten kurmuştu; orada bir ad bir işlev çağrısı bittiğinde kaybolur, modül düzeyindeki bir ad ise programın sonuna kadar yaşardı. C’nin karşılığı burada ölçülüyor: kapsam ile ömür C’de yalnız kavramsal olarak değil, karara bağlandıkları aşama bakımından da ayrılıyor — biri derlemede sabitlenir, öbürü ancak koşum sırasında gözlenebilir.
Kapsam: Bir Adın Nereden Göründüğü
Bir adın hangi bildirime bağlandığı, kaynağın yapısına bakılarak derlemede belirlenir. Aynı ad, iç içe geçmiş bloklarda yeniden bildirildiğinde, en içteki bildirim dıştakini gölgeler — dıştaki ad geçici olarak erişilemez olur, ama yok olmaz.
// kapsam.c — bir adin hangi bildirime baglandigi derlemede belli olur
#include <stdio.h>
int main(void) {
int deger = 10;
printf("%s %d\n", "disaridaki deger", deger);
{
int deger = 20;
printf("%s %d\n", "icerideki deger", deger);
}
printf("%s %d\n", "blok sonrasi deger", deger);
return 0;
}
disaridaki deger 10 icerideki deger 20 blok sonrasi deger 10
TS3. Blok içindeki deger bildirimi, dıştaki degeri yalnız kendi bloğunun
sınırları içinde gölgeliyor; blok kapandığında dıştaki ad yeniden görünür oluyor ve
değeri hiç değişmemiş — çünkü iki deger aslında iki ayrı nesne, aynı adı taşıyan iki
farklı depolama alanı. Hangi degere erişildiği, programın hangi satırda olduğuna değil,
o satırın kaynakta hangi blok içinde yazıldığına bakılarak belirleniyor; bu bilgi kaynağın
kendisinde tam olarak yazılı ve derleyici onu koşum hiç başlamadan çözüyor. Aynı kural
işlev parametreleri için de geçerlidir: bir işlevin parametre adı, işlev gövdesinin en dış
bloğunda bildirilmiş bir addır ve aynı gölgeleme kuralına tabidir.
Kapsam kuralı yalnız { } ile açılan bloklarla sınırlı değil; bir for döngüsünün
başlığında bildirilen sayaç da kendi kapsamını taşır ve döngü bittiğinde o ad artık
görünmez olur.
// dongu_kapsam.c — for basligindaki bildirim, dongunun kendi kapsamina aittir
#include <stdio.h>
int main(void) {
int i = 100;
for (int i = 0; i < 3; i++) {
printf("%s %d\n", "dongu icindeki i", i);
}
printf("%s %d\n", "dongu disindaki i", i);
return 0;
}
dongu icindeki i 0 dongu icindeki i 1 dongu icindeki i 2 dongu disindaki i 100
for başlığındaki i, dış kapsamdaki iyi yalnız döngü gövdesi boyunca gölgeliyor;
döngü bittiğinde dıştaki i — hiç dokunulmamış hâliyle, 100 — yeniden görünür oluyor.
Bu, blok gölgelemesinin özel bir biçimi: döngü başlığı kendi başına bir kapsam açıyor ve
bu kapsam yalnız kaynağın yazılış biçiminden, hiçbir veriye bakılmadan belirleniyor.
Ömür: Bir Değerin Ne Kadar Yaşadığı
Kapsamın aksine, bir değerin ne kadar yaşadığı kaynağı okuyarak değil, programı çalıştırarak gözlenir. Aşağıdaki ölçüm aynı işlevi üç kez çağırıyor ve iki farklı bildirim biçiminin çağrılar arasında nasıl davrandığını sayıyor.
// omur.c — bir degerin ne kadar yasadigi ancak kosarken belli olur
#include <stdio.h>
int otomatik_sayac(void) {
int sayac = 0;
sayac++;
return sayac;
}
int kalici_sayac(void) {
static int sayac = 0;
sayac++;
return sayac;
}
int main(void) {
for (int i = 0; i < 3; i++) {
printf("%s %d\n", "cagri", i + 1);
printf(" %s %d\n", "otomatik sayac", otomatik_sayac());
printf(" %s %d\n", "kalici sayac", kalici_sayac());
}
return 0;
}
cagri 1 otomatik sayac 1 kalici sayac 1 cagri 2 otomatik sayac 1 kalici sayac 2 cagri 3 otomatik sayac 1 kalici sayac 3
otomatik_sayac içindeki sayac, her çağrıda yeniden doğuyor ve işlev dönerken ölüyor —
bu, C’nin storage duration kavramının ömür karşılığı ve M10/K02’nin yaşam süresi (lifetime) terimiyle karıştırılmaz: orada ölçülen, bir değerin sahiplik
zincirinde ne zaman geçersiz kaldığıydı; burada ölçülen, bir değişkenin bellekte ne kadar
süre ayrılmış kaldığı. static belirteci bu süreyi değiştiriyor: kalici_sayac
içindeki sayac, programın başında bir kez ilklenir ve işlev her döndüğünde ortadan
kalkmaz — bir sonraki çağrı, bir önceki çağrının bıraktığı değeri bulur. Üç çağrı
boyunca otomatik sayac hep 1 kalırken kalici sayac 1, 2, 3 diye ilerliyor;
bu fark kaynakta değil, kaç kez çağrıldığında ortaya çıkıyor — kaynağı okuyarak
önceden söylenemez, yalnız bir koşum bunu ortaya koyar.
TS4. static belirteci burada iki farklı şeyi aynı anda etkilemiyor: sayacın
kapsamı hâlâ işlev gövdesiyle sınırlı — dışarıdan doğrudan erişilemez — ama ömrü artık
işlev çağrısına değil, programın tamamına bağlı. Kapsam ile ömrün ayrı sorular olduğu
tam da burada görünür: aynı bildirimde biri değişirken öbürü sabit kalabiliyor.
Bir dosyanın en dış düzeyinde (herhangi bir işlev gövdesinin dışında) bildirilen bir
değişken zaten programın tamamı kadar yaşar — birim_sayaci gibi bir dosya kapsamlı ad,
static yazılsın ya da yazılmasın, ömür bakımından en baştan kalıcıdır. static
belirtecinin dosya düzeyinde eklediği şey ömür değil, kapsamın daraltılmasıdır: aşağıda
ölçülecek olan kapsam_sayaci örneğinde görüleceği gibi, kendi biriminin dışına
kapatılır. Yerel bir değişkende ise staticin etkisi tam tersi yönde çalışıyor — kapsamı
aynı kalırken ömrü uzatıyor. Aynı anahtar sözcük, bildirimin bulunduğu yere göre iki farklı
kararı etkiliyor; bu, staticin tek bir anlamı olmadığının, bağlamına göre kapsamı ya da
ömrü değiştirebildiğinin bir göstergesi.
Kapsam ve Ömrün Bağımsızlığı
Kapsam ile ömür ayrı sorular olduğu için, bir bildirimin ikisinin de farklı bileşimlerini
taşıması mümkün. Yerel bir değişken (blok kapsamı, otomatik ömür) ve yerel bir static
değişken (blok kapsamı, kalıcı ömür) yukarıda ölçüldü — ikisi de aynı bloğa hapsolmuş, ama
biri her çağrıda yeniden doğuyor, öbürü kalıcı. Dosya düzeyinde bildirilen bir değişken ise
üçüncü bileşimi gösteriyor: birim_sayaci dosya kapsamlıdır — bildirildiği birimin her
yerinden görünür — ve ömrü baştan itibaren kalıcıdır, static yazılsın ya da yazılmasın.
Kaynakta olmayan tek bileşim ise dosya kapsamı ile otomatik ömrün birleşimi: dosya
düzeyinde bildirilen bir ad hiçbir zaman bir işlev çağrısına bağlı olarak doğup ölmez,
çünkü dosya kapsamındaki bir bildirim zaten bir çağrı çerçevesinin içinde değildir. Bu
üç bileşim, kapsam ile ömrün gerçekten bağımsız iki eksen olduğunu gösteriyor: biri
“nereden görünür” sorusuna, öbürü “ne kadar yaşar” sorusuna yanıt veriyor ve bir
bildirimin bu iki eksendeki konumu ayrı ayrı seçilebiliyor.
Bağlayıcı: Başka Birimdeki Değişken
Kapsam ile ömür bir birimin içinde kalan sorulardı. Bir üçüncü soru, birden çok birim
devreye girdiğinde ortaya çıkıyor: bir ad başka bir birimde tanımlıysa, o ada erişim
ne zaman çözülür? Bu soruyu yanıtlamak için bildirim ile tanım arasındaki farkı
ayırmak gerekiyor. Bir bildirim, derleyiciye yalnız bir adın tipini ve var olduğunu
söyler — hiçbir bellek ayırmaz. Bir tanım ise o ad için gerçekten bir depolama alanı
oluşturur. extern int birim_sayaci; bir bildirimdir; int birim_sayaci = 2; ise bir
tanımdır. Bir ad, farklı birimlerde istenildiği kadar çok kez bildirilebilir — her birim
kendi kopyasını tutar — ama harici bağlantılı bir ad için tanım tam olarak bir kez
bulunmalıdır; aksi halde bağlayıcı, aynı ada birden fazla depolama alanı ayırma çelişkisiyle
karşılaşır. Aşağıdaki kesit, Derleme Aşamaları dersinde tanıtılan çekirdeğin
bir parçasıdır.
// asama.h — baska birimlerde tanimli adlarin bildirimi (kesit: degisken bildirimi) #ifndef ASAMA_H #define ASAMA_H /* Bu degisken baska bir birimde tanimli: adi burada bildirilir, degeri baglama asamasinda cozulur. */ extern int birim_sayaci; #endif
// secim.c — ikinci birim: degiskenin tanimi burada durur #include "asama.h" int birim_sayaci = 2;
// asama.c — baska birimdeki degiskenin baglanmasi (kesit)
#include <stdio.h>
#include "asama.h"
int main(void) {
printf("%s %d\n", "baska birimdeki degisken", birim_sayaci);
return 0;
}
baska birimdeki degisken 2
asama.c, birim_sayaci adını yalnız bildirimden (extern int birim_sayaci;) tanıyor;
bu bildirim adın tipini ve varlığını garanti ediyor, ama değerini değil. Değer
secim.cde tanımlanıyor ve asama.cnin derlenmesi sırasında henüz hiçbir yere
bağlanmamış durumda — iki birim ayrı ayrı çevriliyor. Adın gerçek konumunu bulup çağrıyı
buna bağlayan adım bağlayıcıdır: iki birimin nesne dosyaları bir araya getirildiğinde,
asama.cnin bıraktığı boşluk secim.cnin sağladığı tanımla dolduruluyor. Bu, kapsam ve
ömürden ayrı bir üçüncü karardır: kapsam adın görünürlüğünü, ömür değerin süresini,
bağlama ise adın hangi tanıma karşılık geldiğini belirliyor.
Bildirim ile tanım arasındaki bu ayrım, başlık dosyalarının neden yalnız bildirim
taşıdığını da açıklıyor: asama.h birden fazla birime dahil edilebilir (her ikisi de
#include "asama.h" yazabilir) ve her dahil edilişte yalnız bir bildirim eklenir, bir
tanım değil. Eğer başlık dosyası int birim_sayaci = 2; gibi bir tanım taşısaydı, onu
dahil eden her birim kendi tanımını üretir ve bağlayıcı aynı ad için birden fazla
depolama alanıyla karşılaşırdı. Bildirim ile tanımın ayrılması, bu yüzden yalnız bir
sözdizimi ayrıntısı değil, çok birimli bir programın derlenebilmesinin ön koşulu.
Sınırlayıcı Ölçüm
Bir adın iki birimde aynı yazılması, o adın tek bir nesneye karşılık geldiği anlamına
gelmez. birim_sayaci gibi bir extern değişken bilerek paylaşılmak için
tasarlanmıştı; ama static bir değişken tam tersini garanti eder — kendi birimine
kapanır ve başka hiçbir birim ona doğrudan erişemez. Bu, aynı adın iki ayrı birimde iki
ayrı nesneye düşebileceği anlamına geliyor ve bunu belirleyen bağlayıcıdır.
// birim_a.c — bu birimde tanimli, disaridan erisilemeyen bir kapsam sayaci
static int kapsam_sayaci = 11;
int a_dan_oku(void) {
return kapsam_sayaci;
}
// birim_b.c — ayni adi tasiyan, ama ayri bir nesne
static int kapsam_sayaci = 22;
int b_den_oku(void) {
return kapsam_sayaci;
}
// sinirlayici.c — ayni ad iki birimde iki ayri nesne, karari baglayici verir
#include <stdio.h>
int a_dan_oku(void);
int b_den_oku(void);
int main(void) {
printf("%s %d\n", "a biriminden okunan", a_dan_oku());
printf("%s %d\n", "b biriminden okunan", b_den_oku());
return 0;
}
a biriminden okunan 11 b biriminden okunan 22
birim_a.c ve birim_b.c, ikisi de kapsam_sayaci adında bir değişken tanımlıyor, ama
static belirteci her ikisini de kendi birimine kapatıyor. Bağlayıcı bu iki tanımı asla
karşılaştırmıyor ve bir çakışma raporlamıyor — çünkü ikisi de kendi birimlerinin dışına
hiç çıkmıyor, sembol tablosunda görünmüyor. a_dan_oku yalnız kendi biriminin
kapsam_sayacisını, b_den_oku yalnız kendisininkini görebiliyor ve sonuçlar bunu
doğruluyor: 11 ve 22, birbirinden tamamen bağımsız. Bu, extern bir değişkenin
davranışının tam tersi: orada tek bir tanım vardı ve bağlayıcı bütün çağrıları o tek
tanıma bağlıyordu; burada iki ayrı tanım var ve bağlayıcı ikisini hiç karşılaştırmadan,
her birini kendi biriminde bırakıyor. Aynı ad, static belirtecine bağlı olarak, bir
bağlayıcı kararının iki tamamen farklı sonucuna yol açabiliyor.
Bu üçüncü aşamanın pratik sonucu, staticin dosya düzeyinde bir gizleme aracı olarak
kullanılabilmesidir. Bir birim, kendi iç işleyişi için bir sayaç ya da yardımcı bir
değişken tutmak isteyip bunu başka hiçbir birimin görmesini istemiyorsa, o değişkeni
static bildirir; ad geri kalan birimlerle çakışsa bile — kapsam_sayaci örneğinde
olduğu gibi — bağlayıcı bir çakışma raporlamaz, çünkü her ikisi de kendi biriminin
sınırları içinde kalır. extern bir değişken ise tam tersi bir sözleşme kurar: birimler
arasında kasıtlı olarak paylaşılan, tek bir tanıma sahip bir addır. Kaynak yazarı hangi
ismin paylaşılacağını, hangisinin kendi biriminde kalacağını static anahtar sözcüğünün
varlığıyla veya yokluğuyla belirler — ve bu tercih, birimler bağlanırken bağlayıcının
izleyeceği kuralı önceden belirlemiş olur.
Özet
- Bir adın kapsamı — nereden göründüğü — kaynağın blok yapısına bakılarak derlemede belirlenir; iç içe bloklarda aynı ad dıştakini gölgeler.
- Bir değerin ömrü — ne kadar yaşadığı — ancak koşumda gözlenebilir; otomatik
bir yerel değişken her çağrıda yeniden doğar,
staticbir yerel değişken çağrılar arasında kalıcıdır. ömürburada C’ninstorage durationkarşılığıdır; M10/K02’ninyaşam süresi(lifetime) teriminden ayrı tutulur — biri depolama süresini, öbürü sahiplik geçerliliğini ölçer.externbir değişken tek bir tanıma bağlanır ve birimler arasında paylaşılır; bu bağlanma bağlayıcı aşamasında, birimler bir araya geldiğinde çözülür.- Aynı ad iki birimde
staticolarak tanımlandığında, bağlayıcı bunları hiç karşılaştırmadan iki ayrı nesne olarak bırakır — kapsam, ömür ve bağlama üç ayrı soru olduğu için üçü de aynı anda, ama birbirinden bağımsız karara bağlanabilir. - Kapsam ile ömür bağımsız eksenlerdir: blok kapsamlı bir ad otomatik ya da kalıcı ömürlü olabilir, dosya kapsamlı bir ad ise her zaman kalıcı ömürlüdür — dosya kapsamı ile otomatik ömrün birleşimi kaynakta hiç ortaya çıkmaz.
Sonraki Adım
Bu derste ölçülen kararların hiçbiri, bir ifadenin sonucunu neyin belirlediğine
değinmedi — yalnız bir adın nereden göründüğü, bir değerin ne kadar yaşadığı ve bir
tanımın hangi birimde durduğu ölçüldü. İşleçler dersi bu zemine yeni bir soru ekliyor:
aynı işleç, işlendiği değerlerin tipine göre farklı bir sonuç üretebiliyor ve bu seçim
de veriye değil, derlemede bilinen tiplere bakılarak yapılıyor. 7 / 2 ile 7 / 2.0
aynı işleçle yazılsa da, hangi bölmenin çalışacağı işlenenlerin tipinden geliyor —
tıpkı bu derste bir adın hangi bildirime bağlandığının kaynağın yapısından gelmesi gibi.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.