Fonksiyon Göstericileri — 1(Function Pointers)

Necati Ergin
9 min readJun 24, 2020
Resim: Three musicians (Pablo Picasso)

C dilinde adresler iki ayrı kategoriye ayrılır:
Nesne adresleri (Object pointers)
Fonksiyon adresleri (function pointers)

Bir fonksiyonun adresi o fonksiyonun makine kodlarının yerleştiği bellek bloğunun adresi olarak görülebilir. C dilinde bir fonksiyonun adresi, fonksiyon göstericisi (function pointer) denen özel bir gösterici değişkende saklanabilir. Bir fonksiyon adresi başka bir fonksiyona argüman olarak gönderilebilir. Bir fonksiyonun geri dönüş değeri bir fonksiyon adresi olabilir. Elemanları fonksiyon adresleri olan diziler oluşturulabilir. Fonksiyon adresleri C ve C++ dillerinde en sık kullanılan araçlar arasındadır.

fonksiyon adreslerinin elde edilmesi

Bir fonksiyonun adresi fonksiyon isminin adres operatörünün terimi (operand) yapılmasıyla elde edilir. Yani örneğin

int func(int x, int y);

biçiminde bildirilen func fonksiyonunun adresi

&func

ifadesiyle kullanılabilir.

fonksiyon adreslerinin türleri

C dilinde her ifadenin (expression) bir türü vardır. Bir fonksiyonun adresi anlamına gelen bir ifadenin türü, söz konusu fonksiyonun geri dönüş değerinin ve parametre değişkenlerinin türleriyle ilişkilendirilmiş bir türdür. Örneğin yukarıda bildirilen func fonksiyonunun adresi aşağıdaki türdendir:

int (*)(int, int)

Soldaki parantezin içindeki asterisk (*) atomu türün bir adrese ilişkin olduğunu anlatıyor. Bu parantezin soluna yazılan int söz konusu adrese ilişkin fonksiyonun geri dönüş değerinin int türden olduğuna işaret ediyor. Sağdaki parantez içinde virgüllerle ayrılmış tür listesi ise söz konusu fonksiyonun parametre değişkenlerinin türlerini gösteriyor. Birkaç örnek daha verelim. Örneğin standart strcmp fonksiyonunun adresi

int (*)(const char *, const char *)

türündendir. Standart strcpy fonksiyonunun adresi

char *(*)(char *, const char *)

türündendir.

fonksiyon isimlerinin fonksiyon adreslerine dönüştürülmesi

Bir dizi isminin bir ifade içinde kullanıldığında derleyici tarafından dizinin ilk elemanının adresine dönüştürüldüğünü (array to pointer conversion / array decay) biliyorsunuz. Benzer şekilde bir fonksiyon ismi de bir ifade içinde kullanıldığında derleyici tarafından ilgili fonksiyonun adresine dönüştürülür (function to pointer conversion). Bir ifade içinde yer alan fonksiyon isimlerine yazılımsal olarak fonksiyonların adresleri gözüyle bakılabilir.

&func

ile

func

eşdeğer ifadeler olarak düşünülebilir. Her iki ifade de func fonksiyonunun adresi olarak kullanılabilir.

Fonksiyon göstericisi değişkenler

Bir fonksiyon göstericisi değişken değeri bir fonksiyonun adresi olan değişkendir. Bu tür değişkenlerin amacı fonksiyonların adreslerini tutmaktır. Yukarıda bildirilen func fonksiyonunun adresini tutabilecek fptr isimli bir gösterici değişkeni aşağıdaki gibi tanımlayabiliriz:

int func(int, int);
int (*fptr)(int, int);

fptr değişkenine func fonksiyonunun adresi ilk değer olarak verilebilir ya da atanabilir

fptr = &func;

Fonksiyon isimlerinin derleyici tarafından fonksiyonların adreslerine dönüştürülmesinden faydalanarak bu atama deyimi aşağıdaki gibi de yazılabilirdi:

fptr = func;

Şimdi de aşağıdaki koda bakalım:

#include <string.h>int main()
{
int(*fp)(const char *, const char *) = &strcmp;
//...
fp = &strcoll;
//...
}

Yukarıdaki main fonksiyonunda tanımlanan fp isimli fonksiyon göstericisi değişkene standart strcmp fonksiyonunun adresiyle ilk değer veriliyor. Daha sonra yer alan deyimle fp değişkenine bu kez aynı parametrik yapıda olan standart strcoll fonksiyonunun adresi atanıyor.

fonksiyon göstericileri ve gösterici aritmetiği

Bir fonksiyon göstericisi gösterici aritmetiği ile kullanılamaz, yani fonksiyon adresleri diğer adresler gibi tam sayılar ile toplanamaz. Fonksiyon göstericisi değişkenler, ++ ya da -- operatörlerinin terimi olamazlar. Zaten gösterici aritmetiğinin fonksiyon adresleri için bir anlamı da olamazdı. Bir fonksiyon adresine bir tam sayı toplayıp bellekte bir sonraki fonksiyonun adresine erişmek nasıl mümkün olurdu? Fonksiyon adresleri içerik operatörünün (dereferencing) terimi olabilir. Böyle ifadeler ile yalnızca fonksiyon çağrısı yapılabilir.

Fonksiyon göstericileri ve karşılaştırma işlemleri

Fonksiyon göstericileri karşılaştırma operatörlerinin de terimi olabilirler. Örneğin, iki fonksiyon göstericisinin değerlerinin aynı adres olup olmadığı yani aynı fonksiyonu gösterip göstermedikleri == ya da != operatörleri ile karşılaştırılabilir:

#include <stdio.h>double f1(double) { return 1.; }
double f2(double) { return 2.; }
int main()
{
double(*fptr1)(double) = &f1;
double(*fptr2)(double) = &f2;
if (fptr1 == fptr2)
printf("esit\n");
else
printf("esit degil\n");
fptr1 = &f2; if (fptr1 == fptr2)
printf("esit\n");
else
printf("esit degil\n");
}

Hiçbir fonksiyonu göstermeyen bir fonksiyon göstericisi değişken, değeri NULL gösterici olan değişkendir. Fonksiyon göstericisi değişkenler için de NULL göstericiden faydalanılabilir:

#include <stddef.h>int main()
{
void(*fp)(void) = NULL;
//...
if (fp)
fp();
//...
}

Yukarıdaki main fonksiyonu içinde fp isimli fonksiyon göstericisine NULL adresi ile ilk değer veriliyor. Daha aşağıdaki if deyiminde ise fp fonksiyon göstericisinin değerinin NULL gösterici olup olmadığı yani fp'nin bir fonksiyonu gösterip göstermediği sınanıyor.

Fonksiyon çağrı operatörü (function call operator)

Fonksiyon çağrı operatörü tek terimli son ek konumunda bir operatördür. Operatör öncelik tablomuzun en yüksek seviyesindedir (primary expression). Bu operatörün terimi bir fonksiyon adresidir. Operatör programın akışını o adrese yöneltir, yani o adresteki kodun çalıştırılmasını sağlar. Örneğin:

func(10, 20)

Burada fonksiyon çağrı operatörünün terimi func adresidir. Operatör, func adresinde bulunan kodu, yani func fonksiyonunun kodunu çalıştırır. Fonksiyon çağrı operatörünün ürettiği değer çağrılan fonksiyonun geri dönüş değeridir. Örneğin:

int result = strcmp(s1, s2);

Burada strcmp ifadesinin türü, geri dönüş değeri int parametreleri (const char *, const char *) olan bir fonksiyon adresidir. Yani strcmp ifadesi

int (*)(const char *, const char *)

türüne dönüştürülür.

strcmp(s1, s2)

gibi bir fonksiyon çağrısının oluşturduğu ifade ise int türdendir.

fonksiyonların fonksiyon göstericileri ile çağrılması

Bir fonksiyon göstericisinin gösterdiği fonksiyon iki ayrı biçimde çağrılabilir. pf bir fonksiyon gösterici değişkeni olmak üzere, bu değişkenin gösterdiği fonksiyon

pf()

biçiminde, ya da

(*pf)()

biçiminde çağrılabilir. Birinci biçim daha doğal görünmekle birlikte, pf isminin bir gösterici değişkenin ismi mi yoksa bir fonksiyonun ismi mi olduğu çok açık değildir. İkinci biçimde, *pf ifadesinin öncelik parantezi içine alınması zorunludur. Çünkü fonksiyon çağrı operatörünün öncelik seviyesi, içerik (dereferencing) operatörünün öncelik seviyesinden daha yüksektir. Bu çağrı biçimi kullanılan ismin bir fonksiyon göstericisine ilişkin olduğu vurgusunu yaptığından tercih edilebilmektedir. Aşağıdaki programı derleyerek çalıştırın:

