İçeriğe geç
academia.sh

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, static bir yerel değişken çağrılar arasında kalıcıdır.
  • ömür burada C’nin storage duration karşılığıdır; M10/K02’nin yaşam süresi (lifetime) teriminden ayrı tutulur — biri depolama süresini, öbürü sahiplik geçerliliğini ölçer.
  • extern bir 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 static olarak 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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat