Ders 10 / 13
Fonksiyonlar
Bir çağrının başka bir birimdeki tanıma bağlanmasının bağlayıcı aşamasında çözülmesi; bildirim-tanım ayrımı, parametre aktarımının her zaman kopya olması ve dönüş değerinin tipinin çağıran tarafın yorumunu belirlemesi.
Yayın Güncelleme
Bu derste
- Bir çağrının başka birimdeki tanıma bağlanmasının bağlayıcı aşamasında çözüldüğünü ölçer
- Bildirim ile tanımın ayrılmasını ve parametre aktarımının her zaman kopya olduğunu gösterir
- Dönüş değerinin tipinin çağıranın yorumunu nasıl belirlediğini örnekle ayırt eder
İçindekiler
Denetim Akışı dersi, hep aynı işlevin içinde kalan bir soruyla ilgilendi: hangi dal seçilecek. Bu ders o sınırı kaldırıyor ve bir çağrının işlev sınırını nasıl geçtiğini ölçüyor. Bir işlev çağrıldığında üç ayrı soru birden çözülüyor: çağrının hangi tanıma bağlanacağı, argümanların işleve nasıl ulaştığı ve dönen değerin nasıl yorumlanacağı — üçü de aynı anda, ama farklı gerekçelerle karara bağlanan sorular. İlki bağlayıcıya aittir, çünkü birden çok birim gerektirir; öbür ikisi ise derleyicinin tek bir birime bakarak verdiği kararlardır — bir çağrının tam olarak nereye bağlanacağını bilmese bile, derleyici argümanların nasıl kopyalanacağını ve dönüşün nasıl yorumlanacağını bildirimden çıkarabilir.
Programlama Temelleri kursu (M01/K02) fonksiyon kavramını Python’la zaten kurmuştu; orada bir işlevin çağrılabilmesi için önce tanımlanmış olması yeterliydi ve bildirim-tanım ayrımı yoktu. C’nin karşılığı burada ölçülüyor: bir çağrı, tanımı hiç görmeden, yalnız bir bildirime dayanarak derlenebiliyor — tanımın kendisi başka bir birimde durabiliyor ve iki tarafı birbirine bağlayan adım ayrı bir aşamaya ait. Bu derste ölçülen üç karar — bağlanma, aktarım, dönüş — bir işlevin çağrılma yüzeyinin üç ayrı bileşeni ve her biri kendi aşamasında, birbirinden bağımsız olarak sabitleniyor.
Bağlayıcı: Başka Birimdeki İşlev
Aşağıdaki kesit, Derleme Aşamaları dersinde tanıtılan çekirdeğin bir parçasıdır: bir işlev bir birimde çağrılıyor, tanımı ise başka bir birimde duruyor.
// asama.h — baska birimlerde tanimli adlarin bildirimi (kesit: islev bildirimi) #ifndef ASAMA_H #define ASAMA_H /* Bu islev baska bir birimde tanimli: cagrisi baglama asamasinda cozulur. */ int kimlik(void); #endif
// secim.c — ikinci birim: islevin tanimi burada durur
#include "asama.h"
int kimlik(void) {
return 7;
}
// asama.c — baska birimdeki islevin baglanmasi (kesit)
#include <stdio.h>
#include "asama.h"
int main(void) {
printf("%s %d\n", "baska birimdeki islev", kimlik());
return 0;
}
baska birimdeki islev 7
TS8. asama.c derlenirken kimlikin gövdesi hiçbir yerde görünmüyor — derleyici
yalnız asama.hdeki bildirimi görüyor ve bu bildirimden, çağrının doğru sayıda ve doğru
tipte argüman aldığını, dönüş tipinin int olduğunu denetleyip nesne dosyasına bir
“bu ada ihtiyacım var” notu bırakıyor. secim.c ayrı ayrı derleniyor ve kendi nesne
dosyasına kimlikin gerçek gövdesini yazıyor. İki nesne dosyası bir araya getirildiğinde
bağlayıcı, asama.cnin bıraktığı notu secim.cnin sağladığı gövdeyle eşleştiriyor —
bu eşleşme olmadan çalıştırılabilir bir program hiç oluşmaz. Kapsam ve ömür bir birimin
içinde kalan sorulardı; bir çağrının hangi gövdeye bağlanacağı ise ancak birimler bir
araya geldiğinde, bağlayıcı aşamasında çözülüyor.
Bu üç aşamalı akış — önce iki birim ayrı ayrı derlenir, sonra bağlayıcı ikisini
birleştirir — Derleme Aşamaları dersinde ölçülen genel düzenin işlevlere uygulanmış
hâli. asama.cnin derlenmesi secim.cnin var olup olmadığından habersiz ilerler;
derleyici yalnız asama.hdeki bildirime bakarak “bu isimde, bu imzada bir işlev bir
yerlerde tanımlı olmalı” varsayımıyla çalışır ve bu varsayımı doğrulamak bağlayıcının
işidir. Eğer secim.c hiç derlenmemiş ya da kimlik tanımı yanlışlıkla silinmiş olsaydı,
asama.cnin derlenmesi yine sorunsuz tamamlanırdı — hata ancak bağlama adımında,
bağlayıcı kimlik adını hiçbir nesne dosyasında bulamadığında ortaya çıkardı. Bu, bir
derleme hatasıyla bir bağlama hatasının neden farklı aşamalarda, farklı zamanlarda
ortaya çıkabildiğini gösteriyor.
Bu düzenek, Değişkenler, Kapsam ve Ömür dersinde ölçülen extern değişken bağlanmasıyla
aynı ailededir: orada bir adın hangi depolama alanına, burada bir adın hangi
gövdeye karşılık geldiği bağlayıcıda çözülüyor. Fark, işlevlerin varsayılan olarak
dış bağlantılı olması — bir değişkeni paylaşmak için extern yazmak gerekirken, bir
işlev static yazılmadıkça zaten birimler arasında görünür ve çağrılabilir durumdadır.
kimlik işlevi static olmadığı için secim.cdeki tanımı asama.cden görülebiliyor;
işlev static kimlik(void) diye tanımlansaydı, kendi birimine kapanır ve asama.cnin
derlenmesi, bağlayıcının aynı adı bulamaması yüzünden başarısız olurdu.
Bu, static anahtar sözcüğünün işlevler için de değişkenlerdekiyle aynı işlevi
gördüğünü doğruluyor: kapsamı dosya sınırına daraltıyor ve bağlayıcının o adı başka
birimlere göstermesini engelliyor. Bir işlevi static bildirmek, o işlevin yalnız kendi
biriminin iç işleyişine ait bir yardımcı olduğunu, dışarıya bir arayüz olarak
sunulmadığını kaynakta açıkça işaretler; bu, bir birimin kendi iç ayrıntılarını
gizleyebilmesinin işlevler düzeyindeki karşılığı.
Bildirim ile Tanımın Ayrılması
Bir bildirim, derleyiciye bir işlevin imzasını (adı, parametre tipleri, dönüş tipi)
söyler; bir tanım ise gövdeyi de içerir. asama.hdeki int kimlik(void); bir
bildirimdir — noktalı virgülle biter, gövde taşımaz. secim.cdeki int kimlik(void) { return 7; } ise bir tanımdır. Bir işlev, farklı birimlerde (aynı başlık dosyası
aracılığıyla) istenildiği kadar çok kez bildirilebilir, ama tanımı tam olarak bir kez
bulunmalıdır — aksi hâlde bağlayıcı aynı ada birden fazla gövde bulur ve bu bir çelişkidir.
Bu ayrım, önceki derste değişkenler için ölçülen bildirim-tanım ayrımının işlevlere
uygulanmış hâli: extern int birim_sayaci; bir değişken bildirimiyken int birim_sayaci = 2; tanımdı; burada da aynı örüntü, gövde yerine yalnız imza taşıyan bir
bildirimle tekrar ediyor.
Bir işlev bildirimi olmadan, yalnız çağrılma biçimine bakarak bir çağrının doğru olup
olmadığını denetlemek mümkün değildir; derleyici, kaç parametre beklendiğini ve hangi
tipte olduklarını yalnız bildirimden öğrenir. asama.hnin asama.c tarafından dahil
edilmesi tam olarak bunu sağlıyor: asama.c derlenirken kimlikin gövdesini hiç
görmüyor, ama parametresiz ve int döndüren bir işlev olduğunu biliyor ve çağrıyı buna
göre derliyor. Bildirim, bu yüzden yalnız bir belge değil — derleyicinin çağrıyı doğru
biçimde üretebilmesi için ihtiyaç duyduğu asgari bilgi.
Parametre Aktarımı: Her Zaman Kopya
C’de bir işleve geçirilen her argüman, işlevin kendi parametresine kopyalanır. İşlev parametresinde yapılan bir değişiklik, çağıran taraftaki değişkeni hiçbir zaman etkilemez.
// kopya.c — parametre aktarimi her zaman kopyadir, cagiranin degiskenini etkilemez
#include <stdio.h>
static void ikiye_katla(int deger) {
deger = deger * 2;
printf("%s %d\n", "islev icinde deger", deger);
}
int main(void) {
int sayi = 5;
ikiye_katla(sayi);
printf("%s %d\n", "cagiran taraftaki sayi", sayi);
return 0;
}
islev icinde deger 10 cagiran taraftaki sayi 5
Ölçüm iki satır yazdırıyor: işlev içindeki deger, çarpma sonrası 10; çağıran
taraftaki sayi ise hâlâ 5. ikiye_katla çağrıldığında, sayinın değeri (5)
deger adlı yeni bir depolama alanına kopyalanıyor; degerin içindeki değişiklik yalnız o kopyayı etkiliyor,
maindeki sayiya hiç dokunmuyor. İşlev döndüğünde deger ortadan kalkıyor —
Değişkenler, Kapsam ve Ömür dersinde ölçülen otomatik ömrün bir örneği — ve sayi hiç
değişmemiş hâliyle kalıyor. Bu davranış, işlevin aldığı parametrenin türü ne olursa
olsun (tam sayı, kayan nokta, yapı) aynıdır: C’de aktarım her zaman değer aktarımıdır,
kaynaktaki değişkenin kendisi hiçbir zaman doğrudan işleve geçmez.
Bu kopyalama, kaç parametre olursa olsun aynı şekilde işler ve işlevin gövdesinin
uzunluğundan bağımsızdır: deger adı hem çağıranın sayisinden hem de işlevin kendi
gövdesinden ayrı bir depolama alanına karşılık gelir, işlev çağrıldığı anda
oluşturulur ve döndüğünde ortadan kalkar. Bu, Değişkenler, Kapsam ve Ömür dersinde
ölçülen otomatik ömür kuralının parametrelere de uygulandığını gösteriyor: bir
parametre, tıpkı gövde içinde bildirilen sıradan bir yerel değişken gibi davranır —
yalnız ilk değeri çağıran taraftan gelir. Çağıran tarafın değişkenini gerçekten
değiştirmek isteyen bir kaynak yazarı, C’de bunu değeri değil, o değerin adresini
geçirerek yapar; ama adresin kendisi bu derste basılmıyor ve bu konu, bir kursun
tamamına ayrılmış işaretçiler konusunun parçası.
Dönüş Değerinin Tipi
Bir işlevin dönüş tipi, gövde içinde hangi ifadenin hesaplandığından bağımsız olarak, çağıranın o değeri nasıl yorumlayacağını belirler.
// donus.c — donus degerinin tipi, cagiran tarafin degeri nasil yorumladigini belirler
#include <stdio.h>
static int oranla(int pay, int payda) {
double gercek_oran = (double)pay / (double)payda;
return (int)(gercek_oran * 100);
}
int main(void) {
int yuzde = oranla(1, 3);
printf("%s %d\n", "int donus tipiyle yuzde", yuzde);
return 0;
}
int donus tipiyle yuzde 33
oranla içinde gercek_oran bir double ve 1 / 3 tam sayı bölmesi yerine kayan
nokta bölmesiyle hesaplanıyor (0.333...). Ama işlevin dönüş tipi int olarak
bildirildiği için, return ifadesindeki değer — 33.333... — çağırana ulaşmadan önce
inte dönüştürülüyor ve kesirli kısım atılıyor: 33. Bu dönüşüm, işlev gövdesinin
neresinde hangi tipin kullanıldığına bakılmaksızın, yalnız işlevin bildirilen dönüş
tipine bakılarak uygulanıyor — tıpkı bileşik atamada sonucun sol tarafın tipine
zorlanması gibi, burada da sonuç işlevin imzasında yazılı tipe zorlanıyor. oranla(1, 3) ve oranla(1, 4) gibi farklı çağrılar farklı gerçek oranlar üretse de, ikisi de aynı
return ifadesinden, aynı inte daraltma kuralından geçer — hangi girdiyle
çağrıldığına bakılmaksızın, dönüşüm kuralının kendisi değişmez.
Bu dönüşümün yeri önemlidir: gercek_oran * 100 ifadesi hâlâ oranlanın gövdesi
içinde, double genişliğinde hesaplanıyor; inte daraltma yalnız return ifadesinin
değeri işlevin dışına çıkarken, tam olarak işlev sınırında uygulanıyor. Çağıran taraf
(main), oranlanın içinde hangi ara tiplerin kullanıldığını hiç bilmez ve bilmesi de
gerekmez — yalnız bildirimdeki dönüş tipini (int) görür ve gelen değeri o tipte bir
değişkene (yuzde) atar. Bu, işlev sınırının bir tür kapsülleme sağladığını
gösteriyor: gövde içindeki hesaplama hangi ara tipleri kullanırsa kullansın, dışarıya
sızan tek bilgi, imzada yazılı dönüş tipidir.
Sınırlayıcı Ölçüm
Bir çağrı, çağrılan işlevin bildirimiyle uyumlu olduğu sürece güvenle derlenir ve
bağlanır — yukarıdaki dogru.c örneği bunu gösteriyor.
// asama.h — cagrinin bicimini derleyiciye bildiren tam bildirim #ifndef ASAMA_H #define ASAMA_H int kimlik(void); #endif
// secim.c — tanim, bildirimle ayni imzayi tasir
#include "asama.h"
int kimlik(void) {
return 7;
}
// dogru.c — bildirimle tanim ayni imzada oldugunda cagri guvenle baglanir
#include <stdio.h>
#include "asama.h"
int main(void) {
printf("%s %d\n", "dogru bildirimli cagri", kimlik());
return 0;
}
dogru bildirimli cagri 7
Bu ortamda, bildirimi hiç görünmeyen bir işlevi çağırmayı deneyen bir kaynak derleyici
tarafından doğrudan reddediliyor — derleyici, bir çağrının biçimini denetleyebilmek için
en azından bir bildirime ihtiyaç duyuyor ve onsuz kaynağı derlemeyi durduruyor. Ama bu,
bütün uyuşmazlıkları derleyicinin yakaladığı anlamına gelmiyor. Bir bildirim, yalnız o
bildirimin bulunduğu birimde denetlenir; derleyici asama.cyi derlerken secim.cnin
içine hiç bakmaz, secim.cdeki gerçek tanımın bildirimle gerçekten eşleştiğini
doğrulayan bir mekanizma yoktur. C’nin bağlayıcısı da sembol adına bakar, tipe bakmaz —
iki birim aynı isimde ama farklı parametre tipleriyle bir işlevi bildirip tanımlasa,
bağlayıcı bunları yine de birbirine bağlar. Böyle bir çağrı, derleneni ve bağlananı
çalıştırdığında tanımsız davranışa düşer, çünkü çağıran taraf argümanları bir biçimde
hazırlar, çağrılan taraf onları başka bir biçimde okur; bu kursun genel kuralınca böyle
bir uyumsuzluk kod bloğunda denenmiyor. Ders yalnız yukarıdaki doğru bildirimli
kurguyu koşturuyor: bildirim çağrının biçimini derleyiciye söyler, ama tanımla
gerçekten eşleştiğini garanti eden şey, kaynak yazarının kendi tutarlılığıdır — ne
derleyici ne bağlayıcı bunu birimler arasında denetler.
Bu sınırın pratik sonucu, bir başlık dosyasının neden tek bir doğruluk kaynağı
olarak tutulduğudur. kimliki çağıran her birim asama.hyi dahil ettiği ve secim.c
de aynı başlığı dahil ettiği sürece, bildirim ile tanım aynı kaynaktan geldiği için
birbirleriyle otomatik olarak tutarlı kalır — kaynak yazarı iki farklı yerde aynı
imzayı elle iki kez yazmak zorunda kalmaz. Bir birim başlığı dahil etmeyip işlevin
imzasını kendi kaynağında elden yazsaydı, iki bildirim kaynakta ayrı ayrı durur ve
biri değiştirildiğinde öbürünün fark edilmeden eskimesi mümkün olurdu — tam da
yukarıda anlatılan riskin kaynağı. Başlık dosyasını paylaşmak, bu riski ortadan
kaldırmaz ama en azından imzanın bir kez yazılmasını, dolayısıyla en az bir
kaynaktan gelen tutarlılığı garanti eder.
Özet
- Bir çağrının hangi tanıma bağlanacağı, birimler bir araya geldiğinde bağlayıcı tarafından çözülür; derleyici yalnız bildirimden imzayı denetler, gövdeyi görmez.
- Bir bildirim yalnız imzayı (ad, parametre tipleri, dönüş tipi) taşır; bir tanım gövdeyi de içerir. Bir işlev çok kez bildirilebilir, ama tam olarak bir kez tanımlanır.
- Parametre aktarımı her zaman değer aktarımıdır: işlev içinde yapılan bir değişiklik, çağıranın kendi değişkenini hiçbir zaman etkilemez.
- Bir işlevin dönüş değeri, gövde içindeki ara hesaplamaların tipinden bağımsız olarak, işlevin bildirilen dönüş tipine dönüştürülür.
- Bir bildirim ile o adın gerçek tanımı arasındaki uyum, ne derleyici ne bağlayıcı tarafından birimler arasında denetlenir; bu tutarlılığı korumak kaynak yazarının işi.
staticbir işlev kendi birimine kapanır ve bağlayıcı tarafından başka hiçbir birime gösterilmez; işlevler için de değişkenlerdeki gizleme kuralı aynen geçerlidir.
Sonraki Adım
Bu derste ölçülen kararların hepsi, bir işlevin çağrılma biçimiyle ilgiliydi: hangi tanıma bağlandığı, argümanın nasıl aktarıldığı, dönüş değerinin nasıl yorumlandığı. Diziler ve Dizgiler dersi bu zemine, bir verinin uzunluğunun ne zaman belli olduğu sorusunu ekliyor: bir dizinin eleman sayısı kaynakta sabit bir ifadeden derlemede üretilebilirken, bir dizginin uzunluğu ancak koşumda, sonlandırıcı bayta ulaşılana kadar taranarak öğrenilebiliyor — ve bu derste ölçülen “parametre her zaman kopyadır” kuralı, bir dizi işleve geçtiğinde ilginç bir istisnaya dönüşecek: bir dizi adı işleve geçirildiğinde, kopyalanan şey dizinin kendisi değil, dizinin ilk elemanının adresi olacak — ve bu, uzunluk bilgisinin neden ayrıca taşınması gerektiğinin de kaynağı.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.