#include <stdio.h>void func()
{
printf("func()\n");
}
int main()
{
void (*pf)(void) = func;

pf();
(*pf)();
}

main fonksiyonu içinde tanımlanan pf isimli gösterici değişkene func fonksiyonunun adresi ile ilk değer veriliyor. Daha sonra pf fonksiyon göstericisinin gösterdiği fonksiyonun iki ayrı biçimde çağrıldığını görüyorsunuz.

fonksiyon göstericileri ve typedef bildirimleri

Fonksiyon göstericilerine ilişkin kodların kolay yazılması ve okunması amacıyla çoğunlukla fonksiyon göstericisi türlerine typedef bildirimleriyle eş isimler verilir. Aşağıdaki bildirime bakalım:

int(*func(int(*fp)(int, int)))(int, int);

Bu bildirimi okumak bir hayli zor değil mi? Bir de bildirimi yapana sorun:

func, geri dönüş değeri, geri dönüş değeri int türden ve iki int parametreli bir fonksiyonun adresi, türünden olan ve parametresi yine geri dönüş değeri int türden ve iki int parametreli bir fonksiyonun adresini isteyen gösterici olan bir fonksiyon. Yani func fonksiyonunun hem geri dönüş değeri hem de parametresi

int (*)(int, int)

türünden. Oysa bu türe bir typedef bildirimiyle eş isim verilseydi bu türe bağlı karmaşık bildirimleri okumak ya da yazmak çok daha kolay olacaktı:

typedef int(*Fptr)(int, int);Fptr func(Fptr fp);int main()
{
Fptr fp1, fp2;
Fptr ar_fp[10];
Fptr *fpptr = &fp1;
//...
}

Yukarıdaki kodda global isim alanında, geri dönüş değeri int türden olan ve int türden iki parametre değişkeni olan fonksiyonların adreslerinin türüne Fptr eş ismi veriliyor. Artık bu ismin kapsamı (scope) içinde bu isim bu türün karşılığı eş isim olarak kullanılabilir. Diğer bildirimlere sırasıyla bakalım:

Fptr func(Fptr fp);

Burada bildirilen func fonksiyonunun hem parametre değişkeni hem de geri dönüş değeri fonksiyon adresleri. Eğer typedef bildirimi olmasaydı bu bildirim

int(*func(int(*fp)(int, int)))(int, int);

biçiminde yapılacaktı.

Fptr fp1, fp2;

Burada hem fp1 hem de fp2 fonksiyon göstericisi değişkenler.

Fptr ar_fp[10];

Burada ar_fp elemanları fp1, fp2 gibi olan, yani elemanları fonksiyon göstericileri olan, 10 elemanlı bir dizidir.

Fptr *fpptr = &fp1;

Burada fpptr, fp1 ve fp2 gibi fonksiyon göstericisi değişkenlerin adresini tutacak bir gösterici değişkendir (pointer to function pointer).

fonksiyon adresi döndüren işlevler

İşlevlerin geri dönüş değerleri fonksiyon adresleri olabilir. Bu durumda böyle bir fonksiyona çağrı yapacak bir kod işlevden geri dönüş değeri yoluyla bir fonksiyonun adresini alabilir:

#include <stdio.h>typedef int(*Fptr)(int, int);static int sum_square(int x, int y)
{
return x * x + y * y;
}
Fptr func()
{
//...
return sum_square;
}
int main()
{
int a = 10, b = 20;
int ival = func()(a, b);
printf("ival = %d\n", ival);
}

Yukarıdaki kodda

func()(a, b)

ifadesi ile func işlevinin adresini döndürdüğü sum_square isimli fonksiyon çağrılıyor.

fonksiyonlara fonksiyon göndermek

Bir fonksiyon işinin bir kısmını gerçekleştirmesi için kendisini çağıran koddan adresini aldığı bir fonksiyonu çağırabilir. Fonksiyonu çağıran kod, çağırdığı fonksiyonun işinin belirli bir kısmının kendi belirlediği gibi yapılmasını sağlayabilir. Böylece bir fonksiyonun davranışının bir kısmı onu çağıran kodun belirleyeceği şekilde değiştirilebilir ya da özelleştirilebilir. Fonksiyon göstericileri ile çağrılan işlevler genelleştirilmiş işlevlerdir. Müşteri kodlar böyle işlevlere diledikleri fonksiyonun adresini geçerek onları daha özel hale getirmiş olurlar. Bu mekanizmaya popüler olarak geri çağrı” (call back) denilmektedir. C dilinde geri çağrı mekanizması bir fonksiyonun çağıracağı fonksiyona bir fonksiyonun adresini göndermesi şeklinde gerçekleşir. Bu durumda çağrılan fonksiyonun bir parametre değişkeni bir fonksiyon göstericisi olur. Fonksiyon göstericilerinin en sık kullanıldığı tema budur:

