Bir C programını derlemeye çalıştığımızda, program yukarıdan aşağıya doğru derlenecektir. Bu nedenle, örneğin bir değişkeni üst satırlarda bildirmeden alt satırlarda kullanmaya çalışmak veya kullandıktan sonra bildirmeye çalışmak hatalı bir davranıştır. Çünkü derleyici henüz o değişkenin ne olduğunu (tür bilgisini) anlayamamış ve onu tanımsız bir varlık olarak görmüştür. Benzer bir şekilde derleyici bir fonksiyonun çağrıldığı satıra geldiğinde, bu fonksiyon ile ilgili geri dönüş türünü bilmek durumundadır. Eğer derleyici bu fonksiyon ile daha önce karşılaşmamışsa, onun geri dönüş türünü ototmatik olarak int kabul eder. Şimdi şu koda bakalım:
int main()
{
int sum = func(5, 6);
printf("%d", sum);
return 0;
}
int func(int a, int b)
{
return a + b;
}
Burada derleyici func fonksiyonunun tanımından önce, onun çağrısı ile karşılaşır. Bu yüzden onun geri dönüş değerini otomatik olarak int kabul eder. Daha sonra fonksiyonun tanımı ile karşılaşınca da onun int türünden geri dönüş değerine sahip olduğunu doğrular. Bu nedenle yukarıdaki kod parçasında herhangi bir problem çıkmadan ekrana 11 sayısı basılır. Şimdi de şu örneğe bakalım:
int main()
{
double sum = func2(5.3, 6.4);
printf("%lf", sum);
return 0;
}
double func2(double a, double b)
{
return a + b;
}
İşte burada derleyici func2 fonksiyonunun geri dönüş türünü int olarak ele alacak, fakat bu fonksiyonun tanımı ile karşılaşınca ortaya çelişki (conflict) çıkacaktır. Dolayısıyla derleme zamanında hata ortaya çıkacaktır. Bu sorunu hızlıca çözmek için func2 fonksiyonunun tanımını main fonksiyonunun üstüne alabiliriz:
double func2(double a, double b)
{
return a + b;
}
int main()
{
double sum = func2(5.3, 6.4);
printf("%lf", sum);
return 0;
}
Bu kodu çalıştırdığınızda ekranda 11.7 değerini görebilirsiniz. Fakat bu iş her zaman bu şekilde mi yürüyecektir? Hangi fonksiyonun hangisini önce kullandığını kararlaştırıp her zaman kodu bu şekilde sırayla mı yazmamız gerekir? Peki aşağıdaki gibi birbirlerini kullanan fonksiyonları göz önüne alırsak, bunu az önceki yer değiştirme ile nasıl çözeriz:
void func()
{
func2();
}
void func2()
{
func();
}
Bu tip karmaşalardan kurtulmanın elbette bir yolu vardır. Bildiğiniz gibi bir değişkenin bildirimi (declaration) ve tanımı (definition) farklı anlamlara gelmekteydi. C dilinde bir fonksiyonun bildirimi ve tanımı da birbirinden ayrılabilir. Yani fonksiyonun içeriğini doldurmadan, önce bu fonksiyonun isminin ne olacağını, geri dönüş türünün ne olacağını ve hangi tür parametreleri hangi sırayla alacağını belirleyebiliriz. Bu bildirime fonksiyon bildirimi (function declaration) veya fonksiyon prototipi (function prototype) adı verilir. Bir fonksiyon prototipinin genel biçimi şu şekildedir:
<Geri Dönüş Türü> <Fonksiyon Adı>(<Parametre Türü 1>, <Parametre Türü 2>, …);
Örneğin ilk kod örneğinde tanımladığımız func fonksiyonunun prototipi şöyledir:
int func(int, int);
Bu şekilde derleyiciye böyle bir fonksiyonun var olduğunu (ancak tanımının henüz yapılmadığını) ve geri dönüş türü ile parametre türlerinin neler olduğunu bildirebilirsiniz. Gördüğünüz gibi burada sadece parametre türlerini belirtmeniz (virgül ile ayırarak) yeterlidir. Ancak fonksiyon tanımında olduğu gibi parametrelerin isimlerini de belirtebiliriz. Bu derleyici açısından hiçbir şey ifade etmezken, kodu okuyan kişi için okunabilirliği arttırır:
int func(int a, int b);
Bu arada prototipin sonundaki “;” işaretini de unutmayın. Yoksa yine derleme zamanı hataları ile karşılaşırsınız. Şimdi ilk double geri dönüş türü ile tanımladığımız func2 fonksiyonu örneğini, fonksiyon prototipini kullanarak tekrar yazalım:
double func2(double a, double b);
int main()
{
double sum = func2(5.3, 6.4);
printf("%lf", sum);
return 0;
}
double func2(double a, double b)
{
return a + b;
}
Kodun en yukarısında kullanacağımız func2 fonksiyonu hakkında derleyiciyi bilgilendirdik. Böylelikle tanımını daha sonra yapacağımız bu fonksiyonu ileriki satırlarda rahatlıkla çağırabiliriz. Eğer fonksiyonun prototipini yazıp tanımını yapmazsak veya prototip ile tanımdaki türler birbirine uymazsa yine derleme zamanı hataları alırız (gerçi tanım yoksa derleme zamanında değil bağlama zamanında hata alırız, ancak program yine de derlenmemiş olur). Eğer prototipte herhangi bir geri dönüş türü belirtilmemişse, derleyici onu yine int türüne geri dönecekmiş gibi algılar. Örnek:
func3(double, int);
Ancak bu tehlikeli bir kullanım olduğundan pek tavsiye edilmez. Bir fonksiyonun bilidirmi birden fazla kez yapılabilir. Örneğin aşağıdaki prototipler aynı fonksiyonu belirtmektedir ve bu prototiplerin ard arda yapılması bir sorun oluşturmaz:
int func4(int); func4(int a); int func4(int a);
Ancak aynı isimde ve farklı geri dönüş türü veya parametre türü ile yapılan bildirimler geçersizdir. Örneğin aşağıdaki ikinci func5 bildirimi geçersizdir. Çünkü daha önce func5 isimli bir fonksiyonun bildirimi farklı parametre ve geri dönüş değeri türleri için zaten yapılmış olur:
int func5(double a); double func5(double a);
C dili standartlaştırılmadan önce fonksiyon bildirimlerinde parametrenin içi boş bırakılırdı. Yani bir fonksiyonun sadece ismi ve geri dönüş türünü belirtmek yeterliydi. Yani şöyle bir bildirim aslında fonksiyonun bir parametresinin olmadığı anlamına gelmezdi:
float topla();
C standartlaştığında bu durum değiştirildi. Artık bildirimlere fonksiyonun parametre sayısı ve türleri hakkında bilgiler yazılabiliyordu. Yani yukarıdaki topla fonksiyonunu artık gerçek formunda yazabilirdik:
float topla(float, float);
Ancak burada bir başka problem daha ortaya çıktı. Eski kurallara göre yazılan kodlar yeni kurallar ile uyuşmuyordu. Yani başta yazdığımız topla fonksiyonunun bildirimi için topla(5.5, 4.4) gibi bir çağrı, bildirimin sanki herhangi bir parametresi yokmuş gibi algılandığından hata ile sonuçlanacaktı. Bu nedenle geçmişe yönelik uyumluluğu sağlama amacıyla parametreyi belirten parantezlerin içine void anahtar kelimesinin yazımı önerildi. Böylelikle parametreli ve parametresiz fonksiyonların ayrımı yapılabilecekti. Şimdi aşağıdaki bildirimleri inceleyelim:
float topla(); void ekranaBas(void);
Yukarıdaki bildirimlere göre topla(5.5, 4.4) gibi bir çağrıda hata alınmaz. Çünkü topla fonksiyonunun prototipinde parametre parantezleri içine void anahtar kelimesi yazılmadığından, bu fonksiyonun parametreleri ile ilgili bir bilgi verilmemiş olur. Fakat “ekranaBas” fonksiyonunun parametre parantezleri arasındaki void anahtar kelimesi, bu fonksiyonun kesin olarak parametresiz olduğunu bildirmektedir. Yani aşağıdaki çağrı bu durumda hata ile sonuçlanır:
ekranaBas(56);
C++ dilinde ise parantezler arasında void konulması ve konulmaması arasında bir fark yoktur. Çünkü C++ dilinde bildirim tanımlamaları ile ilgili bazı kurallar ve kısıtlamalar getirilmiştir. C dilinde ise void ile ilgili aynı kuralın C23 standartları ile dile eklenmesi planlanmaktadır. Buna ek olarak geri dönüş türü yazılmayan bildirimlerde, C++ dilinde geri dönüş türü otomatik olarak int türünde kabul edilmez. Böyle bir bildirimde hata ile karşılaşırsınız.
Fonksiyon bildirimleri tıpkı değişken bildirimleri gibi programın herhangi bir yerinde yapılabilir. Ancak bildirimlerin temel amacı kullanılacak olan fonksiyonların erkenden görülebilirliği olduğu için, genellikle kaynak kod dosyasını en yukarısına yapılırlar. Fakat C dilinde en sık kullanılan mekanizma, bildirimlerin bir başlık dosyasında (header file) yapılıp bu başlık dosyasının #include önişlemci bildirimiyle programa dahil edilmesidir. Böylelikle bildirim ve implementasyonları birbirinden ayırmış oluruz.
Bildiğiniz gibi başlık dosyaları “.h” uzantısına sahip olan dosyalardır. Standart kütüphanede bulunan çeşitli fonksiyonlar çeşitli başlık dosyaları içerisinde yer alırlar. Biz standart kütüphaneden bir fonksiyonu çağırdığımızda, onun bildirimi derleyici tarafından bilindiğinden bir sıkıntı çıkmaz. Bu fonksiyonların implementasyonları ise o mimariye özel olarak derlenmiş ve belli uzantılarda dosyaların içine yerleştirilmiştir. Bu dosyalar çeşitli sistemlerde çeşitli derleyicilerin dosyaları altında yer alırlar. Standart kütüphane ile ilgili önemli ve kullanışlı fonksiyonları yakında inceleyeceğiz.
Bildirimi yaparken parametre parantezleri arasında herhangi bir sayıda tür bilgisi belirtirsek, derleyici fonksiyon çağrısı yaparken buradaki sayı ile çağrıda gönderiken argüman sayısını karşılaştırır. Eğer bir eksiklik veya fazlalık varsa derleme zamanında hata oluşur. Eğer parametre ve gönderilen argüman sayılarında bir eşitsizlik yoksa, ancak türler tam olarak uyuşmuyorsa işte burada kapalı tür dönüşümü (implicit type conversion) gerçekleştirilir. Hemen şöyle bir kod örneği verelim:
double func(double a);
int main()
{
int number = 10;
func(number);
}
Burada number değişkeni int türünden ve çağrılan fonksiyonun parametre türü double türünden olduğu için, number değişkeninin değeri double türüne dönüştürüldükten sonra fonksiyon çağrısı yapılır. Hatırlayacağınız gibi küçük türlerden büyük türlere olan dönüşümlerde bilgi kaybı yaşanmazken, büyük türlerden küçük türlere olan dönüşümlerde ise bilgi kaybı söz konusu olabilir.
Şimdi son bir örnek ile yazımızı bitirelim. Bu örnekte, verilen iki sayının OBEB’ini bulup geri döndüren bir fonksiyon tasarlayalım. Bildiğiniz gibi iki sayının OBEB’i (Ortak Bölenlerin En Büyüğü) bu sayıların bölenleri arasından ortak olan en büyük sayıdır. İngilizce’si Greatest Common Divisor (GCD) olan OBEB, başlangıç seviyesi algoritmalarda sıklıkla karşımıza çıkmaktadır. Bu algoritmada bir i sayısını 1’den başlayarak en küçük sayıya kadar arttıracak ve eğer o anki i sayısı bu sayıları tam bölüyorsa, OBEB değerini ona eşitleyeceğiz. Böylelikle işlem sonucu OBEB’i bulmuş olacağız. Şimdi koda bakalım:
// Code 5.2.1: Code5_2_1.c
// Verilen iki sayının OBEB'ini bulma
#include <stdio.h>
int obeb(int sayi1, int sayi2);
int main()
{
int a, b;
printf("Ilk sayiyi girin: ");
scanf("%d", &a);
printf("Ikinci sayiyi girin: ");
scanf("%d", &b);
int sonuc = obeb(a, b);
printf("%d ve %d sayilarinin OBEB'i: %d\n", a, b, sonuc);
return 0;
}
int obeb(int sayi1, int sayi2)
{
int i;
int sonuc = 1;
for(i = 1; i <= sayi1 && i <= sayi2; i++)
{
if(sayi1 % i == 0 && sayi2 % i == 0)
{
sonuc = i;
}
}
return sonuc;
}
Ikinci sayiyi girin: 40
60 ve 40 sayilarinin OBEB’i: 20
Gördüğünüz gibi 6. satırda main fonksiyonunun içinde kullanacağımız obeb fonksiyonu için bildirim yaptık. Daha sonra da 25. satırda onu tanımladık. obeb fonksiyonun içinde tek bir for döngüsü ile OBEB alma işlemini tamamladık. 30. satırdaki koşul ile hangi sayının büyük olduğunu baştan anlamak zorunda kalmadık, çünkü i sayısı en küçük sayıya eşit olduktan sonra bir kez daha artarsa koşul otomatik olarak yanlış olacak ve for döngüsünden çıkılacaktır. Bu arada sonuc değişkeninin ilk değerini 1 olarak belirlediğimize dikkat edin. Böylelikle ortak böleni olmayan sayılar girildiğinde for döngüsüne hiç girilemeyecek ve sonuç da 1 olacaktır.
Bu yazıda fonksiyon prototipleri ile ilgili çeşitli bilgileri aktarmaya çalıştık. Fonksiyon prototiplerine neden ihtiyaç duyduğumuzu, genellikle nerelerde kullandığımızı ve C dili standartlarında ne tip prototiplerin geçerli olduğunu anlatmaya çalıştık. Fonksiyon prototiplerini bundan sonraki örneklerde sıklıkla göreceğiz. Bir sonraki yazıda standart kütüphanede yer alan çeşitli başlık dosyalarını ve bunların içinde ne tür fonksiyon bildirimlerinin bulunduğunu açıklayacağım.



Güzel bir yazı. Teşekkürler.
Baya iyi bir yazı.
Çok güzel olmuş ellerinize sağlık