İçeriğe geç
academia.sh

Ders 03 / 13

Başlık Dosyaları

Bir başlık dosyası (header file) bir adı taşımaz, o adın metnini taşır ve ön işlemci onu her kullanan birime ayrı ayrı kopyalar. Bu, aynı bildirimin iki birime birden girmesini ve ikisinin de derlenmesini mümkün kılar; ölçüm bunu koşturarak, çifte içerme korumasının neyi engellediğini de ayrıca göstererek kanıtlıyor.

Yayın Güncelleme

Bu derste

  • Bildirim ile tanım arasındaki farkı kaynak üzerinden ayırt eder
  • Bir başlık dosyasının aynı bildirimi birden çok birime nasıl taşıdığını koşturarak gösterir
  • Çifte içerme korumasının hangi hatayı engellediğini bir örnekle açıklar
İçindekiler

Önceki ders bağlayıcı sınıfını yalnız bir çağrıyla gösterdi: kimlikin bildirimi bir dosyada, tanımı başka bir dosyadaydı ve aradaki köprüyü asama.h kurdu. Bu ders o köprünün kendisine bakıyor. Bir başlık dosyası (header file) ne bir birimdir ne de kendi başına derlenir; yalnız metin taşır ve o metin, onu #include eden her birime ayrı ayrı kopyalanır.

Bu tek cümle, kursun geri kalanında sık hatırlanacak bir gerçeği özetliyor: C’de bir birim kendi kaynağıyla #include ettiği her şeyin birleşimidir. Derleyicinin gördüğü şey, asama.c dosyasının ekranda göründüğü hâli değil, ön işlemcinin bütün #includeleri çözdükten sonra ürettiği tek parça metindir. Başlık dosyası bu birleşimin neresinde durduğunu belirler ve bu ders o konumun neden yalnız bildirimlere ayrılması gerektiğini gösteriyor.

Bildirim ile Tanım

C’de bir adın kaynakta iki farklı görünümü vardır. Bildirim (declaration) bir adın var olduğunu, tipini ve nasıl çağrılacağını söyler, ama bir gövde vermez. Tanım (definition) ise o adın karşılığını — bir işlevin gövdesini, bir değişkenin ayrılmış yerini — verir. Bir ad birden çok yerde bildirilebilir, ama yalnız bir yerde tanımlanabilir; bu ayrım, başlık dosyalarının neden var olduğunun gerekçesidir.

Bir birim, çağırdığı her adın tanımını görmek zorunda değildir; yalnız bildirimini görmesi yeter — bu, tek bir başlık dosyasının onlarca birim arasında paylaşılabilmesinin nedenidir. Derleyici bir çağrıyı üretmek için adın tipini, kaç parametre aldığını ve ne döndürdüğünü bilmelidir — bunların hepsi bildirimde vardır. Adın gerçek karşılığının nerede olduğu, derleyiciyi ilgilendirmeyen bir sorudur; o soruyu bağlayıcı yanıtlayacaktır. Bu ayrım sayesinde bir birim, hiç görmediği bir tanımı çağırabilir — yeter ki doğru bir bildirime sahip olsun.

// asama.h — baska birimlerde tanimli adlarin bildirimi
#ifndef ASAMA_H
#define ASAMA_H

/* Bu iki islev baska bir birimde tanimli: cagrilari baglama asamasinda cozulur. */
int kimlik(void);
const char *birim_adi(void);

/* Bu degisken de baska bir birimde tanimli. */
extern int birim_sayaci;

#endif

asama.h üç adı bildiriyor, hiçbirini tanımlamıyor. kimlik ve birim_adinin gövdesi yok; birim_sayacinin başında duran extern de tam olarak bunu söylüyor — “bu ad başka bir yerde tanımlı, burada yalnız bildiriliyor.” Tanımların kendisi ikinci birimde duruyor.

// secim.c — ikinci birim: tanimlar burada, cagrilar baglamada cozulur
#include "asama.h"