#include <stdio.h>void func(void(*fp)(void))
{
printf("func cagrildi\n");
fp();
//
}
void f()
{
printf("f cagrildi\n");
//
}
int main()
{
func(f);
}

Yukarıdaki kodda func fonksiyonunun parametre değişkeni bir fonksiyon göstericisi. main fonksiyon içinde func fonksiyonu f fonksiyonunun adresi ile çağrılıyor. func fonksiyonu da adresini aldığı fonksiyonu çağırıyor.

Türden bağımsız işlem yapan işlevler

Bazı işlevler belirli bir tür için yazılır ve dolayısıyla yalnızca belirli bir türe hizmet verirler. Örneğin elemanları int türden olan bir diziyi sıralayan bir fonksiyon şöyle bildirilebilir:

void sort_int_array(int *ptr, size_t size);

Böyle bir fonksiyon elemanları double türden olan bir diziyi sıralayamaz. Bu tür durumlarda aynı kodu, farklı türlere göre yeniden yazmak gerekir. Ancak aynı işi her tür için yapabilecek, türden bağımsız olarak tek bir fonksiyonun yazılabilmesi mümkün olabilir. Türden bağımsız işlem yapan fonksiyonların gösterici parametreleri void * türünden olmalıdır. Ancak parametrelerin void * türünden olması yalnızca çağrı açısından kolaylık sağlar. Fonksiyonu yazacak olan programcı yine de fonksiyona geçirilen adresin türünü saptamak zorundadır. Bunun için fonksiyona tür bilgisine karşılık gelen bir numaralandırma değeri gönderilebilir. Örneğin herhangi bir türden diziyi sıralayacak fonksiyonun bildirimi aşağıdaki gibi olsun:

void *gSort(void *parray, size_t size, int type);

Şimdi fonksiyonu yazacak programcı type isimli parametre değişkenini kullanarak bir switch deyimiyle dışarıdan adresi alınan dizinin türünü saptayabilir. Ancak bu yöntem C’nin doğal türleri için çalışsa da programcı tarafından oluşturulan türlerden (user defined types) diziler için doğru çalışmaz. Böyle genel işlevler ancak fonksiyon göstericileri kullanılarak yazılabilir. Şimdi bir dizinin en büyük elemanın adresiyle geri dönen bir fonksiyonu türden bağımsız olarak yazmaya çalışalım. Dizi türünden bağımsız olarak işlem yapan fonksiyonların parametre değişkenleri tipik olarak aşağıdaki gibi olur:

void *g_max(const void *pa, size_t size, size_t width, int (*cmp)(const void *, const void *));
  • Dizinin başlangıç adresini alacak void * türden bir gösterici
  • Dizinin eleman sayısını alacak size_t türünden bir parametre değişkeni.
  • Dizinin bir elemanının bellekte kaç byte yer kapladığı değerini yani sizeof değerini alan size_t türünden bir parametre değişkeni.
  • Dizinin elemanlarını karşılaştırma amacıyla kullanılacak bir fonksiyonun başlangıç adresini alan fonksiyon göstericisi parametre değişkeni.

Bu tür fonksiyonların tasarımındaki genel yaklaşım şudur: Fonksiyon her bir dizi elemanının adresini gösterici aritmetiğinden faydalanarak bulabilir. Ancak dizi elemanlarının türü bilinmediğinden fonksiyon dizinin elemanlarının değerlerini karşılaştırma işlemini yapamaz. Bu karşılaştırmayı fonksiyon göstericisi kullanarak fonksiyonu çağıran kodun gönderdiği fonksiyona yaptırır. Fonksiyonu çağıracak programcı, karşılaştırma fonksiyonunun dizinin herhangi iki elemanının adresiyle çağrılacağını göz önüne alarak karşılaştırma işlevini şöyle yazar: Karşılaştırma fonksiyonu karşılaştırılacak iki nesnenin adresini alır. Fonksiyonun birinci parametresine adresi alınan nesne, ikinci parametreye adresi alınan nesneden daha büyükse fonksiyon pozitif herhangi bir değere, küçükse negatif herhangi bir değere, bu iki değer eşitse sıfır değerine geri döner. Bu, standart strcmp fonksiyonunun sunduğu uzlaşımdır (convention). Aşağıdaki programı inceleyin:

