Ders 04 / 13
Ön İşlemci
Ön işlemcinin dört farklı yönergesi — makro değeri, koşullu gövde, ad birleştirme, içerme — aslında tek bir işin dört görünümüdür: metin ikamesi. Makro tip bilmediği için aynı ifadeyi iki farklı tiple doğru sonuç verecek şekilde üretir; ama argümanını birden çok kez yerine koyabildiği için yan etkili bir çağrıyı gerçek bir işlevden farklı sayıda çalıştırabilir.
Yayın Güncelleme
Bu derste
- Ön işlemcinin dört farklı yönergesinin ortak işleyişini (metin ikamesi) tanımlar
- Bir makronun tip bilmediğini, aynı tanımın iki farklı tiple çalıştığını göstererek kanıtlar
- Makro argümanının birden çok kez yerine konabileceğini ve bunun riskini bir örnekle gösterir
İçindekiler
Önceki ders baslik metni satırının arkasındaki mekanizmayı açtı ve ön işlemcinin bir
dosyayı kopyaladığını, ama kaynağın geri kalanının bu kopyalamadan habersiz kaldığını
gösterdi. Bu ders aynı gözlemi ön işlemcinin öteki üç kararına — makro değeri, koşullu
gövde, ad birleştirme — taşıyor ve dördünün de aslında tek bir işin görünümü olduğunu ortaya
koyuyor: kaynaktaki bir metni, derleyici hiç haberdar olmadan, başka bir metinle değiştirmek.
Bu dört karar kursun ilk dersinden beri tek tek görüldü: KAT metni, #ifdef AYRINTILI
seçimi, BIRLESTIR(topla, _uc) adı, asama.hnin kopyalanması. O derste dördü de yan yana,
tek bir koşumun parçası olarak geçti. Bu ders her birine ayrı ayrı döner ve ortak paydayı
gösterir: dördü de derleyici hiç devreye girmeden, salt metin üzerinde çalışır.
Dördü de Metin İkamesi
Ön işlemcinin bütün yönergeleri tek bir işi yapar: kaynaktaki bir metni başka bir metinle değiştirmek.
| Yönerge | Neyi neyle değiştirir |
|---|---|
#define AD deger |
AD geçtiği her yeri deger ile |
#define AD(x) govde |
AD(gercek) çağrısını govde içindeki xler gercek ile değiştirilmiş haliyle |
a##b |
İki token’ı tek bir yeni token ile |
#ifdef / #if |
Bir kod bloğunu ya kendisiyle ya da boş metinle |
#include "d.h" |
Bir satırı, dosyanın tüm içeriğiyle |
Beşi de aynı sınıfa girer, çünkü beşi de derleyici devreye girmeden, kaynağın metnini değiştirir. Aralarındaki fark yalnız neyi neyle değiştirdikleridir — bir ad mı, bir çağrı mı, iki token mı, bir kod bloğu mu, yoksa bir dosyanın tamamı mı.
Bu ortak paydanın bir sonucu var: ön işlemcinin ürettiği metin, derleyici için kaynaktan elle yazılmış metinden ayırt edilemez. Derleyici bir satırın bir makrodan mı geldiğini, bir başlıktan mı kopyalandığını, yoksa doğrudan mı yazıldığını bilmez ve bilmesine gerek de yoktur — önündeki metin ne ise, onu derler. Bu görünmezlik hem ön işlemcinin gücünün hem sınırlarının kaynağı: bir yandan kaynağı programcının elle yazmayacağı kadar tekrarlı biçimlerde üretebiliyor, öte yandan ürettiği metnin doğruluğunu kendisi denetlemiyor.
Tablodaki beşinci satır — #include — önceki dersin bütün konusuydu, ama orada bir
paylaşım aracı olarak görüldü. Burada aynı yönergeye ikame ailesinin bir üyesi olarak
bakılıyor: #include "asama.h" satırı da tıpkı bir makro çağrısı gibi, kendi yerine başka
bir metni koyar. Aradaki tek fark ölçek — bir makro birkaç token’ı değiştirirken, bir
#include bütün bir dosyayı değiştirir. Ön işlemcinin gözünden ikisi arasında bir fark
yoktur; ikisi de aynı mekanizmanın, metni okuyup başka bir metinle değiştirmenin, farklı
boyutlardaki uygulamalarıdır.
Makro Tip Bilmez
Bir makro parametre alsa bile bir işlev değildir; parametre burada bir tip bildirmez, yalnız gövde içinde nereye yapıştırılacağını gösteren bir yer tutucudur. Bu yüzden aynı makro, farklı tiplerdeki argümanlarla doğru sonuç üretebilir. Gerçek bir C işlevi böyle davranamaz: bir işlevin her parametresi tek bir tiple bildirilir ve çağrıda verilen argüman o tipe dönüştürülür — dönüşüm bazen sessiz, bazen bilgi kaybıyla olur. Makronun parametresi bu dönüşümden tamamen bağışıktır, çünkü dönüştürülecek bir tip zaten yoktur.
// hesap.c — ayni makro iki farkli tiple cagriliyor
#include <stdio.h>
#define KARE(x) ((x) * (x))
int main(void) {
int tam = KARE(4);
double kesirli = KARE(2.5);
printf("tam: %d\n", tam);
printf("kesirli: %.2f\n", kesirli);
return 0;
}
tam: 16 kesirli: 6.25
DD9. KARE(4) ön işlemcide ((4) * (4)) metnine, KARE(2.5) ise ((2.5) * (2.5))
metnine dönüşüyor — ikisi de aynı gövdeden, xin yerine konan tek token değişerek üretildi.
Ön işlemci bu değişimi yaparken 4ün bir tam sayı, 2.5in bir kayan noktalı sayı olduğunu
hiç bilmiyor; onun gördüğü yalnız iki ayrı metin parçası. Sonucun int mi double mı
olacağına derleyici karar veriyor — ama bu karar, makronun genişlemesinden sonra, tıpkı
kaynağa elle ((4) * (4)) ve ((2.5) * (2.5)) yazılmış gibi veriliyor.
Bu, KAREin neden bir tip parametresi almadığını da açıklıyor: alamaz, çünkü zaten tip diye
bir kavramı yok. Aynı gövde her tipe uyuyor, çünkü hiçbir tipe özel yazılmadı — yalnız
metne yapıştırıldı.
Bir gerçek işlev aynı işi yapamaz — en azından tek bir tanımla. int kare(int x) yazılırsa
yalnız int argümanlarla çalışır; KARE(2.5)e benzer bir çağrı, 2.5i sessizce inte
kesip 2nin karesini, 4ü verirdi. KARE makrosunun int ve double ile aynı gövdeyle
doğru sonuç vermesi bu yüzden bir kolaylık değil, tipin ön işlemci aşamasında hiç var
olmamasının doğrudan sonucu. Aynı esneklik bir bedel taşır: makro, argümanına hangi tipin
geleceğini denetleyemez, çünkü denetleyecek bir tip sistemi yoktur — KARE("metin") yazılsa
bile ön işlemci bunu reddetmez, yalnız (("metin") * ("metin")) metnini üretir ve
sorunu derleyiciye devreder.
Koşullu Gövde: Bir Adım İleri
Önceki dersler koşullu gövdeyi yalnız bir adın tanımlı olup olmadığına (#ifdef)
bakarak seçti. #if bundan bir adım ilerisini yapar: bir tam sayı sabit ifadesinin
değerini ön işlemcide hesaplar. Bu, ön işlemcinin tamamen boş bir kavrayışa sahip
olmadığını gösteriyor — metni okurken bazı basit hesapları da yapabiliyor, ama bu hesap
derleyicinin zengin tip sisteminden çok daha dar bir aritmetiktir.
// esik.c — #if bir tam sayi sabit ifadesini on islemcide degerlendirir
#include <stdio.h>
#define ESIK 5
#if ESIK > 3
static const char *sonuc = "esik 3'ten buyuk";
#else
static const char *sonuc = "esik 3'ten kucuk ya da esit";
#endif
int main(void) {
printf("%s\n", sonuc);
return 0;
}
esik 3'ten buyuk
DD10. #if ESIK > 3 derleyicinin > işlecini kullandığı yerle aynı sözdizimini taşısa
da, aynı hesap makinesini kullanmaz. Ön işlemcinin kendi aritmetiği vardır: bütün tam sayılar
tek bir geniş tipe genişletilir, kayan noktalı sayı hiç yoktur ve tanımsız bir ad 0 olarak
okunur. ESIKin değeri önce 5 metnine açılır, sonra 5 > 3 ön işlemcinin kendi
aritmetiğiyle hesaplanır ve sonuç dogru ya da yanlisa indirgenir. Derleyici bu hesabı hiç
görmez; onun önüne yalnız seçilen dal — burada static const char *sonuc = "esik 3'ten buyuk"; satırı — gelir.
Bu ayrım #ifdef ile #if arasındaki farkı da netleştiriyor. #ifdef AD, ADin tanımlı
olup olmadığına bakar — değerine hiç bakmaz; AD 0 olarak tanımlansa bile #ifdef AD
doğrudur. #if AD, tam tersine, ADin değerini ön işlemcinin kendi aritmetiğiyle
hesaplar; AD 0sa #if AD yanlıştır. Kaynakta bu iki yönergeyi karıştırmak, tanımlı ama
sıfır bir sabitle çalışırken sessizce yanlış dalı seçtirebilir — bu ders bu iki yönergeyi
birbirinin yerine kullanmaz, ama aradaki farkı bilerek seçer.
#ifin kendi aritmetiği sayesinde yapabildiği bir şey daha var: birden çok koşulu &&,
|| ve karşılaştırma işleçleriyle tek bir satırda birleştirmek, ya da #elif ile art arda
sıralanan alternatiflerden ilk doğru olanı seçmek. #ifdef bunu yapamaz — yalnız tek bir
adın tanımlı olup olmadığını sorabilir, tanımlı iki addan hangisinin daha büyük bir sayıya
karşılık geldiğini soramaz. Bu, iki yönergenin neden birbirinin yerini tutmadığını bir kez
daha gösteriyor: biri varlığı sorar, öbürü değeri hesaplar.
Ad Birleştirme: Aynı Gövdeden Farklı Adlar
Önceki derste ## tek bir örnekte görüldü: iki sabit token’ı (topla ve _uc) birleştirip
tek bir ad üretti. Asıl gücü orada görünmedi, çünkü o çağrı yalnız bir kezdi. ##nin gerçek
gücü, aynı makro gövdesinin farklı çağrılarda farklı adlar üretebilmesidir — bir makro
gövdesi, kaynakta yalnız bir kez yazılıp defalarca çağrılarak, her seferinde yeni ve
birbirinden bağımsız bir ada dönüşebilir.
// uret.c — ayni makro govdesi iki farkli cagriyla iki farkli isleve donusuyor
#include <stdio.h>
#define ISLEV_URET(ad) int uret_##ad(int x) { return x + 1; }
ISLEV_URET(a)
ISLEV_URET(b)
int main(void) {
printf("uret_a(5) = %d\n", uret_a(5));
printf("uret_b(5) = %d\n", uret_b(5));
return 0;
}
uret_a(5) = 6 uret_b(5) = 6
DD11. ISLEV_URET(a) satırı ön işlemcide int uret_a(int x) { return x + 1; } metnine,
ISLEV_URET(b) satırı ise int uret_b(int x) { return x + 1; } metnine dönüşüyor. Derleyici
ISLEV_URET diye bir ad hiç görmez; onun gördüğü kaynakta iki ayrı işlev tanımı var, ikisi de
elle yazılmış gibi duruyor. Aynı makro gövdesi, argümanı değiştirilerek istenildiği kadar
farklı işlev üretebilir — ama her üretilen ad, gövdenin geri kalanıyla birlikte tek bir
metin parçası olarak derleyiciye gider; ikisi arasında hiçbir bağ, hiçbir “aynı makrodan
geldi” bilgisi taşınmaz.
Kursun ilk dersindeki BIRLESTIR(topla, _uc) de aynı işlemi yapıyordu, ama tek bir çağrıyla.
Buradaki fark, aynı üretici gövdenin birden çok kez çağrılabilmesi — ISLEV_URET iki
kez kullanıldı ve iki farklı işlev doğdu. Kaynağa üçüncü bir ISLEV_URET(c) satırı
eklenseydi, derleyici üçüncü bir işlev daha görürdü; ad birleştirmenin ürettiği metin
miktarı, makronun kaç kez çağrıldığına bağlıdır, kaynağın uzunluğuna değil.
Sınırlayıcı Ölçüm
Bu dersin dört bölümü de makronun tip bilmediğini, koşullu gövdenin kendi aritmetiğiyle
karar verdiğini, ad birleştirmenin yeni tanımlayıcılar üretebildiğini gösterdi — üçü de
ön işlemcinin gücünü anlatıyordu. Bu son bölüm aynı gücün bir bedelini gösteriyor.
Makro bir işlev değildir; argümanı birden çok kez yerine konabilir. KARE(x) gövdesinde
x iki kez geçiyor — bu, argüman yan etkili bir ifadeyse iki kez çalıştırılacağı anlamına
gelir. Aynı işi yapan gerçek bir işlevde argüman her zaman bir kez hesaplanır.
// karsilastir.c — makro arguman iki kez, islev arguman bir kez calistiriyor
#include <stdio.h>
#define KARE(x) ((x) * (x))
static int sayac = 0;
static int artir(void) {
sayac++;
return sayac;
}
static int kare(int x) {
return x * x;
}
int main(void) {
sayac = 0;
int makro_sonuc = KARE(artir());
int makro_sayac = sayac;
sayac = 0;
int islev_sonuc = kare(artir());
int islev_sayac = sayac;
printf("makro: sonuc %d, sayac %d kez arttirildi\n", makro_sonuc, makro_sayac);
printf("islev: sonuc %d, sayac %d kez arttirildi\n", islev_sonuc, islev_sayac);
return 0;
}
makro: sonuc 2, sayac 2 kez arttirildi islev: sonuc 1, sayac 1 kez arttirildi
DD12. KARE(artir()) ön işlemcide ((artir()) * (artir())) metnine dönüşüyor — kaynakta
artir() çağrısı artık iki kez yazılı. Program çalışırken bu iki çağrı da gerçekten
yapılıyor: sayac sıfırdan başlayıp önce 1e, sonra 2ye çıkıyor, çarpım 1 * 2 = 2
veriyor. Hangi çağrının önce çalıştığı derleyiciye bağlıdır ve bu ders onu hiç iddia etmiyor
— iddia edilen tek şey, iki çağrının da gerçekleştiği ve sayacın iki kez arttığıdır. kare
çağrısında ise artir() yalnız bir kez yazılı ve yalnız bir kez çalışıyor: argüman önce
hesaplanır, sonucu kareye bir kez geçirilir, x gövde içinde ne kadar geçerse geçsin
artık sabit bir değerdir.
Bu fark, KARE gibi bir makroyu yan etkili bir argümanla çağırmanın neden riskli olduğunu
gösteriyor: makro yazarı argümanın kaç kez kullanılacağını gövdeye bakarak bilir, ama
çağıran taraf bunu görmeden, sıradan bir işlevmiş gibi çağırabilir. KAREin adı ve
kullanım biçimi bir işlevinkiyle aynı, ama sözleşmesi farklıdır — argüman yan etkisiz bir
ifade olmalıdır.
KARE(x) gövdesindeki (x) çevresindeki parantezlerin bu riskle bir ilgisi yok; onlar
ayrı bir sorunu çözüyor. Parantezsiz bir gövde (x * x) KARE(1 + 2)yi 1 + 2 * 1 + 2
metnine açardı; çarpma toplamadan önce işlendiği için bu ifade 1 + 2 + 2 = 5 verir,
oysa beklenen 3ün karesi, yani 9dur — çarpma önceliği, toplamanın parçalarını
kendine argüman sanır. Parantezli gövde bunu önler: ((1 + 2) * (1 + 2)) önce iki parantezi
kendi bütünlüğü içinde değerlendirir, sonra çarpar, sonuç doğru biçimde 9 çıkar. Ama bu
parantezler argümanın kaç kez yazıldığını değiştirmez — ((1 + 2) * (1 + 2)) hâlâ
1 + 2yi iki kez taşıyor. İki sorun da aynı kökten gelir — argüman metne yerine konurken
çevresiyle etkileşir — ama çözümleri ayrıdır: parantez öncelik sırasını korur, argümanı
yan etkisiz tutmaksa çağıranın sorumluluğudur.
Özet
- Ön işlemcinin bütün yönergeleri tek bir işi yapar: kaynaktaki bir metni başka bir metinle değiştirmek; makro değeri, koşullu gövde, ad birleştirme ve içerme bunun dört görünümüdür.
- Bir makro tip bilmez: aynı gövde, farklı tiplerdeki argümanlarla, ön işlemci hiç değişmeden, doğru sonuç üretebilir.
#ifbir tam sayı sabit ifadesini ön işlemcinin kendi aritmetiğiyle hesaplar; bu aritmetik derleyicinin tip sistemiyle aynı değildir.- Aynı makro gövdesi, ad birleştirmeyle farklı çağrılarda farklı adlar üretebilir; derleyici bu adların ortak bir kaynaktan geldiğini bilmez.
- Bir makro argümanını birden çok kez yerine koyabilir; yan etkili bir argüman bu yüzden gerçek bir işlevden farklı sayıda çalışabilir.
- Bir makro gövdesindeki parantezler öncelik sırasını korur, ama argümanın kaç kez yazıldığını değiştirmez — iki ayrı sorunun iki ayrı çözümüdür.
Sonraki Adım
Bu dört ders boyunca hiçbir kod bloğu bir uyarı bırakmadı — ama bu bir tesadüf değil, elle sağlanan bir tutarlılıktı: her makro dikkatle parantezlendi, her koşullu dal tutarlı bir tipte tutuldu, her ad birleştirme geçerli bir tanımlayıcıyla sonuçlandı. Ön işlemci bu tutarlılığın hiçbirini denetlemez; onu denetleyen, kendisinden bir sonraki aşamadır. Sıradaki ders bu denetimin kendisine bakıyor: bir uyarının hangi aşamada doğduğu, bir derleyici uyarı bayrağının neyi görünür kıldığı ve standardın hangi davranışı garanti edip hangisini uygulamaya bıraktığı. Bu konunun son dersi burada bu kursun açılışında sorulan soruya döner: dört aşamanın her biri kendi payına düşen hatayı nasıl görünür kılar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.