int birim_sayaci = 2;

int kimlik(void) {
    return 7;
}

const char *birim_adi(void) {
    return "secim";
}

Üçüncü dosya asama.hyi kendi başına, secim.cden habersiz olarak dahil ediyor ve üçünü de çağırıyor.

// asama.c — baslik dosyasinin tasidigi bildirimlerle ikinci birime baglanan kesit
#include <stdio.h>
#include "asama.h"

static void gozle(const char *ad, const char *sinif, const char *kanit) {
    printf("%-30s%-14s%s\n", ad, sinif, kanit);
}

int main(void) {
    char tampon[64];

    printf("%-30s%-14s%s\n", "gozlem", "asama", "kanit");

    gozle("baslik metni", "on islemci", "bildirimler kaynaga metin olarak kopyalandi");

    snprintf(tampon, sizeof tampon, "cagri %d verdi", kimlik());
    gozle("baska birimdeki islev", "baglayici", tampon);

    snprintf(tampon, sizeof tampon, "birim %s olarak bildirildi", birim_adi());
    gozle("baska birimdeki ad", "baglayici", tampon);

    snprintf(tampon, sizeof tampon, "sayac %d", birim_sayaci);
    gozle("baska birimdeki degisken", "baglayici", tampon);

    return 0;
}
gozlem                        asama         kanit
baslik metni                  on islemci    bildirimler kaynaga metin olarak kopyalandi
baska birimdeki islev         baglayici     cagri 7 verdi
baska birimdeki ad            baglayici     birim secim olarak bildirildi
baska birimdeki degisken      baglayici     sayac 2

DD6. asama.hnin kendisi hiçbir gözlemde on islemci etiketiyle görünmüyor; onun yerine görünen baslik metni satırı bir sonuçtur, asama.hnin kendisi değil. Ön işlemcinin yaptığı iş bir metin kopyalamaktan ibarettir: #include "asama.h" satırı, derleyici devreye girmeden önce, asama.hnin içeriğiyle değiştirilir. asama.cde görünen kimlik(void) bildirimi bu yüzden hiçbir zaman asama.cnin kaynağında elle yazılmadı — kopyalanarak oraya geldi. Aynı satır secim.cnin kaynağına da kopyalandı; iki kopya birbirinden bağımsız, ikisi de kendi biriminin bir parçası oldu. Bir dosyayı düzenlerken görünen #include satırı, derlenen metnin yalnız bir özetidir — gerçek kaynak, ön işlemci çalıştıktan sonra ortaya çıkan, gözle hiç görülmeyen genişletilmiş metindir.

secim.c de asama.hyi dahil ediyor ve aynı üç bildirim onun kaynağına da kopyalanıyor. İki birim de bu bildirimlerle derleniyor ve ikisi de uyarısız derleniyor — aynı bildirim iki ayrı yere girdi ve ikisi de bundan rahatsız olmadı, çünkü bildirim bir söz verir, bir yer ayırmaz. Tanım ise yalnız secim.cde bir kez var; asama.c üç adı da kullanıyor ama hiçbirinin tanımını taşımıyor, tanımları bağlama aşamasında ödünç alıyor.

Çıktıdaki dört satır bu ödünç almanın nasıl işlediğini gösteriyor. baslik metni satırı kaynağın kendisiyle ilgili bir gözlem, çünkü tek bir metnin bildirim taşıdığını kanıtlıyor — ama önemli olan asama.cnin bu üç adı kullanabilmesi. baska birimdeki islev ve baska birimdeki ad satırları secim.cnin işlev tanımlarına ulaşıyor; baska birimdeki degisken satırı ise bir değişken tanımına ulaşıyor. Üçü de aynı köprüyü kullanıyor: asama.hdeki bildirim, asama.cye “bu ad var, işte tipi” diyor; gerçek karşılığı yalnız bağlama sırasında geliyor. asama.c tek başına derlenirken birim_sayacinin değerinin 2 olduğunu bile bilmiyor — bildiği yalnız böyle bir tam sayının bir yerde var olduğu.

