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.