Ders 06 / 15
Fonksiyon İşaretçileri
Bir işlevin kendisini gösteren ve başka bir işleve parametre olarak geçirilebilen işaretçiler; bir geri çağrının taşıdığı bağlamın sahipliğinin çağırana ait olduğu.
Yayın Güncelleme
Bu derste
- Bir fonksiyon işaretçisini tanımlar ve çağırarak başka bir işlevi dolaylı biçimde çalıştırır
- Bir işlevi başka bir işleve parametre olarak geçirip bir geri çağrı kurar
- Bir geri çağrının taşıdığı bağlamın sahipliğinin çağırana ait olduğunu ölçer
İçindekiler
Şimdiye kadarki beş ders, bir işaretçinin veriyi — bir tamsayıyı, bir diziyi, opak bir kaydı — gösterdiği durumları ölçtü. Bu son ders farklı bir şeyi gösteren bir işaretçiyi ölçüyor: işlevin kendisini. Bir fonksiyon işaretçisi, derlenmiş bir işlevin adresini tutar ve o işlevi dolaylı biçimde çağırmayı sağlar — bu, bir işlevi çalışma zamanında seçmenin ya da başka bir işleve parametre olarak geçirmenin temelidir.
Bir fonksiyon işaretçisinin bildirimi ilk bakışta karışık görünür: int (*islem)(int, int)
ifadesindeki parantezler rastgele değil, zorunlu — int *islem(int, int) yazılsaydı bu,
int * döndüren ve islem adlı bir işlevin bildirimi olurdu, islem adında bir
işaretçi değil. Parantezler *islem’i önce bir isim olarak gruplandırıp derleyiciye
“islem bir işaretçidir, gösterdiği şey şu imzaya uyan bir işlevdir” dedirtiyor. Bu
karmaşıklık genellikle bir typedef ile gizlenir — nitekim geri_baglam.c’deki
void (*islev)(int, void *) parametre tipi, islem_tablosu.c’deki IkiliIslem tanımıyla
aynı deseni izliyor, yalnız birinde tip doğrudan yazılmış, diğerinde bir isme bağlanmış.
Fonksiyon İşaretçisini Tanımlamak ve Çağırmak
// islev_isaretcisi.c — bir islev isaretcisi, derlenmis bir islevin adresini tutar
#include <stdio.h>
static int topla(int a, int b) {
return a + b;
}
static int carp(int a, int b) {
return a * b;
}
int main(void) {
int (*islem)(int, int) = topla;
printf("%s %d\n", "topla uzerinden dolayli cagri", islem(3, 4));
islem = carp;
printf("%s %d\n", "carp uzerinden dolayli cagri", islem(3, 4));
printf("%s %d\n", "islem toplaya esit mi", (int)(islem == topla));
printf("%s %d\n", "islem carpa esit mi", (int)(islem == carp));
return 0;
}
topla uzerinden dolayli cagri 7 carp uzerinden dolayli cagri 12 islem toplaya esit mi 0 islem carpa esit mi 1
IS25. int (*islem)(int, int) bildirimi, islem’in iki int alıp bir int
döndüren bir işlevin adresini tutacağını söylüyor. islem = topla atamasından sonra
islem(3, 4) çağrısı 7 veriyor — topla’yı dolaylı biçimde çalıştırıyor. islem
daha sonra carp’a yeniden atanıyor ve aynı çağrı yazımıyla (islem(3, 4)) bu kez
12 veriyor. islem == topla 0, islem == carp 1 — bu, fonksiyon
işaretçilerinin de veri işaretçileri gibi eşitlikle karşılaştırılabildiğini gösteriyor;
islem şu an hangi işlevi gösteriyorsa onunla eşleşiyor.
Geri Çağrı: Bir İşlevi Başka Bir İşleve Geçirmek
// geri_cagri.c — bir islevi parametre olarak gecirmek, calisma zamaninda davranisi secer
#include <stdio.h>
static int kare(int x) {
return x * x;
}
static int negatif(int x) {
return -x;
}
static void uygula_ve_yazdir(int deger, int (*donusum)(int), const char *etiket) {
printf("%s %d\n", etiket, donusum(deger));
}
int main(void) {
uygula_ve_yazdir(6, kare, "kare donusumu");
uygula_ve_yazdir(6, negatif, "negatif donusumu");
return 0;
}
kare donusumu 36 negatif donusumu -6
IS26. uygula_ve_yazdir işlevi, int (*donusum)(int) parametresiyle hangi
işlevin çalıştırılacağını çağıranın belirlemesine izin veriyor — işlevin kendi içinde
kare ya da negatif adı hiç geçmiyor. İlk çağrıda kare geçiyor ve sonuç 36
(6*6), ikinci çağrıda negatif geçiyor ve sonuç -6 çıkıyor. Bu düzen, aynı
uygula_ve_yazdir gövdesini değiştirmeden farklı davranışlar elde etmeyi sağlıyor —
davranış, derleme zamanında değil çağrı anında parametre olarak seçiliyor. Bu
düzene geri çağrı deniyor: donusum, uygula_ve_yazdir’in içinden geriye, çağıranın
sağladığı işleve çağrı yapıyor.
Bu düzenin topla/carp örneğinden farkı, işlevin kendisinin artık bir değişkende
tutulmaması, doğrudan bir parametre olarak akmasıdır. islem değişkeni main içinde
kalıcıydı ve yeniden atanabiliyordu; donusum ise yalnız uygula_ve_yazdir’in çalıştığı
süre boyunca var olan geçici bir parametre. İki kullanım da aynı fonksiyon işaretçisi
mekanizmasını paylaşıyor, ama biri durum tutmayı (islem hangi işlevi gösteriyor),
diğeri tek seferlik yönlendirmeyi (bu çağrıda hangi işlev kullanılacak) örnekliyor.
Bağlamın Sahipliği: Geri Çağrıya Taşınan Veri
Bir geri çağrı çoğu zaman yalnız bir değeri değil, o değere eşlik eden bir bağlamı (context) da taşır. Bu bağlam kimin sahipliğinde olmalı? Aşağıdaki örnek, çağıranın bir bağlam sağladığı ve bu bağlamın ömrünü kendisinin yönettiği durumu ölçüyor.
// geri_baglam.c — geri cagriya tasinan baglamin sahibi onu saglayan cagirandir
#include <stdio.h>
typedef struct {
int carpan;
int toplam;
} Baglam;
static void birikti(int deger, void *baglam) {
Baglam *b = baglam;
b->toplam += deger * b->carpan;
}
static void dizi_uzerinde_calistir(const int *dizi, int uzunluk,
void (*islev)(int, void *), void *baglam) {
for (int i = 0; i < uzunluk; i++) {
islev(dizi[i], baglam);
}
}
int main(void) {
int degerler[4] = {1, 2, 3, 4};
Baglam b = {.carpan = 3, .toplam = 0};
dizi_uzerinde_calistir(degerler, 4, birikti, &b);
printf("%s %d\n", "carpanli toplam", b.toplam);
b.carpan = 1;
b.toplam = 0;
dizi_uzerinde_calistir(degerler, 4, birikti, &b);
printf("%s %d\n", "carpansiz toplam", b.toplam);
return 0;
}
carpanli toplam 30 carpansiz toplam 10
IS27. Baglam yapısı main içinde yerel bir değişken olarak tanımlanıyor —
say_ayir ya da malloc ile hiç ayrılmıyor, sınıfı dil. Ama dizi_uzerinde_calistir
işlevine &b olarak geçiyor ve o işlev bunu birikti’ye void *baglam parametresi
üzerinden aktarıyor. birikti, baglam’ı kendi sahiplenmiyor — yalnız main’in
sahip olduğu belleğe yazıyor. İlk koşumda çarpan 3 ve toplam 30 (3*(1+2+3+4)),
ikinci koşumda çarpan 1’e sıfırlanıyor ve toplam 10 çıkıyor — b yeniden
kullanılabiliyor çünkü ömrü hâlâ main’e ait. Geri çağrıya geçen bir bağlam, dinamik
olarak ayrılmış olsaydı bile aynı kural geçerli olurdu: bağlamı sağlayan taraf
(çağıran) onu bırakmakla yükümlü kalırdı, birikti gibi bir geri çağrı işlevi onu asla
kendi sahiplenmez.
Bu düzen, bu kursun ilk dersindeki opak kayit gözlemiyle aynı sahiplik biçimini
paylaşıyor ama ters yönden: orada çağrılan (sahip.c) kendi verisini tutuyor ve
çağırana yalnız bir ödünç veriyordu, burada ise çağıran kendi verisini tutuyor ve
çağrılan (birikti, dizi_uzerinde_calistir) bu veriye yalnız bir işaretçi üzerinden
erişiyor. İki durumda da veri tek bir tarafta kalıyor; değişen yalnızca hangi tarafın
belleği tuttuğu. void *baglam parametresi burada 05-void-and-opaque-pointers.mdx
dersindeki void *’la aynı kuralı izliyor: birikti işlevi baglam’ı önce Baglam *
tipine dönüştürüyor, ancak ondan sonra alanlarına erişiyor — tip bilgisi çağıran
tarafından taşınıyor, geri çağrı işlevi bunu yalnız geri kazanıyor.
Fonksiyon İşaretçisi Dizisi: Bir Dağıtım Tablosu
Bir fonksiyon işaretçisi tek başına yeniden atanabildiği gibi, bir dizi içinde de tutulabilir — bu, çalışma zamanında hangi işlevin çalışacağını bir indeksle seçmeyi sağlar.
// islem_tablosu.c — bir islev isaretcisi dizisi, calisma zamaninda dagitim tablosu kurar
#include <stdio.h>
typedef int (*IkiliIslem)(int, int);
static int topla(int a, int b) { return a + b; }
static int cikar(int a, int b) { return a - b; }
static int carp(int a, int b) { return a * b; }
int main(void) {
IkiliIslem tablo[3] = { topla, cikar, carp };
const char *adlar[3] = { "topla", "cikar", "carp" };
int sonuclar[3];
for (int i = 0; i < 3; i++) {
sonuclar[i] = tablo[i](10, 4);
printf("%s %d\n", adlar[i], sonuclar[i]);
}
int toplam = 0;
for (int i = 0; i < 3; i++) {
toplam += sonuclar[i];
}
printf("%s %d\n", "sonuclarin toplami", toplam);
return 0;
}
topla 14 cikar 6 carp 40 sonuclarin toplami 60
Bu tablo, if/else if zincirinin ya da bir switch ifadesinin yerini alan bir dağıtım
biçimidir: yeni bir işlem eklemek tablo ve adlar dizilerine bir eleman eklemekle
sınırlı kalır, çağıran kodun kendisi değişmez.
IS28. tablo dizisinin her elemanı farklı bir işlevin adresini tutuyor;
tablo[i](10, 4) çağrısı hangi işlevin koşacağını çalışma zamanında, i indeksine
göre seçiyor — kaynak kodunda topla(10, 4), cikar(10, 4), carp(10, 4) gibi üç
ayrı satır yazmaya gerek kalmıyor. sonuclar dizisi hem tablo hem de kendisi main
içinde yerel değişken olarak tanımlı; ikisi de sınıfı çağıran — main onları ayırıyor,
doldurup okuyor, işlev çağrısı bitince kapsam dışına çıkıp kendiliğinden geri veriyor. Bu,
geri_baglam.c’deki Baglam b’nin sahiplik biçimiyle aynı: dizi de yapı da dil
sınıfının otomatik ömrüyle yönetiliyor, ama dizinin içeriği (hangi işlevlerin hangi sırada
durduğu) tamamen main’in kararı — topla, cikar ve carp bu kararda hiç söz sahibi
değil, yalnız çağrıldıklarında kendi işlerini yapıyorlar.
Sınırlayıcı Ölçüm
Bir fonksiyon işaretçisinin imzası (int (*)(int, int) gibi) yalnız parametre ve dönüş
tiplerini eşleştirir — gösterdiği işlevin ne yaptığını ya da yan etkilerinin
neler olduğunu söylemez. Yukarıdaki topla ve carp aynı imzayı paylaşıyor ama tamamen
farklı işler yapıyor; bir geri çağrı parametresi alan bir işlev, kendisine geçirilen
işlevin davranışı hakkında derleme zamanında hiçbir garanti alamaz. Bu, işaretçi
tiplerinin sahiplik bilgisini taşımadığı önceki derslerdeki bulguyla aynı sınırın bir
başka biçimidir: tip, şekli garanti eder, anlamı garanti etmez.
Bu sınır, geri çağrıya taşınan bağlamla birleştiğinde ayrı bir riske dönüşür. geri_baglam.c’de
dizi_uzerinde_calistir çağrısı birikti’ye geçtiği anda b hâlâ main’in kapsamında,
dolayısıyla baglam’ın gösterdiği bellek geçerli. Ama bir geri çağrı daha sonra,
örneğin bir olay kuyruğuna kaydedilip çağıran işlev geri döndükten sonra çalışacaksa,
bağlam olarak geçirilen adres artık dil sınıfının otomatik ömrünü aşmış olabilir —
04. dersteki sarkan işaretçi durumunun aynısı, burada geri çağrı biçiminde. Bu blok bilerek
koşturulmuyor: bir işlev, yerel bir Baglam değişkeninin adresini bir geri çağrı kaydına
yazıp döner, sonra kayıtlı geri çağrı tetiklendiğinde baglam, artık kapsamı kapanmış bir
çağrı çerçevesini gösteriyor olurdu — dolaylamak tanımsız davranıştır. dizi_uzerinde_calistir
örneğinde bu risk yok, çünkü geri çağrı main’in kendi çağrı çerçevesi içindeyken, b
hâlâ yaşarken çalışıyor; risk yalnız bağlamın ömrü ile geri çağrının çalışma anı
ayrıştığında ortaya çıkıyor. Bu yüzden bağlam sağlayan tarafın (çağıran) yükümlülüğü
yalnız belleği bırakmakla sınırlı değil — geri çağrı ne zaman tetiklenebilirse bağlamın o
âna kadar yaşamasını da garanti etmek zorunda.
Bu desenin en yaygın kullanımı C standart kütüphanesinde: qsort, bir diziyi sıralamak
için bir int (*)(const void *, const void *) karşılaştırma işlevi ister, bsearch aynı
imzayı arama için kullanır. qsort’un kendisi hangi tipi sıraladığını bilmiyor —
islem_tablosu.c’deki tablo[i] çağrısıyla aynı dolaylama düzeni, ama bu kez dolaylanan
işlev çağıranın verdiği bir karşılaştırma kararı. qsort’a geçirilen karşılaştırma işlevi
de kendi parametrelerini önce doğru tipe dönüştürmek zorunda — tıpkı birikti’nin
void *baglam’ı önce Baglam *’a çevirmesi gibi; imzadaki const void * burada da tip
bilgisini taşımıyor, yalnız saklıyor. dagitim tablosu ve qsort deseni arasındaki fark
yalnız ölçek: biri üç sabit işlev arasında seçim yapıyor, diğeri çağıranın her seferinde
farklı bir karşılaştırma mantığı sağlamasına izin veriyor — ikisinin de temeli aynı: bir
işlevi veri gibi taşıyıp gerektiğinde dolaylı çağırmak.
Sahiplik Ekseni A Payı Boyunca: Dört Sınıf, Bir Sonuç
İşaretçiler konusunun altı dersi aynı soruyu altı farklı işaretçi biçiminde sordu: bu belleği kim
sahipleniyor? 01 ve 02. derslerde yanıt dil’di — main içindeki yerel değişkenler ve
diziler, kapsam kapanınca kendiliğinden geri veriliyordu, hiçbir taraf free çağırmıyordu.
03. derste sahiplik yine dil’de kaldı, ama sorun sahiplikten çok bilgi kaybıydı: bir
dizi işaretçiye bozunduğunda uzunluğunu kaybediyordu, bu da en_buyuk, matris_bozunmasi.c
ve ciktisi_da_donen.c’nin üç farklı kanaldan aynı eksik bilgiyi tamamlamasını gerektiriyordu.
04. derste sahiplik üç kaynağa (dil, çağıran, çağrılan) ayrıldı ve ortak sonuç sarkan
işaretçiydi — bırakılmış bir bloğu göstermeye devam eden bir değer. 05. derste sahiplik ilk
kez gizlenen bir sınıfa taşındı: opak tip, çağrılan’ın kendi verisini tuttuğu ve
çağırana yalnız ödünç verdiği bir sözleşmeydi. Bu son derste sahiplik yeniden çağıran’a
döndü — ama bu kez veri değil, bir işlev seçimi ve ona eşlik eden bağlam söz konusu;
geri_baglam.c’deki Baglam b ve islem_tablosu.c’deki tablo/sonuclar dizileri,
main’in ayırdığı ve main’in geri aldığı belleklerdi.
Dört sınıfın (dil, çağıran, çağrılan, sahipsiz) ortak noktası, hiçbirinin işaretçinin
tipinde görünmemesiydi — int *, Kayit * ya da int (*)(int, int) hiçbiri hangi
tarafın bırakmakla yükümlü olduğunu söylemiyordu. Bu bilgi her zaman ya bir isimlendirme
kuralıyla (_olustur/_yok_et çiftleri), ya bir yorumla, ya da bir sözleşme belgesiyle
dışarıdan taşınmak zorundaydı. Sıradaki konu, Dinamik Bellek, bu dört sınıfı artık
malloc/free çiftinin kendisine uygulayacak; buradaki sınıflandırma değişmeyecek, yalnız
hangi işlevlerin hangi sınıfı temsil ettiği netleşecek.
Özet
- Bir fonksiyon işaretçisi (
int (*)(int, int)) derlenmiş bir işlevin adresini tutar ve o işlevi dolaylı biçimde çağırmayı sağlar; farklı işlevlere yeniden atanabilir. - Bir işlevi başka bir işleve parametre olarak geçirmek bir geri çağrı kurar; çağrılan işlevin davranışı, çalışma zamanında hangi işlevin geçirildiğine bağlı olarak değişir.
- Bir geri çağrıya taşınan bağlam, çoğunlukla
dilsınıfında bir yerel değişkenin adresidir; geri çağrı işlevi bu bağlamı sahiplenmez, yalnız kullanır. - Bağlamı sağlayan taraf çağırandır ve bağlam dinamik olarak ayrılmış olsaydı da onu bırakma yükümlülüğü yine çağıranda kalırdı.
- Bir fonksiyon işaretçisinin imzası yalnız parametre ve dönüş tiplerini eşleştirir; gösterdiği işlevin davranışını garanti etmez.
- Bir fonksiyon işaretçisi dizisi (
islem_tablosu.c’dekitablo) çalışma zamanında indeksle seçilen bir dağıtım tablosu kurar;qsort/bsearch’ün karşılaştırma parametresi aynı deseni standart kütüphanede kullanır.
Sonraki Adım
İşaretçiler konusu burada tamamlanıyor: dört sınıf (dil, çağıran, çağrılan, sahipsiz) altı ders
boyunca hem veri işaretçileri hem işlev işaretçileri üzerinden ölçüldü. Her derste
sınıfı belirleyen şey işaretçinin tipi değil, hangi tarafın ayırma ve bırakma işlemini
üstlendiğiydi — bu ayrım kaynak kodda hiçbir zaman doğrudan görünmedi, yalnız isimlendirme
ve sözleşmeyle iletildi. Sıradaki konu, Dinamik Bellek, bu dört sınıfı malloc, calloc,
realloc ve free işlevlerinin kendi davranışlarına — başarısız ayırma, yeniden
boyutlandırmanın taşıdığı riskler ve serbest bırakma disiplini — taşıyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.