Başlık Neyi Paylaşır, Neyi Paylaşmaz

asama.hnin yalnız bildirim taşıması bir seçim değil, bir zorunluluktur. Bir başlık dosyası birden çok birime dahil edilebildiği için, içine bir işlev tanımı konsaydı — gövdesiyle birlikte — o gövde her onu dahil eden birime ayrı ayrı kopyalanırdı. asama.hyi hem asama.c hem secim.c dahil ediyor; eğer kimlikin gövdesi orada dursaydı, iki birim de aynı adı tanımlamış olur ve bağlama aşamasında “aynı ad iki kez tanımlı” çakışması çıkardı. Tanımın başlıkta değil, yalnız bir birimde durması bu yüzden zorunludur — çifte içerme koruması bunu tek bir birim içinde önler, ama bir tanımın birden çok birime yayılmasını önlemez; onu önleyen şey, tanımı hiç başlığa koymamaktır.

Bu, extern int birim_sayaci; satırının neden int birim_sayaci = 2; olarak yazılmadığını da açıklıyor. extern anahtar sözcüğü derleyiciye “burada yer ayırma, bu ad başka bir yerde zaten var” der; onsuz yazılan bir değişken bildirimi aslında bir tanımdır ve bir yer ayırır. Başlığa externsiz bir değişken konsaydı, onu dahil eden her birim kendi kopyasını ayırırdı — asama.c ile secim.c aynı adı taşıyan ama farklı adreslerde duran iki ayrı birim_sayaci elde ederdi, bu da bağlama aşamasında ya çakışma ya da sessiz bir tutarsızlık doğururdu.

Bu kural, işlev bildirimleri için doğrudan geçerli değildir; bir işlev bildirimi zaten kendi başına bir yer ayırmaz, yalnız externli bir değişken bildirimiyle aynı rolü oynar. Kuralın özü şu: bir başlığa konan her satır, onu okuyan her birim tarafından aynı şekilde yorumlanmalıdır. Bir bildirim bu koşulu doğal olarak sağlar, çünkü kopyalandığı her yerde aynı sözü verir; bir tanım sağlamaz, çünkü kopyalandığı her yerde ayrı bir yer talep eder.

Çifte İçerme Koruması

Bir başlık dosyası aynı birime birden fazla yoldan girebilir — doğrudan, ya da başka bir başlık dosyasının içinden. Bu, tek bir dosyanın iki kez #include edilmesi kadar açık bir hata olmak zorunda değil; çoğu zaman iki ayrı başlık, üçüncü ortak bir başlığa aynı anda bağımlı olduğunda kendiliğinden ortaya çıkar. Bir birim ikisini de dahil ettiğinde, ortak başlık o birime iki kez girmiş olur. asama.hnin başındaki #ifndef / #define çifti tam olarak bunu karşılamak için var. Aşağıdaki küçük örnek bunu, asama.hden bağımsız bir başlıkla gösteriyor.

// sinir.h — bir tip bildiren kucuk baslik; cifte icerme korumasi tasiyor
#ifndef SINIR_H
#define SINIR_H

typedef struct {
    int satir;
    int sutun;
} Konum;

#endif
// giris.h — sinir.h'yi bir yoldan ceken ara baslik
#include "sinir.h"
// cikis.h — sinir.h'yi baska bir yoldan ceken ikinci ara baslik
#include "sinir.h"
// baglam.c — sinir.h iki ayri yoldan ayni birime giriyor
#include <stdio.h>
#include "giris.h"
#include "cikis.h"

int main(void) {
    Konum k = {3, 7};
    printf("konum: satir %d sutun %d\n", k.satir, k.sutun);
    printf("baslik iki yoldan cekildi, tip bir kez tanimlandi\n");
    return 0;
}
konum: satir 3 sutun 7
baslik iki yoldan cekildi, tip bir kez tanimlandi