#include <stdio.h>
#include <string.h>
#define MAX_NAME_LEN 20#define asize(a) (sizeof(a) / sizeof(*a))typedef struct {
char name[MAX_NAME_LEN];
int no;
}Person;
typedef unsigned char Byte;typedef int(*Cmpfn)(const void *, const void *);void *g_max(const void *pa, size_t size, size_t sz, Cmpfn fp)
{
Byte *pb = (Byte *)pa;
const void *pmax = (void *)pa;
for (size_t k = 1; k < size; ++k)
if (fp(pb + k * sz, pmax) > 0)
pmax = pb + k * sz;
return pmax;
}
int cmp_int(const void *vp1, const void *vp2)
{
if (*(const int *)vp1 > *(const int *)vp2)
return 1;
return *(const int *)vp1 < *(const int *)vp2 ? -1 : 0;
}
int cmp_person_by_name(const void *vp1, const void *vp2)
{
return strcmp(((const Person *)vp1)->name,
((const Person *)vp2)->name);
}
int cmp_person_by_no(const void *vp1, const void *vp2)
{
return ((const Person *)vp1)->no - ((const Person *)vp2)->no;
}
int main()
{
int a[] = { 3, 8, 4, 7, 6, 9, 12, 1, 9, 10 };
Person pa[] = {
{ "Oguz Karan", 12 },{ "Kaan Can", 56 },{ "Ali Orak", 31 },
{ "Emre Koc", 19 },{ "Nur Elci", 29 },{ "Eda Alan", 14 } };
Person *p;
int *iptr = g_max(a, asize(a), sizeof(int), cmp_int);
printf("max = %d %d indisli eleman\n", *iptr, iptr - a);
p = g_max(pa, asize(pa), sizeof(Person), cmp_person_by_name);
printf("max pa (isme gore) %s %d\n",p->name, p->no);
p = g_max(pa, asize(pa), sizeof(Person), cmp_person_by_no);
printf("max pa (numaraya gore) %s %d\n", p->name, p->no);
}

Yukarıdaki kodda,

typedef int(*Cmpfn)(const void *, const void *);

bildirimi ile geri dönüş değeri int türden olan const void * türden iki parametresi olan fonksiyonların adresi olan türe Cmpf eş ismi veriliyor. Daha sonra bir dizinin en büyük elemanının adresini döndüren türden bağımsız g_max isimli bir fonksiyon tanımlanıyor. Fonksiyonun son parametresinin bir fonksiyon göstericisi olduğunu görüyorsunuz. g_max fonksiyonu, türünü bilmediği bir dizinin başlangıç adresini, boyutunu ve elemanlarının sizeof değerlerini kendisini çağıran koddan alıyor. Böylece adresini aldığı dizinin türünü bilmese de bu dizinin herhangi bir elemanının adresini gösterici aritmetiği ile hesaplayabiliyor. Dizinin iki elemanının büyüklük karşılaştırması için son parametresine adresi geçilen fonksiyonun çağrıldığını görüyorsunuz. İşleve çağrı yapacak tüm müşteri kodlar fonksiyonun bu parametresine, bir karşılaştırma fonksiyonunun adresini göndermek zorundalar. Kodda tanımlanan

int cmp_int(const void *, const void *);
int cmp_person_by_no(const void *, const void *);
int cmp_person_by_name(const void *, const void *)

fonksiyonları dizi elemanlarının karşılaştırılması amacını taşıyorlar.

Serimizin ikinci bölümünde standart C kütüphanesinin fonksiyon gösterici parametreli bazı işlevlerini inceleyeceğiz. Serimizin üçüncü bölümünde ise fonksiyon gösterici dizileri ele alınacak.

C ve Sistem Programcıları Derneği ve Plepa Eğitim işbirliği ile düzenlenen 162 saatlik Online C Programlama Dili Kursu, 1 Şubat 2021 Pazartesi günü başlıyor.
Kurs hakkında bilgi için tıklayın:

--

--