Bu yazımızda fonksiyon göstericilerini inceleyeceğiz. Ancak konuya geçmeden
önce, göstericilere bir göz atmamızın yararlı olacağını düşünüyorum. Bildiğimiz
gibi gösterici (pointer), belli bir türden adres tutan veri türüdür. Eğer tutacağımız
adresin türü int ise, yani int türünden bir nesnenin adresini tutmak istiyorsak
bunun için tanımlamamız gereken gösterici (int *) türünden olmalıdır. Benzer
şekilde double nesnelerin adresini tutmak için (double *) türünden gösterici
tanımlanmalıdır. Bu ifade, C dilinin tüm doğal veri türleri için, ayrıca programcının
tanımladığı veri türleri için geçerli bir ifadedir. Örneğin, test isimli bir
yapı tanımladığımızı düşünelim. Bu yapı türünden bir nesnenin adresini tutmak
için (test *) türünden bir gösterici tanımlamamız gereklidir. Bu durumda C dilinde
sonsuz türde gösterici olabilir. Şimdi bir göstericiyi nasıl tanımladığımızı
örneklerle hatırlayalım :
int a = 10;
int *pi = &a; struct
test {
    //...
}; struct test t;
struct test *pt = &t; |
Peki fonksiyon
göstericisi ne demektir? Gösterici, adres tutan bir tür olduğuna göre, fonksiyon
göstericisi de adından anlaşılacağı üzere, bir fonksiyonun adresini tutacaktır.
Peki fonksiyonun adresi ne demektir? Kaynak kod içerisinde programcı tarafından
yazılan fonksiyonlar, derleme aşamasında derleyici tarafından makine koduna
dönüştürülür. Fonksiyonun kodları da, tıpkı tanımlanan değişkenler gibi bellekte
belli bir yer kaplarlar. Fonksiyonun adresi kavramı, fonksiyonun bellekte kapladığı
adresin başlangıcı anlamına gelmektedir. Tıpkı dizi isimlerinde olduğu gibi,
fonksiyonların isimleri, onların başlangıç adreslerini belirtir. Aşağıdaki örneği
inceleyiniz :
int Add_i(int
a, int b)
{
     return a + b;
} |
şeklinde bir fonksiyon
tanımlandığını düşünelim. Bu durumda, Add_i ismi, bu fonksiyonun başlangıç adresini
göstermektedir. Tanımlayacağımız fonksiyon göstericilerine atayacağımız değerler
de fonksiyon isimleri olacaktır. C dilinde, bir fonksiyonu, adını ve adının
yanına da parantez içerisinde parametre değişkenlerini yazarak çağırırız. Bu
fonksiyonu uygun türden bir göstericiye atadığımız zaman, fonksiyonu bu gösterici
yoluyla da çağırabiliriz. Bu durumun bize nasıl bir fayda sağlayacağını örnek
üzerinde göstereceğiz. Fonksiyon göstericisi tanımlama ifadesi şu şekildedir
:
<fonksiyonun
geri dönüş değeri türü>(*gösterici adı)([fonksiyonun parametre türleri])
int (*Func1)
(int,int);    /* Geri dönüş değeri int olan, iki parametre
değişkeni olan ve parametre değişkenlerinin türleri int olan fonksiyonları
göstermeye aday Func1 ismindeki fonksiyon göstericisi */ void
(*Func2) (void);    //Geri dönüş değeri ve parametre değişkeni
olmayan fonksiyon göstericilerini göstermeye aday Func2 ismindeki göstericisi
int
Add_i(int a, int b)
{
      return a + b;
}
int(*Func1) (int,int);
Func1 = Calculate;  /* Calculate ismi fonksiyonun
adresini gösterir, Func1 göstericisi artık Calculate fonksiyonunu göstermektedir*/
Calculate(10,20);
  //Fonksiyonun adı ve parametre değişkenleri ile çağrılması
Func1(10,20);
      //Fonskiyonun kendisini gösteren bir gösterici
ve parametre değişkenleri ile çağrılması
|
Peki bu durumdan
nasıl bir fayda sağlayabiliriz ? Bilindiği üzere, C dilinde aynı isimli birden
fazla fonksiyon bulunamaz. Nesne yönelimli tekniği uygulayan yüksek seviyeli
dillerde (örneğin C#), fonksiyonlar aşırı yüklenerek birden fazla tür için çalışan
aynı isimli fonksiyonlar yazılabilir. Durumu bir örnekle açıklayalım : Diziler
üzerinde işlem yaptığımızı düşünelim. Hem tamsayı türünden diziler hem de ondalıklı
sayı türünden diziler için sıralama algoritması yazmak istiyoruz. Bu durumda
C dilinin kuralları gereğince, sort_int ve sort_double isimli iki ayrı fonksiyon
yazarak isteğimizi karşılayabiliriz. İki ayrı fonksiyon yazma gereği, sıralanacak
elemanların türlerinin farklılığından kaynaklanmaktadır. Kullanacağımız algoritma
ise her iki fonksiyon için de aynı olacaktır. Büyük bir uygulamayı göz önünde
bulundurduğumuzda bu gibi durumlar sıklıkla karşımıza çıkar. Belli bir noktadan
sonra, farklı türler için aynı işi yapan fonksiyonların sayıları artacaktır.
Bu fonksiyonların isimlerinin farklı olması programcının algılamasını zorlaştırır.
Sıralama yapmak isteyen programcı, kullanacağı tür için uygun olan fonksiyonun
ismini hatırlamak zorunda kalır. Oysa bir tane sıralama fonksiyonu olsa, ismi
de örneğin "Sort" olsa, programcı sıralamayı yapacağı anda bu Sort
fonksiyonunu çağıracaktır. Böyle bir kullanım, kodun okunabilirliğini de artırır.
Fonksiyon göstericileri yardımıyla bunu yapabiliriz. Yine farklı türler için
farklı isimli fonksiyonlar yazmamız gerekecek, ancak işlemi yapacağımız anda
çağıracağımız fonksiyon bir tane olacak ve biz sadece bu fonksiyonun adını hatırlamak
zorunda kalacağız. Fonksiyon göstericilerini, bir yapıya veri elemanı olarak
ekleyebiliriz. C dilinde, yapı türleri içerisinde fonksiyon tanımlanamaz. Ancak
yapıya bir fonksiyon göstericisi eklemek yoluyla dolaylı yoldan bunu yapabiliriz.
Şimdi örnek bir
uygulama yapalım. Hem tamsayı türünden, hem de ondalıklı sayı türünden diziler
için en büyük ve en küçük elemanı bulma, aritmetik ortalamayı bulma ve elemanların
toplam değerlerini bulma işlemlerini yapmak istediğimizi düşünelim. Her tür
için işlem yapan farklı fonksiyonlar yazacağız. Ancak en son, belli işleri yapan
fonksiyon göstericileri tanımlayacağız ve kullanacağımız türe göre bu fonksiyon
göstericilerine türlere özel yazdığımız fonksiyonları atayacağız. Şimdi bahsettiğimiz
bu yapıyı oluşturmaya başlayalım :
// Tamsayı
üzerinde mi ondalıklı sayı üzerinde mi çalışıyoruz ?
typedef enum _myTypes {
    t_integer = 0,
    t_double = 1,
}MyTypes;
// Üzerinde
çalışacağımız türe göre dizimizi tutacak birlik
typedef union _vals {
    double *dVals;
    int *iVals;
}Vals;
// Üzerinde
çalışacağımız türe göre çağıracağımız fonksiyonların geri dönüş değerlerini
tutacak birlik
typedef union _result {
    double dResult;
    int iResult;
}Result;
// Temel
yapımız
typedef struct _myArray {
    MyTypes type;
    Vals vals;
    double (*GetMathAvr)(int elemNumber,Result res);
    Result (*GetSum)(void const *elems,int elemNumber);
    int (*GetMax)(void const *elems,int elemNumber);
    int (*GetMin)(void const *elems,int elemNumber);
}MyArray;
|
Buradaki tanımlamaları
açıklayalım. MyArray isimli bir yapı tanımladık. Bu yapıda, üzerinde çalışacağımız
tür bilgisini tutan tutan type isimli bir veri elemanı, dizimizin elemanlarını
tutan vals veri elemanı ve dört tane de fonksiyon göstericisi bulunuyor. Eğer
üzerinde çalışacağımız tür tamsayı ise type elemanına t_integer değerini, ondalıklı
sayı ise t_double değerini atayacağız. Bu değeri de her iki tür için de ortak
yazdığımız fonksiyonlarda, tür bilgisini alabilmek için kullanacağız. Yapımız
içerisinde dört tane de fonksiyon göstericisi tanımladık. Bu fonksiyon göstericilerinin
göstereceği fonksiyonların yapacağı işler şunlardır :
1- Dizinin aritmetik
ortalamasını bulacak
2- Dizinin elemanlarının toplam değerini bulacak
3- Dizinin en büyük elemanını bulacak
4- Dizinin en küçük elemanını bulacak
Bu durumda, kullandığımız
türe göre bu göstericilere uygun fonksiyonların adreslerini atayacağız. Farklı
türler için fonksiyonlar çağırırken, farklı fonksiyon isimleri kullanmak zorunda
kalmayacağız, bunun yerine tanımladığımız fonksiyon göstericilerinin adını kullanacağız.
Fonksiyon göstericilerinin
göstereceği fonksiyonların parametre değişkenlerinde (void *) türünün kullanılmasının
nedeni türden bağımsızlığı sağlayabilmektir. Farklı türler için aynı işlemleri
yapan fonksiyonları çağırmak için bir tane fonksiyon göstericisi tanımladık.
Bu göstericiye atayacağımız fonksiyonun parametrik yapısı da göstericinin parametrik
yapısına uygun olmalıdır. Bu durumda kullanılabilecek en uygun tür de (void
*) olacaktır. Fonksiyonlara göndereceğimiz (int *) ve (double *) türünden nesnelerimizi
(void *) türüne tür dönüşümü ile dönüştüreceğiz. Türe bağımlı yazacağımız fonksiyonlarda
ise, parametre değişkenini kullanılan türe göre (int *) veya (double *) türüne
dönüştüreceğiz. Bu yapı biraz karmaşık gibi görünse de birkaç uygulama yapılarak
rahatlıkla anlaşılabilir. Öncelikle türe bağımlı işlemleri yapacak fonksiyonları
tanımlayalım :
Result GetSumDouble(void
const *elems,int elemNumber)
{
    double const *myElems = (double *)elems;
    int i;
    double sum = 0.0;
    Result res;
    for (i = 0; i < elemNumber; ++i)
    sum += myElems[i];
    res.dResult = sum;
    return res;
}
Result
GetSumInt(void const *elems,int elemNumber)
{
    int const *myElems = (int *)elems;
    int i,sum = 0;
    Result res;
    for (i = 0; i < elemNumber; ++i)
       sum += myElems[i];
    res.iResult = sum;
    return res;
} |
Bu iki fonksiyon,
parametre olarak geçilen dizinin elemanlarının değerleri toplamını Result türünden
bir nesnenin ilgili elemanına atamaktadır. Dizi parametresini (void *) türünden
tanımladık. Çünkü bu fonksiyonu yapımız içerisindeki GetSum fonksiyon göstericisine
atayacağız ve parametrik uyum sağlamalıyız. Biz, her iki fonksiyon içerisinde
de tür bilgisine sahip olacağımız için tür dönüşümü ile istediğimiz türe dönüşüm
yapabiliriz. void göstericiler için, gösterici aritmetiği geçerli olmadığı için
bu tür dönüşümünü yapmak zorundayız. Aksi durumda void bir gösterici için köşeli
parantez operatörünü kullanmak mümkün değildir. Biz dizi elemanlarına ulaşmak
için bu operatörü kullanacağız. Bu durumda tür bilgisine de sahip olmamız kaçınılmazdır.
Yukarıdaki fonksiyonlarda da bu işlemi gerçekleştirdik. Tamsayı türü için yazılan
fonksiyonda parametre değişkenini (int *) türüne, ondalıklı sayı türü için yazılan
fonksiyonda ise (double *) türüne dönüştürdük. Bu sayede dizimizin elemanlarına
köşeli parantez operatörü ile ulaşmayı mümkün kıldık.
int GetMaxInt(void
const *elems,int elemNumber)
{
    int const *myElems = (int *)elems;
    int i, maxIndex = 0;
    int max = myElems[0];
    for (i = 1; i < elemNumber; ++i)
    {
       if (myElems[i] > max)
       {
          max = myElems[i];
          maxIndex = i;
       }
    }
    return maxIndex;
}
int GetMaxDouble(void
const *elems,int elemNumber)
{
    double const *myElems = (double *)elems;
    int i, maxIndex = 0;
    double max = myElems[0];
    for (i = 1; i < elemNumber; ++i)
    {
       if (myElems[i] > max)
       {
          max = myElems[i];
          maxIndex = i;
       }
    }
    return maxIndex;
}
|
Yukarıdaki iki
fonksiyon, parametre olarak aldıkları dizilerin en büyük elemanlarının indekslerine
geri dönmektedir. Bu fonksiyonlarda da yukarıda bahsettiğimiz tür dönüşümü işlemini
gerçekleştirdik. En büyük elemanı bulma fonksiyonunun başında, tipik olarak
öncelikle dizinin ilk elemanı en büyük eleman kabul edilir ve ilgili türden
bir değişkene bu değer atanır. Sonraki adımlarda ise dizinin diğer elemanları,
bu değişkenin değeri ile karşılaştırılarak işlemlere devam edilir. İlk fonksiyonda,
bu değişkenin türü int, ikinci fonksiyonda ise double olmalıdır. Dolayısıyla
bu iki fonksiyonu tek bir fonksiyon haline getirmek mümkün değildir. İki farklı
isme sahip fonksiyon yazılması zorunludur. Ancak bir fonksiyon göstericisi tanımlarsak,
bu iki fonksiyonu, tanımlayacağımız bu göstericiye atayabiliriz. En büyük elemanı
bulmak için çağıracağımız fonksiyon her iki durumda da yapı içerisinde tanımladığımız
fonksiyon göstericisinin gösterdiği fonksiyon olacaktır. Yukarıdaki fonksiyonlara
benzer şekilde dizinin en küçük elemanının indeksine dönen fonksiyonlar da yazılır.
int GetMinInt(void
const *elems,int elemNumber)
{
    int const *myElems = (int *)elems;
    int i, minIndex = 0;
    int min = myElems[0];
    for (i = 1; i < elemNumber; ++i)
    {
       if (myElems[i] < min)
       {
          min = myElems[i];
          minIndex = i;
       }
    }
    return minIndex;
}
int GetMinDouble(void
const *elems,int elemNumber)
{
    double const *myElems = (double *)elems;
    int i, minIndex = 0;
    double min = myElems[0];
    for (i = 1; i < elemNumber; ++i)
    {
       if (myElems[i] < min)
       {
          min = myElems[i];
          minIndex = i;
       }
    }
    return minIndex;
}
|
Aritmetik ortalamayı
hesaplayan fonksiyonlar ise şu şekilde tanımlanır :
double GetMathAvrDouble(int
elemNumber,Result res)
{
    return (double)res.dResult / elemNumber;
}
double
GetMathAvrInt(int elemNumber,Result res)
{
    return (double)res.iResult / elemNumber;
}
|
Bu fonksiyonlar,
parametre değişkeni olarak Result türünden bir değişken ve dizinin eleman sayısını
tutan int türünden bir değişken almaktadır. Fonksiyonlar, üzerilerinde çalıştıkları
türe göre, Result türünden nesnenin iResult ya da dResult değerine erişerek
ortalamayı hesaplıyorlar. Bu durumda bu fonksiyonlara gönderilecek Result türünden
değişkenin ilgili veri elemanında, dizi elemanlarının toplam değerleri atanmalıdır.
Zaten biz bu işlemi yapan birer fonksiyon da yazmıştık. Şimdi tanımladığımız
bu yapıyı nasıl kullanacağımızı inceleyelim. Programımız hem tamsayı dizileri,
hem de ondalıklı sayılar dizileri üzerinde işlem yapacak. Kullanıcıdan bu bilgiyi
alacağız. MyArray yapısı türünden bir nesne tanımlayarak elemanlara ilgili değerleri
atayacağız. Bu yapı nesnesinin elemanlarının doldurulması işlemini de bir fonksiyon
ile yapalım. Ancak her iki tür için de tek bir fonksiyon yazacağız. Bu noktada,
yapımızın type elemanından faydalanacağız :
void StartOperation(MyArray
*array,void *myVals)
{
    if (array->type == 0) //integer
    {
       array->vals.iVals = (int *)myVals;
       array->GetSum = GetSumInt;
       array->GetMin = GetMinInt;
       array->GetMax = GetMaxInt;
       array->GetMathAvr = GetMathAvrInt;
    }
    else if (array->type == 1) //double
    {
       array->vals.dVals = (double *)myVals;
       array->GetSum = GetSumDouble;
       array->GetMin = GetMinDouble;
       array->GetMax = GetMaxDouble;
       array->GetMathAvr = GetMathAvrDouble;
    }
} |
Eğer türümüz tamsayı
ise ilk koşul gerçeklenir ve bu bloktaki işlemler yapılır. Bu blokta yapımızın
fonksiyon göstericisi elemanlarına, tamsayılar üzerinde işlem yapan fonksiyonların
adresleri atanmıştır. Eğer türümüz ondalıklı sayı ise ikinci koşul gerçeklenir.
Bu durumda da fonksiyon göstericisi elemanlarına ondalıklı sayılar üzerinde
işlem yapan fonksiyonların adresleri atanır. Bu yapı nesnesini bir defa oluşturduktan
sonra artık farklı fonksiyon isimlerini hatırlamak zorunda kalmayacağız. Göstericilerimiz
ilgili fonksiyonları gösterdiklerine göre, bize düşen sadece yapının fonksiyon
göstericisi isimlerini kullanarak ilgili parametrelerle fonksiyonları çağırmak
olacak. Şimdi programımızın ana bloğunu inceleyelim :
int main()
{
    MyArray array;
    int opNum = 0,i;
    int myIntArray[] = {1,2,3,4,5,6,7,8,9,10};
    double myDoubleArray[] = {1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9.9,10.10};
    Result tmpRes;
    while (opNum < 1 || opNum > 3)
    {
       printf("Tamsayi islemleri icin : 1\n");
       printf("Ondalikli sayi islemleri icin
: 2\n");
       printf("Programdan cikmak icin : 3\n");
       printf("Giriniz..\n");
       scanf("%d",&opNum);
    }
    if (opNum == 1)
    {
       array.type = t_integer;
       StartOperation(&array,(void *)myIntArray);
       printf("Dizinin Elemanlari :\n");
       for (i = 0; i < ARRAY_SIZE; ++i)
          printf("%d\n",array.vals.iVals[i]);
       tmpRes = array.GetSum(array.vals.iVals,ARRAY_SIZE);
       printf("Toplam = %d\n",tmpRes.iResult);
       printf("En Kucuk Eleman = %d\n",array.vals.iVals[array.GetMin(array.vals.iVals,ARRAY_SIZE)]);
       printf("En Buyuk Eleman = %d\n",array.vals.iVals[array.GetMax(array.vals.iVals,ARRAY_SIZE)]);
       printf("Aritmetik Ortalama = %lf\n",array.GetMathAvr(ARRAY_SIZE,tmpRes));
    }
    else if (opNum == 2)
    {
       array.type = t_double;
       StartOperation(&array,(void *)myDoubleArray);
       printf("Dizinin Elemanlari :\n");
       for (i = 0; i < ARRAY_SIZE; ++i)
          printf("%lf\n",array.vals.dVals[i]);
       tmpRes = array.GetSum(array.vals.dVals,ARRAY_SIZE);
       printf("Toplam = %lf\n",tmpRes.dResult);
       printf("En Kucuk Eleman = %lf\n",array.vals.dVals[array.GetMin(array.vals.dVals,ARRAY_SIZE)]);
       printf("En Buyuk Eleman = %lf\n",array.vals.dVals[array.GetMax(array.vals.dVals,ARRAY_SIZE)]);
       printf("Aritmetik Ortalama = %lf\n",array.GetMathAvr(ARRAY_SIZE,tmpRes));
    }
    else if (opNum == 3)
    {
       printf("Program Sonlaniyor...\n");
    }
    return 0;
} |
Kullanıcı klavyeden
1 değerini girerse, yapımızı tamsayı türüne göre, 2 değerini girerse ondalıklı
sayı türüne göre oluşturuyoruz. Daha sonra yapımızın fonksiyon göstericilerini
kullanarak istediğimiz fonksiyonların çağrılmasını sağlıyoruz. Programın çıktı
görüntüleri şöyledir :
Seçeneklerin sunulması
:
    
Tamsayı seçeneğinin
seçilmesi :                                Ondalıklı Sayı Seçeneğinin Seçilmesi :
             
Uygulamanın tamamını
aşağıdan indirebilirsiniz. MinGW 2.05 ve Microsoft Visual C++ 6.0 derleyicilerinde
derlenmiştir.
Bir sonraki makalemizde
görüşmek üzere..
Kaynak kod için tıklayın.
Makale:
Fonksiyon Göstericilerinin Kullanılması C ve Sistem Programlama Çiğdem Çavdaroğlu
|