DD7. baglam.c sinir.hyi hiç doğrudan dahil etmiyor — giris.h ve cikis.h üzerinden, iki ayrı yoldan dolaylı olarak çekiyor. Ön işlemci her iki #includei de sırayla işler: birincisinde SINIR_H henüz tanımlı olmadığı için Konum tipi kaynağa kopyalanır ve SINIR_H tanımlanır; ikincisinde #ifndef SINIR_H artık yanlıştır ve #endife kadar olan her şey atlanır — Konum bir daha kopyalanmaz. Koruma olmasaydı Konum aynı birimde iki kez tanımlanmış olurdu ve bu derlenmezdi. Buradaki yapı etiketsizdir: C, etiketsiz her yapı bildirimini ayrı bir tip sayar, bir typedef adı ise ancak aynı tipe yeniden bağlanabilir. İkinci kopya Konumu ikinci ve farklı bir anonim tipe bağlamaya çalışır, kural çiğnenir. Tanım, bildirimin aksine, tekil olmak zorundadır.

Bu, #ifndef ailesinin bildirimle değil tanımla ilgili bir sorunu çözdüğünü gösteriyor. kimlikin bildirimi iki birime kopyalanabiliyordu, çünkü bildirim çoğaltılabilir. Konumun tanımı ise aynı birime iki kez kopyalanamaz, çünkü tanım tekildir — tıpkı bir işlevin gövdesi gibi. Çifte içerme koruması, bir başlığın kendi içindeki tanımları bu tekillik kuralından korur.

DD8. SINIR_H adının kendisinin özel bir anlamı yok; yalnız “bu başlık daha önce kopyalandı mı” sorusuna yanıt veren bir işaretçi görevi görüyor. Ön işlemcinin gördüğü şey bir dosya adı ya da bir yol değil, yalnız bu adın tanımlı olup olmadığıdır — #define SINIR_H satırı çalıştığı andan itibaren, aynı birimdeki her sonraki #ifndef SINIR_H yanlış sonuç verir. Koruma bu yüzden dosyanın kendisine değil, kaynağa bırakılan bu tek işarete dayanır; adı çakışan başka bir başlık aynı işareti kullansaydı, koruma yine çalışır ama bu kez yanlış başlığı dışarıda bırakırdı. Adın başlığa özgü ve tekil olması bu yüzden programcının sorumluluğudur, dilin garantisi değildir.

giris.h ve cikis.hnin kendileri hiçbir yeni bildirim taşımıyor; ikisi de yalnız sinir.hye birer köprü. Bu, gerçek başlık ağaçlarında sık görülen bir örüntüyü küçük ölçekte yansıtıyor: bir başlık kendi ihtiyaç duyduğu başka başlıkları dahil eder ve onu kullanan birim, bu iç bağımlılıkların farkında olmak zorunda kalmaz. baglam.c sinir.hnin var olduğunu bile bilmeden Konum tipine erişti — giris.h ve cikis.hnin ikisi de bu bağımlılığı kendi başına taşıdı.

Sınırlayıcı Ölçüm

Başlık dosyası bir sözleşme değildir. asama.hdeki bildirim secim.cdeki tanımla birebir örtüşüyor çünkü bu çekirdekte ikisi elle tutarlı tutuldu — ama dilin kendisi bu tutarlılığı denetlemez. asama.h int kimlik(void); yerine yanlışlıkla int kimlik(int); bildirseydi, secim.cdeki tanım hâlâ int kimlik(void) { return 7; } olurdu. asama.cyi derleyen derleyici yalnız asama.hdeki bildirime bakar ve çağrıyı ona göre üretir; secim.cyi derleyen derleyici de yalnız kendi tanımına bakar. İkisi de kendi biriminin içinde tutarlıdır, ikisi de tek başına uyarısız derlenir — çünkü derleyici aynı anda iki birimi göremez, bu kursun ilk dersinden beri bilinen bir sınır.

