İçeriğe geç
academia.sh

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.
  • #if bir 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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat