Şu ana kadar yazdığımız örnekler hep basit programlardan meydana gelmekteydi. Ancak gerçek dünya problemleri oldukça büyük ve karmaşık olabilir. Bu karmaşık problemleri çeşitli algoritmik yaklaşımlarla kağıt üzerinde çözebiliriz. Ancak bunun için önce problemi küçük parçalara ayırmamız gerekmektedir. Algoritma dünyasında problemi daha küçük parçalara bölerek çözme yöntemine böl ve fethet (divide and conquer) tekniği adı verilmektedir. Bu yazıda da bu tip algoritmaları koda nasıl dökebileceğimizden bahsedeceğiz.
Şimdiye kadar bütün kodları main fonksiyonu içerisine yazdık. Ancak bu doğru bir programlama yaklaşımı değildir. Kodları çeşitli fonksiyonlar ve hatta çeşitli dosyalar ile modülerize etmek gerekir. İşte bir C programını modüler hale getirmek için kullanacağımız temel dil özelliğine fonksiyon (function) adı verilmektedir. Fonksiyonlar daha önce pek çok kez karmışıza çıktı: scanf, printf gibi kullandığımız çeşitli yapılar aslında birer fonksiyondu. Bu yapıları anlatırken aslında fonksiyon kavramına da kısaca değinmiştim. Fakat burada daha ayrıntıya gireceğim.
Öncelikle büyük problemlerin parçalanması ve algoritma olarak ifade edilmesi konusuna geri dönelim. Şöyle bir sözde kod yazalım:
Kullanıcıdan üç sayı al ve bunu "sayi1", "sayi2" ve "sayi3" isimli değişkenlere ata Bu üç sayının içlerinden en küçük olanını bul ve onu "minSayi" değişkenine ata "minSayi" isimli değişkenin faktöriyelini hesapla ve sonucu "faktoriyel" isimli değişkene ata "faktoriyel" isimli değişkeni ekrana bas
Oldukça kısa görünen bu sözde kodda aslında birbirinden bağımsız pek çok işlem yapmaktayız. Kullanıcıdan sayıları alma ve ekrana basma kısımlarından ziyade, bu sayılardan en küçük olanını bulma ve ondan sonra da sonucun faktöriyelini alma gibi işlemler de aslında kendi başına birer algoritma oluşturabilir. Şimdi bunu bir akış şemasına dökelim:
Burada kullanıcıdan 3 girdi alıyoruz ve ilkini minSayi değişkenine atıyoruz. Daha sonra bu değişken ile sırasıyla sayi2 ve sayi3 değişkenlerini karşılaştırarak aralarından en küçüğünü minSayi değişkenine atıyoruz. sayac = 1 adımına gelinceye kadar aslında 3 sayı arasından minimum sayı bulma işlemini yapıyoruz. Daha sonra ise sayac ve faktoriyel isimli değişkenler tanımlayarak ilk değerlerini 1 olarak belirliyoruz. Daha sonra da daha önce gördüğümüz gibi döngü ile faktöriyel hesabı yapıp sonuç olarak ortaya çıkan faktoriyel isimli değişkenin değerini ekrana basıyoruz. Bu son anlattığım kısım da aslında bir sayının faktöriyelini alma işlemini yapmaktadır.
Bu kadar kısa bir sözde kod için gerçekten uzun bir akış şeması oldu değil mi? Bir de şöyle düşünün: Minimum sayıyı bulduktan sonra faktöriyel işlemi alıp daha sonra bu 3 sayı arasından benzer biçimde maksimum sayıyı bulup onun da faktöriyelini almak istiyoruz. En sonunda da bu iki değeri ekrana basmak istiyoruz. Burada benzer işlemleri kullanmamıza rağmen bunu bir akış şemasına döktüğümüzde çizim inanılmaz uzayacaktır değil mi? İşte bir programı modüler hale getirmenin aslında bir avantajı da yeniden kulllanılabilir (reusable) parçalar oluşturup bunları tekrar tekrar yazmak zorunda kalmamamızdır.
Şimdi bu akış şemasında belli işlemler yapan kısımları tespit edelim. Öncelikle girdi alma ve çıktı verme kısımlarına dokunmuyoruz. Daha önce söylediğim gibi sayac = 1 adımına kadar olan adımlar aslında 3 sayıdan en küçüğünü bulma işlemini gerçekleştiriyordu. Bu adımdan çıktı adımına kadar olan kısım ise bir sayının faktöriyelini alma işlemini gerçekleştiriyordu. Şimdi bu kısımları akış şemasında “1” ve “2” olarak numaralandıralım:
Burada “1” ile gösterilen kısım 3 sayıdan en küçük olanını alma iken, “2” ile gösterilen kısım bir sayının faktöriyelini alma işlemidir. Şimdi bu kısımları ayrı ayrı akış şemalarına dökelim. Bunu yaparken bir fonksiyon gösterimi kullanmamız gerekecek. Ancak bu gösterimin akış şemalarında belli bir standardı olmadığı için ben kendime uygun bir şeyler yapacağım. Öncelikle verilen 3 sayıdan en küçüğünü çıktı olarak veren bir fonksiyonun akış şemasını oluşturalım:
Burada fonksiyon ismini ve açıklamasını yukarıya düz bir yazı olarak yazmayı tercih ettim. Fonksiyona giren ve fonksiyondan çıkan değişkenleri göstermek için ise yuvarlatılmış dikdörtgen sembolü kullandım. Gördüğünüz gibi bir fonksiyonun belli bir ismi, aldığı girdiler ve verdiği çıktı veya çıktılar olabiliyor. Aslında birazdan C dilinde fonksiyonları implement ederken de benzer bir mekanizma döndüğünü, sadece terimlerin isimlerinin farklı olduğunu fark edeceksiniz. Şimdi de faktöriyel alma fonksiyonu için bir akış şeması çizelim:
Şimdi de bu fonksiyonları daha önceki akış şemamızda gösterelim:
Burada yine bu fonksiyonları kullanırken (yani onları çağırırken) yanları çizgili bir dikdörtgen modeli kullanmayı tercih ettim. Burada yine fonksiyon adı ve parantez içinde bu fonksiyona verilen parametreleri görmektesiniz. Fonksiyonlardan çıkan oklarda ise çıktı değişkeninin ismi yer almaktadır. Bu haliyle algoritma daha temiz bir hal aldı değil mi? Üstelik bu fonksiyonları algoritmanın çeşitli yerlerinde istediğimiz zaman bu şekilde kullanabiliriz. Artık sistematik olarak fonksiyonların ne kadar faydalı olabileceğini gördüğümüze göre bunları C dilinde nasıl yazabileceğimiz konusuna geçebiliriz.
Öncelikle bir fonksiyonun C dilinde nasıl tanımlandığına bakalım. C dilinde bir fonksiyonu tanımlamak (definition) demek, o fonksiyonun ne yapacağını C kodları ile yazmak demektir. C kodunda bir fonksiyonu çağırmak (call, invocation) demek ise o fonksiyona belli değerler vererek onu çalıştırmak ve sonucunda bir çıktı almaktır. Elbette fonksiyonlara bu değerleri vermek veya herhangi bir çıktı almak gibi bir zorunluluğumuz da yoktur. C dilinde bir fonksiyonun tanımı iskelet olarak şu şekilde yapılır:
<Geri Dönüş Değeri Türü> <Fonksiyon İsmi> (<Parametreler>) {…}
“<parametreler>” kısmının kullanım zorunluluğu yoktur. Yani parantezin içi boş bırakılabilir. Ancak “<Geri Dönüş Değeri Türü>” ve “<Fonksiyon İsmi>” belirtilmek zorundadır. Şimdi parametre almayan ve int türünden bir değer döndüren bir fonksiyon tanımlayalım:
int fonksiyon1()
{
// İşlemler...
}
Burada fonksiyonun adı fonksiyon1 iken, fonksiyonun geri dönüş türü int türüdür. Fonksiyonlar tıpkı değişkenler gibi isimlendirilebilirler ve isimlendirme kuralları da değişkenlerde olduğu gibidir. Bu nedenle bu konuyu artık ayrıntılı olarak anlatmayacağım. Fonksiyonun geri dönüş türü (return type) ise adı üstünde bu fonksiyonun geri döndüreceği değerin (yani çıktı olarak çıkan değerin) hangi türde olduğunu belirtir. Parantezlerin içi boş olduğu için fonksiyon herhangi bir değeri girdi olarak alamaz. Blok parantezleri içine ise yine diğer bloklarda olduğu gibi herhangi bir kod yazılabilir.
Peki bu foksiyonun içinde belli bir değeri nasıl geri döndürürüz? Bunu return anahtar kelimesi ile yaparız. Kullanımı şöyledir:
return <Geri Dönüş Değeri>;
Bu değer herhangi bir ifade olabilir. Ancak ifadenin türü fonksiyonun tanımına yazdığımız türden olmalıdır. Örneğin yukarıda yazdığımız fonksiyon1 isimli fonksiyonun içerisine bir şeyler yazıp bir değer geri döndürelim:
int fonksiyon1()
{
int sayi1 = 5;
sayi1++;
return sayi1;
}
Yukarıda tıpkı main fonksiyonunda defalarca yaptığımız gibi önce sayi1 isimli ve int türünden bir değişken tanımlayıp ona ilk değer verdik. Daha sonra da değerini 1 arttırdık. Bu değişkeni return anahtar kelimesi ile fonksiyondan geri döndürdük. Böylelikle bu fonksiyonu bir yerde çağırdığımızda, fonksiyondan her zaman 6 değeri dönecektir.
return anahtar kelimesinin bir kullanım amacı da fonksiyonu o satırda sonlandırmaktır. Derleyici bu anahtar kelime ile karşı karşıya geldiğinde, o anda üzerinde çalıştığı fonksiyonu sonlandırır ve onu çağıran fonksiyonun çağrıldığı satırdan çalışmaya devam eder.
Peki fonksiyon çağırma (function call) nedir? C’de fonksiyonları bu şekilde tanımlasak bile, onları çağırmadan hiçbir işe yaramazlar. Sadece bir taslak olarak kodda dururlar. Onları isimleri ile beraber “()” parantezlerini kullanarak çağırabiliriz. Örneğin yukarıdaki fonksiyon1 fonksiyonunu main fonksiyonunda bir yerlerde şu şekilde çağırabiliriz:
fonksiyon1();
Bu çağrı sonucunda fonksiyon 6 değerini döndürecektir. Fakat nereye döndürecektir? Elbette bu fonksiyondan geri dönen değerleri yakalayan bir değişken de tanımlamamız gerekir:
int deger = fonksiyon1();
Bu çağrıdan sonra deger isimli değişkenin değeri 6 olacaktır. Peki değer döndürmeyen fonksiyonlar var mıdır? C dilinde bir fonksiyon ya bir değer döndürür ya da hiç değer döndürmez. C dilinde birden fazla değer döndüren fonksiyonların yazılması sözdizimsel olarak mümkün değildir. Ancak bunu sağlamanın bazı yolları vardır ki bunlara ileride değineceğiz. Şimdi öncelikle değer döndürmeyen fonksiyonlardan bahsedelim. C dilinde void isimli gizemli bir anahtar kelime vardır. Bu anahtar kelimenin pek çok kullanım alanı olsa da fonksiyonların geri dönüş türü yerine yazıldığında o fonksiyonun herhangi bir değer döndürmeyeceğini belirtir. Örnek:
void fonksiyon2() {
// İşlemler...
}
Bunun anlamı bu fonksiyon return anahtar kelimesini kullanmak zorunda değildir (ama kullanabilir de). Fakat geri dönüş türü void türünden (evet aslında void bir türdür) olmayan fonksiyonlarda eğer bir return bildirimi yapılmazsa, derleyici bununla ilgili size bir uyarı verir, fakat hata vermez. Örneğin ekrana yazı yazan bir fonksiyon, aslında bir yere değer döndürmeyen bir fonksiyondur:
void ekranaBas() {
puts("Tamam bastim!");
}
Peki geri dönüş türü yerine herhangi bir şey yazmazsak ne olur? İlk başta bunun bir zorunluluk olduğunu söylesek de aslında C++ dilinde bunun katı bir zorunluluğu bulunmaktadır. Ancak C dilinde herhangi bir geri dönüş türü belirtilmeyen bir fonksiyon int veri türünden değer döndürüyormuş gibi algılanır. Yani şu iki tanımlama birbirinin aynısıdır:
fonksiyon3() {
}
int fonksiyon3() {
}
Fakat bu şekilde bir kullanım kesinlikle önerilmez. Okunabilirliği azaltabileceği gibi programda çeşitli hataların çıkmasına da yol açabilir. Şimdi de fonksiyonlara nasıl değer veya değerler verebileceğimize bakalım. Tıpkı geri dönüş türünde olduğu gibi fonksiyonlara verilen değerler için de bir tür bilgisi söz konusudur. Fonksiyon tanımında, fonksiyonun bir isim ve tür bilgisi ile aldığı değişkenlere parametre (parameter) adı verilir. Parametreler fonksiyon tanımında şu şekilde belirtilirler:
<Geri Dönüş Değeri Türü> <Fonksiyon İsmi> (<Parametre Türü> <Parametre Adı>, <Parametre Türü> <Parametre Adı>) {…}
Aslında parametre tanımlama işi tıpkı değişken bildirimlerine benzemektedir. Bununla ilgili bir örnek yapalım. Bu örneğimizde int türünden bir parametre alan ve bu parametrenin karesini hesaplayıp yine int türünde değer olarak geri döndüren “kare” isimli bir fonksiyon yazalım:
int kare(int a) {
return a * a;
}
Gördüğünüz gibi sonucu geri dönüş türüne uygun olduğu sürece return kısmına herhangi bir ifade yazabiliriz. Burada tek bir parametre alıp yine bir değer döndüren fonksiyon örneği görülmektedir. Şimdi üç farklı türden parametre alıp bunları ekrana basan bir fonksiyon tanımlayalım:
void kare(char a, int b, double c) {
printf("%c, %b, %c", a, b, c);
}
Gördüğünüz gibi parametreler virgüller ile ayrılmaktadır. Fakat bu işe ilk başlayanların sık yaptığı bir hata vardır. Hatırlarsanız değişkenleri bildirirken aynı türden değişkenleri virgüller ile ayırıp sık sık tür bilgisi yazmaktan kurtuluyorduk:
int a, b, c;
Fakat bunu fonksiyon parametrelerinde yapmak mümkün değildir:
double fonksiyon4(int a, b, c)
{...}
Burada derleme zamanında hata alırsınız. Parametrelerin türleri aynı olsa bile ayrı ayrı belirtilmek zorundadırlar:
double fonksiyon4(int a, int b, int c)
{...}
Parametreli fonksiyonları çağırmak için yine çağrı parantezinin içine değerler yazılarak virgüller ile ayrılır. Ancak bu değerler çağrı ifadesinde artık argüman (argument) adını alır. Parametreler fonksiyonu tanımlarken verilen tür ve isim bilgileri iken, argümanlar fonksiyonları çağırıken verilen değerlerdir. Şimdi hem parametre alan hem de bir değer döndüren bir fonksiyonun tanımını ve çağrılmasını inceleyelim:
double toplam(double a, double b) {
return a + b;
}
double sonuc = toplam(5.7, 6.3);
Burada toplam fonksiyonu çağrıldığında, a parametresine 5.7 değeri ve b parametresine ise 6.3 değeri atanacaktır. Sonuç olarak ise bu iki değerin toplamı sonuc değişkenine geri döndürülecektir. Fonksiyonlara argüman olarak sadece sabitler değil, return anahtar kelimesinde olduğu gibi değer belirten çeşitli ifadeler de yazılabilir:
double deger1 = 5.7; double deger2 = 6.9; double sonuc = toplam(deger1 + deger2, deger2);
Aslında fonksiyon parametrelerinin yazımında C dilinde eski biçim (old-style) ve yeni biçim (new-style) olarak adlandırılan iki biçim olsa da bu konuya burada değinmek istemiyorum. Çünkü hem eski biçim artık neredeyse hiç kullanılmıyor, hem de C++’ta bu biçim tamamen ortadan kaldırılmıştır.
Son olarak fonksiyon tanımlamalarının ve çağrılarının yerinden bahsedip bir örnek ile yazıyı sonlandıracağım. Fonksiyonlar C dilinde iç içe tanımlanamazlar. Yani bir fonksiyonun içinde başka bir fonksiyon tanımlanmasına izin verilmemiştir:
int fonksiyon1() {
int fonksiyon2() {
// Geçersiz!
}
}
Doğal olarak her şeyin çalışmaya başladığı main fonksiyonunda da başka bir fonksiyon tanımlanamaz. Ancak kaynak kodda herhangi bir bloğun yer almadığı yerlerde fonksiyonlar tanımlanabilir. Ayrıca fonksiyonlar çağrılmadan önce tanımlanmış olmaları gerekir. Şöyle bir örnek verelim:
int main() {
fonksiyon();
return 0;
}
void fonksiyon() {...}
Yukarıdaki fonksiyon çağrısı sırasında derleme zamanında hata meydana gelir. Çünkü derleyici henüz fonksiyon isimli bir fonksiyonun varlığından habersizdir. Bunu çözmek için çağrıdan önce, yani main fonksiyonundan önce bu tanımlamayı yapmalıyız:
void fonksiyon() {...}
int main() {
fonksiyon();
return 0;
}
İleride fonksiyon prototipleri konusunu gördüğümüzde bu konuda biraz daha rahat edeceğiz. Fonksiyon çağrıları ise fonksiyon tanımlarının altında ve herhangi bir fonksiyonun içerisinde yapılabilir. Fonksiyonlarla ilgili şu ana kadar gördüğümüz terimleri bir tabloda toparlayalım:
| Terim | İngilizce Karşılığı | Açıklama |
|---|---|---|
| Fonksiyon | Function | Belli bir işi yerine getiren kod parçasıdır. |
| Geri Dönüş Türü | Return Type | Fonksiyondan geri dönen değerin olması gereken türünü belirtir. |
| Geri Dönüş Değeri | Return Value | Fonksiyondan geri dönen değeri belirtir. |
| Parametre | Parameter | Fonksiyon tanımında yer alan ve fonksiyona girecek olan değerlerin türünü, ismini ve sırasını belirtmeye yarayan yapılardır. |
| Argüman | Argument | Fonksiyonu çağırma sırasında, fonksiyonun parametrelerine verilen değerlerdir. |
| Fonksiyon Tanımı | Function Definition | Fonksiyonun alacağı girdilerin, vereceği çıktıların ve yapacağı işin koda dökülmüş halini temsil eder. |
| Fonksiyon Çağırma | Function Call / Invocation | Tanımı yapılan fonksiyonu çalıştırmayı, yani içerisinde bulunan kodları işlemeyi belirtir. |
| Fonksiyon Gövdesi | Function Body | Fonksiyonun çalıştırması gereken ve blok parantezleri içerisinde yer alan deyimleri belirtir. |
Şimdi en başta verdiğimiz sözde kodun son halini belirten Figür 5’teki akış şemasına göre, bu sözde kodun C dili ile impelmentasyonunu yapalım:
// Code 5.1.1: Code5_1_1.c
// C'de fonksiyonların kullanımı
#include <stdio.h>
int minimum(int a, int b, int c)
{
int minSayi = a;
if(b < minSayi)
{
minSayi = b;
}
if(c < minSayi)
{
minSayi = c;
}
return minSayi;
}
int faktoriyelAl(int sayi)
{
int faktoriyel = 1;
for(int i = 1; i <= sayi; i++)
{
faktoriyel *= i;
}
return faktoriyel;
}
int main()
{
int sayi1;
printf("Ilk sayiyi girin: ");
scanf("%d", &sayi1);
int sayi2;
printf("Ikinci sayiyi girin: ");
scanf("%d", &sayi2);
int sayi3;
printf("Ucuncu sayiyi girin: ");
scanf("%d", &sayi3);
int minSayi = minimum(sayi1, sayi2, sayi3);
int faktoriyel = faktoriyelAl(minSayi);
printf("Sonuc: %d\n", faktoriyel);
return 0;
}
Ikinci sayiyi girin: 5
Ucuncu sayiyi girin: 6
Sonuc: 120
Burada 6. satırda 3 sayı arasından minimum sayıyı bulan ve geri döndüren fonksiyon olan minimum fonksiyonu tanımlanmıştır. Bu fonksiyon main fonksiyonunun içerisinde 53. satırda çağrılıp sonucu minSayi isimli değişkene atanmıştır. Bu çağırma sonucunda verilen sayi1 argümanının değeri a parametresine, sayi2 argümanının değeri b parametresine ve sayi3 argümanının değeri c parametresine atılmıştır. Daha sonra örnekte içlerinden en küçüğü olan 5 değeri geri döndürülmüştür.
23. satırda ise faktöriyel alma fonksiyonunu görmektesiniz. Bu fonksiyon 55. satırda main fonksiyonunun içerisinde çağrılıp sonuç faktoriyel isimli değişkene atılmıştır. En sonundaysa 57. satırda bu değer ekrana basılmıştır. Burada gördüğünüz gibi bir fonksiyon aslında başka bir fonksiyonun içerisinden çağrılmaktadır. Burada fonksiyon çağrısı yapılan main fonksiyonuna Çağıran Fonksiyon (Calling Function) veya kısaca Çağırıcı (Caller) adı verilirken, minimum ve faktoriyelAl gibi çağrılan fonksiyonlara ise Çağrılan Fonksiyon (Called Function) adı verilmektedir. Bu terimler tam olarak resmi olmasa da açıklamada kolaylık sağlarlar.
Son olarak main fonksiyonundan bahsedelim. Aslında main de tanımladığımız diğer fonksiyonlar gibi bir fonksiyon tanımıdır. Ancak onun çağrısı sizin gördüğünüz biçimde değil, derleyici tarafından yapılmaktadır. Bu fonksiyon her şeyin başladığı ve bittiği fonksiyondur. Ayrıca bu fonksiyonun bir de geri dönüş değeri bulunmaktadır. Bu değer int türündedir. Bu geri dönüş ise işletim sistemine yapılır ve onu bir mesaj iletimi sağlar. Burada 0 değerinin geri dönmesi programın sorunsuz bir şekilde çalıştırılıp sonlandırıldığı anlamını taşır. Eğer main fonksiyonu 0 dışında bir değer ile geri dönerse, bu durumda işletim sistemi programın başarısız olduğunu anlar.
Bununla birlikte main fonksiyonunda herhangi bir return deyimi yer almayabilir. Bu durumda fonksiyon C99 ve sonrası standartlarda 0 değeri ile geri döner. C99 öncesi standartlarda ise ne döneceği tanımsızdır (undefined). Ayrıca main fonksiyonunu void geri dönüş türü ile tanımlamak da mümkündür, ancak bu pek tercih edilmez:
void main() {
}
Bu bölümde modüler program yazmanın kalbini oluşturan fonksiyonlar konusuna güzel bir giriş yaptık. Bu belki de yazdığım en uzun yazılardan biri oldu. Ancak fonksiyonlar ile ilgili çok fazla kavram ve özellik bulunduğundan ve her birini ayrı ayrı işlemenin kafa karışıklığına neden olacağını düşündüğümden, temel özellikleri bu yazıda açıklamaya çalıştım. Bir sonraki yazıda C dilinde oldukça kullanışlı bir yere sahip olan fonksiyon bildirimlerinden söz edeceğim.