Uyuşmazlık ancak iki birim bir araya gelince, yani bağlama sırasında ortaya çıkabilir. Ama klasik bir C bağlayıcısı bir adın hangi imzayla bildirildiğini değil, yalnız adın var olup olmadığını denetler — sembol tablosunda kimlik adını arar, bulur, adresi eşler. İmzanın uyuşup uyuşmadığı çoğu zaman bağlayıcının da göremediği bir şeydir; hata orada da yakalanmaz, tanımsız davranışa düşer. Çekirdekte durum böyle değildir: asama.hdeki bildirim ile secim.cdeki tanım birebir örtüşüyor ve bu dersteki iki koşum da bunu zaten kanıtladı — hem kimlik çağrısı hem Konum yapısı beklenen değerleri verdi. Başlık dosyası bu tutarlılığı sağlamaz, yalnız onu elle sağlanmışsa taşır.

Bu sınır, önceki dersin ölçtüğü “aşamalar geri dönmez” bulgusunun bir başka görünümü. Ön işlemci bir başlığı kopyalarken metnin doğru olup olmadığını sormaz, yalnız kopyalar. Derleyici kopyalanan metni kendi biriminin içinde tutarlı bulduğu sürece bir şey söylemez. Uyumsuzluk, iki birim ancak bağlanınca — ve o zaman bile yalnız bazı durumlarda — görünür hale gelir. Bu yüzden bir başlık dosyasının doğruluğu, dilin denetlediği bir şey değil, onu yazanın kendi disipliniyle koruduğu bir şeydir: bildirim değişince tanım da değişmeli, ikisi ayrı dosyalarda dursa bile.

Bu disiplinin nasıl korunacağı ayrı bir konu — derleme sistemleri ve bağımlılık takibi sonraki müfredatların konusu — ama bu dersin sınırı şunu söylüyor: asama.h var olduğu için kimlikin imzası otomatik olarak doğru kalmıyor, yalnız tek bir yerden değiştirilebiliyor. Başlık dosyasının asıl kazandırdığı şey doğruluk garantisi değil, bu tekillik; bir bildirim değiştiğinde onu kullanan bütün birimlerin aynı metni görmesi.

Özet

  • Bir ad kaynakta iki ayrı biçimde görünür: bildirim (declaration) yalnız var olduğunu söyler, tanım (definition) gövdesini ya da yerini verir.
  • Bir bildirim birden çok birime kopyalanabilir; bir tanım yalnız bir birimde bulunabilir.
  • Başlık dosyası bir birim değildir; yalnız metin taşır ve onu dahil eden her birime ayrı ayrı kopyalanır.
  • Çifte içerme koruması (#ifndef / #define) aynı başlığın bir birime birden fazla yoldan girip bir tanımı iki kez kopyalamasını engeller.
  • Başlık dosyası bir sözleşme değildir: bildirim ile tanım uyuşmazsa derleyici bunu göremez, çünkü tek seferde tek birime bakar; bağlayıcı da yalnız adı arar, imzayı değil.
  • Bir başlığa tanım değil bildirim konması bir üsluptan çok bir zorunluluktur: aynı tanım onu dahil eden her birime kopyalanırsa, bu birimler bağlanırken çakışma doğar.

Sonraki Adım

Bu ders baslik metni satırının arkasındaki mekanizmayı açtı: ön işlemci bir dosyayı kopyalıyor, ama kaynağın geri kalanı bu kopyalamanın yapıldığını hiçbir şekilde bilmiyor. Kaynakta duran #include "asama.h" satırı ile derleyicinin gördüğü genişletilmiş metin arasındaki fark, ön işlemcinin bütün kararlarının ortak özelliği: hiçbiri kaynakta iz bırakmaz, yalnız sonucuyla var olur. Sıradaki ders ön işlemcinin geri kalan üç kararına dönüyor — makro değeri, koşullu gövde ve ad birleştirme — ve her birinin metin düzeyinde neden derleyiciden tamamen bağımsız çalıştığını, önceki dersten daha derin bir bakışla gösteriyor.

İ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