C Programlama Dersi – X

Bu yazıda öğrenecekleriniz:

Bazı Aritmetik Fonksiyonlar

Geçen dersimizde, fonksiyonları ve bunları nasıl kullanılacağını görmüştük. Ayrıcakütüphanelerin hazır fonksiyonlar içerdiğinden bahsetmiştik. Bazı matematikselişlemlerin kullanımı sıkça gerekebileceği için bunları bir liste hâlinde vermeninuygun olduğuna inanıyorum. Böylece var olan aritmetik fonksiyonları tekrar tekrartanımlayarak zaman kaybetmezsiniz.
•double ceil( double n ) : Virgüllü n sayısını, kendisinden büyük olan ilk tam sayıya tamamlar. Örneğin ceil(51.4) işlemi,52 sonucunu verir.
•double floor( double n ) : Virgüllü n sayısının,virgülden sonrasını atarak, bir tam sayıya çevirir. floor(51.4) işlemi, 51sayısını döndürür.
•double fabs( double n ) : Verilen n sayısının mutlakdeğerini döndürür. fabs(-23.5), 23.5 değerini verir.
•double fmod( double a, double b ) : asayısının b sayısına bölümünden kalanı verir. (Daha önce gördüğümüz modül(%) operatörü, sadece tam sayılarda kullanılırken, fmod fonksiyonu virgüllüsayılarda da çalışır.)
•double pow( double a, double b ) : Üsteldeğer hesaplamak için kullanılır; ab değerini verir.
•double sqrt( double a ) : a’nın karekökünü hesaplar.

Yukarda verilmiş fonksiyonlar, matematik kütüphanesi ( math.h ) altındadır.Bu fonksiyonlardan herhangi birini kullacağınız zaman, program kodununun başına#include<math.h> yazmalısınız. Ayrıca derleyici olarak gcc’yle çalışıyorsanız,derlemek için -lm parametresini eklemeniz gerekir. (Örneğin: “gcc test.c -lm”gibi…)

Bellek Yapısı ve Adresler

Şimdiye kadar değişken tanımlamayı gördük. Bir değişken tanımlandığında, arkaplânda gerçekleşen olaylara ise değinmedik. Hafızayı küçük hücrelerden oluşmuş bir blok olarak düşünebilirsiniz. Bir değişken tanımladığınızda, bellek bloğundangerekli miktarda hücre, ilgili değişkene ayrılır. Gereken hücre adedi, değişkentipine göre değişir. Şimdi aşağıdaki kod parçasına bakalım:
#include<stdio.h>
int main( void )
{
// Degiskenler tanımlanıyor:
int num1, num2;
float num3, num4;
char i1, i2;

// Degiskenlere atama yapiliyor:
num1 = 5;
num2 = 12;
num3 = 67.09;
num4 = 1.71;
i1 = ‘H’;
i2 = ‘p’;

return 0;
}

Yukarda bahsettiğimiz hücrelerden oluşan bellek yapısını, bu kod parçası içinuygulayalım. Değişken tiplerinden int’in 2 byte, float’un 4 byte ve char’ın 1 byteyer kapladığını kabul edelim. Her bir hücre 1 byte’lık alanı temsil etsin.Değişkenler için ayrılan hafıza alanı, 4300 adresinden başlasın. Şimdi bunları temsili bir şekle dökelim:

[Sample Memory Structure]

Bir değişken tanımladığımızda, bellekte gereken alan onun adına rezerve edilir.Örneğin ‘int num1’ yazılması, bellekte uygun bir yer bulunup, 2 byte’ın, num1 değişkeni adına tutulmasını sağlıyor. Daha sonra num1 değişkenine değer atarsak, ayrılan hafıza alanına 5 sayısı yazılıyor.Aslında, num1 ile ilgili yapacağınız bütün işlemler, 4300 adresiyle 4302adresi arasındaki bellek hücrelerinin değişmesiyle alakalıdır. Değişken dediğimiz;uygun bir bellek alanının, bir isme revize edilip, kullanılmasından ibarettir.

Bir parantez açıp, küçük bir uyarı da bulunalım. Şeklimizin temsili olduğunuunutmamak gerekiyor. Değişkenlerin bellekteki yerleşimi bu kadar ‘uniform’olmayabilir. Ayrıca başlangıç adresini 4300 olarak belirlememiz keyfiydi.Sayılar ve tutulan alanlar değişebilir. Ancak belleğin yapısının, aşağı yukarı böyle olduğunu kabul edebilirsiniz.

Pointer Mekanizması

Bir değişkene değer atadığımızda, aslında bellek hücrelerini değiştirdiğimizisöylemiştik. Bu doğru bir tanım ama eksik bir noktası var. Bellek hücrelerinideğiştermemize rağmen, bunu direkt yapamaz; değişkenleri kullanırız. Bellekhücrelerine direkt müdahâle Pointer’lar sayesinde gerçekleşir.

Pointer, birçok Türkçe kaynakta ‘işaretçi’ olarak geçiyor. Direkt çevirirsenizmantıklı. Ancak terimlerin özünde olduğu gibi öğrenilmesinin daha yararlı olduğunudüşünüyorum ve ben Pointer olarak anlatacağım. Bazı yerlerde işaretçi tanımı görürseniz, bunun pointer ile aynı olduğunu bilin. Şimdi gelelim Pointer’in neolduğuna…

Değişkenler bildiğiniz gibi değer (sayı, karakter, vs…) tutar. Pointer’lar iseadres tutan değişkenlerdir. Bellekten bahsetmiştik; küçük hücrelerin oluşturduğuhafıza bloğunun adreslere ayrıldığını ve değişkenlerin bellek hücrelerineyerleştiğini gördük. İşte pointer’lar bu bellek adreslerini tutarlar.

Pointer tanımlamak oldukça basittir. Sadece değişken adının önüne ‘*’ işaretigetiririz. Dikkat edilmesi gereken tek nokta; pointer’ı işaret edeceği değişkentipine uygun tanımlamaktır. Yani float bir değişkeni, int bir pointer ileişaretlemeğe çalışmak yanlıştır! Aşağıdaki örneğe bakalım:
#include<stdio.h>
int main( void )
{
// int tipinde değişken
// tanımlıyoruz:
int xyz = 10, k;
// int tipinde pointer
// tanımlıyoruz:
int *p;

// xyz değişkeninin adresini
// pointer’a atıyoruz.
// Bir değişken adresini ‘&’
// işaretiyle alırız.
p = &xyz;

// k değişkenine xyz’nin değeri
// atanır. Pointer’lar değer tutmaz.
// değer tutan değişkenleri işaret
// eder. Başına ‘*’ koyulduğunda,
// işaret ettiği değişkenin değerini
// gösterir.
k = *p;

return 0;
}

Kod parçasındaki yorumları okuduğunuzda, pointer ile ilgili fikriniz olacaktır.Pointer adres tutan değişkenlerdir. Şimdiye kadar gördüğümüz değişkeninlerinsaklayabildiği değerleri tutamazlar. Sadece değişkenleri işaret edebilirler.Herhangi bir değişkenin adresini pointer içersine atamak isterseniz, değişkenadının önüne ‘&’ getirmeniz gerekir. Bundan sonra o pointer, ilgili değişkeniişaret eder. Eğer bahsettiğimiz değişkenin sahip olduğu değeri pointer ilegöstermek veya değişken değerini değiştirmek isterseniz, pointer başına ‘*’getirerek işlemlerinizi yapabilirsiniz. Pointer başına ‘*’ getirerek yapacağınızher atama işlemi, değişkeni de etkileyecektir. Daha kapsamlı bir örnek yapalım:
#include<stdio.h>
int main( void )
{
int x, y, z;
int *int_addr;
x = 41;
y = 12;
// int_addr x degiskenini
// isaret ediyor.
int_addr = &x;
// int_addr’in isaret ettigi
// degiskenin sakladigi deger
// aliniyor. (yani x’in degeri)
z = *int_addr;
printf( “z: %d\n”, z );
// int_addr, artik y degiskenini
// isaret ediyor.
int_addr = &y;
// int_addr’in isaret ettigi
// degiskenin sakladigi deger
// aliniyor. (yani y’nin degeri)
z = *int_addr;
printf( “z: %d\n” ,z );

return 0;
}

Bir pointer’in işaret ettiği değişkeni program boyunca sürekli değiştirebilirsiniz.Yukardaki örnekte, int_addr pointer’i, önce x’i ve ardından y’yi işaret etmiştir.Bu yüzden, z değişkenine int_addr kullanarak yaptığımız atamalar, her seferindefarklı sonuçlar doğurmuştur. Pointer kullanarak, değişkenlerin sakladığı değerleride değiştirebiliriz. Şimdi bununla ilgili bir örnek inceleyelim:
#include<stdio.h>
int main( void )
{
int x, y;
int *int_addr;
x = 41;
y = 12;
// int_addr x degiskenini
// isaret ediyor
int_addr = &x;
// int_addr’in isaret ettigi
// degiskenin degerini
// degistiriyoruz
*int_addr = 479;
printf( “x: %d y: %d\n”, x, y );
int_addr = &y;

return 0;
}

Kodu derleyip, çalıştırdığınızda, x’in değerinin değiştiğini göreceksiniz.Pointer başına ‘*’ getirip, pointer’a bir değer atarsanız; aslındaişaret ettiği değişkene değer atamış olursunuz. Pointer ise hiç değişmeden, aynı adres bilgisini tutmaya devam edecektir.

Pointer tutan Pointer’lar

Pointer’lar, gördüğümüz gibi değişkenleri işaret ederler. Pointer’da birdeğişkendir ve onu da işaret edecek bir pointer yapısı kullanılabilir. Geçensefer ki bildirimden farkı, pointer değişkenini işaret edecek bir değişkentanımlıyorsanız; başına ‘**’ getirmeniz gerekmesidir. Buradaki yıldız sayısı değişebilir. Eğer, pointer işaret eden bir pointer’i işaret edecek bir pointertanımlamak istiyorsanız, üç defa yıldız ( *** ) yazmanız gerekir. Evet,cümle biraz karmaşık, ama kullanım oldukça basit! Pointer işaret eden pointer’ları aşağıdaki örnekte bulabilirsiniz:
#include<stdio.h>
int main( void )
{
int r = 50;
int *p;
int **k;
int ***m;
printf( “r: %d\n”, r );
p = &r;
k = &p;
m = &k;
***m = 100;
printf( “r: %d\n”, r );

return 0;
}

Yazmış olduğumuz kod içersinde kimin neyi gösterdiğini grafikle daha iyianlayabiliriz:

[Pointer to Pointer Structure]

Birbirini gösteren Pointer’ları ilerki derslerimizde, özellikle dinamik bellektahsis ederken çok ihtiyaç duyacağımız bir yapı. O yüzden iyice öğrenmek gerekiyor.

Referansla Argüman Aktarımı

Fonksiyonlara nasıl argüman aktaracağımızı biliyoruz. Hatırlayacağınız gibiparametrelere değer atıyorduk. Bu yöntemde, kullandığınız argümanların değerideğişmiyordu. Fonksiyona parametre olarak yollanan argüman hep aynı kalıyordu.Fonksiyon içinde yapılan işlemlerin hiçbiri argüman değişkeni etkilemiyordu.Sadece değişken değerinin aktarıldığı ve argümanın etkilenmediği bu duruma,”call by value” veya “pass by value” adı verilir. Bu isimleribilmiyor olsanız dahi, şu ana kadar ki fonksiyon çalışmaları böyleydi.

Geriye birden çok değer dönmesi gereken veya fonksiyonun içersinde yapacağınızdeğişikliklerin, argüman değişkene yansıması gereken durumlar olabilir. İşte bugibi zamanlarda, “call by reference” veya “pass by reference” olarakisimlendirilen yöntem kullanılır. Argüman değer olarak aktarılmaz; argüman olandeğişkenin adres bilgisi fonksiyona aktarılır. Bu sayede fonksiyon içersindeyapacağınız her türlü değişiklik argüman değişkene de yansır.

Söylediklerimizi uygulamaya dökelim ve kendisine verilen iki sayının yerlerinideğiştiren bir fonksiyon yazalım. Yani kendisine a ve b adında iki değişkenyollanıyorsa, a’nın değerini b; b’nin değeriniyse a yapsın.
#include<stdio.h>
// Kendisine verilen iki degiskenin
// degerlerini degistirir.
// Parametreleri tanimlarken baslarina
// ‘*’ koyuyoruz.
void swap( int *x, int *y )
{
int temp;
temp = *x;
*x = *y;
*y = temp;
}
int main( void )
{
int a, b;
a = 12;
b = 27;
printf( “a: %d b: %d\n”, a, b );
// Argumanları aktarırken, baslarina
// ‘&’ koyuyoruz.
swap(&a, &b);
printf( “a: %d b: %d\n”, a, b );

return 0;
}

Referans yoluyla aktarım olmasaydı, iki değişkenin değerlerini fonksiyonkullanarak değiştiremezdik. Eğer yazdığınız fonksiyon birden çok değer döndürmekzorundaysa, referans yoluyla aktarım zorunlu hâle geliyor. Çünkü daha önceişlediğimiz return ifadesiyle sadece tek bir değer döndürebiliriz. Örneğin birbölme işlemi yapıp, bölüm sonucunu ve kalanı söyleyen bir fonksiyon yazacağımızı düşünelim. Bu durumda, bölünen ve bölen fonksiyona gidecek argümanlar olurken;kalan ve bölüm geriye dönmelidir. return ifadesi geriye tek bir değer vereceğinden,ikinci değeri alabilmek için referans yöntemi kullanmamız gerekir.
#include<stdio.h>
int bolme_islemi( int bolunen, int bolen, int *kalan )
{
*kalan = bolunen % bolen;
return bolunen / bolen;
}
int main( void )
{
int bolunen, bolen;
int bolum, kalan;
bolunen = 13;
bolen = 4;
bolum = bolme_islemi( bolunen, bolen, &kalan );
printf( “Bölüm: %d Kalan: %d\n”, bolum, kalan );

return 0;
}

Fonksiyon Prototipleri

Bildiğiniz gibi fonksiyonlarımızı, main( ) üzerine yazıyoruz. Tek kısabir fonksiyon için bu durum rahatsız etmez; ama uzun uzun 20 adet fonksiyonolduğunu düşünün. main( ) fonksiyonu sayfalar dolusu kodun altındakalacak ve okunması güçleşecektir. Fonksiyon prototipleri burada devreye girer.

Bir üstte yazdığımız programı tekrar yazalım. Ama bu sefer, fonksiyon prototipiyapısına uygun olarak bunu yapalım:
#include<stdio.h>
int bolme_islemi( int, int, int * );
int main( void )
{
int bolunen, bolen;
int bolum, kalan;
bolunen = 13;
bolen = 4;
bolum = bolme_islemi( bolunen, bolen, &kalan );
printf( “Bölüm: %d Kalan: %d\n”, bolum, kalan );

return 0;
}
int bolme_islemi( int bolunen, int bolen, int *kalan )
{
*kalan = bolunen % bolen;
return bolunen / bolen;
}

bolme_islemi( ) fonksiyonunu, main( ) fonksiyonundan önceyazmadık. Sadece böyle bir fonksiyon olduğunu ve alacağı parametre tiplerinibildirdik. ( İsteseydik parametre adlarını da yazabilirdik ama buna gerekyok. ) Daha sonra main( ) fonksiyonu altına inip, fonksiyonu yazdık.

Öğrendiklerimizi pekiştirmek için yeni bir program yazalım. Fonksiyonumuz, kendisineargüman olarak gönderilen bir pointer’i alıp; bu pointer’in bellekteki adresini,işaret ettiği değişkenin değerini ve bu değişkenin adresini göstersin.
#include<stdio.h>
void pointer_detayi_goster( int * );
int main( void )
{
int sayi = 15;
int *pointer;
// Degisken isaret ediliyor.
pointer = &sayi;
// Zaten pointer oldugu icin ‘&’
// isaretine gerek yoktur. Eger
// bir degisken olsaydi, basina ‘&’
// koymamiz gerekirdi.
pointer_detayi_goster( pointer );

return 0;
}
void pointer_detayi_goster( int *p )
{
// %p, bellek adreslerini gostermek icindir.
// 16 tabaninda (Hexadecimal) sayilar icin kullanilir.
// %p yerine, %x kullanmaniz mumkundur.
printf( “Pointer adresi\t\t\t: %p\n”, &p );
printf( “İşaret ettiği değişkenin adresi\t: %p\n”, p );
printf( “İşaret ettiği değişkenin değeri\t: %d\n”, *p );
}

Fonksiyon prototipi, “Function Prototype”dan geliyor. Bunun güzel bir çeviriolduğunu düşünmüyorum. Ama aklıma daha uygun bir şey gelmedi. Öneriniz varsadeğiştirebiliriz.

Rekürsif Fonksiyonlar

Bir fonksiyon içersinden, bir diğerini çağırabiliriz. Rekürsif fonksiyonlar,fonksiyon içersinden fonksiyon çağırmanın özel bir hâlidir. Rekürsif fonksiyon birbaşka fonksiyon yerine kendisini çağırır ve şartlar uygun olduğu sürece butekrarlanır. Rekürsif, Recursive kelimesinden geliyor ve tekrarlamalı, yinelemelianlamını taşıyor. Kelimenin anlamıyla, yaptığı iş örtüşmekte.

Rekürsif fonksiyonları aklımızdan çıkartıp, bildiğimiz yöntemle 1, 5, 9, 13 serisinioluşturan bir fonksiyon yazalım:
#include<stdio.h>
void seri_olustur( int );
int main( void )
{
seri_olustur( 1 );
}
void seri_olustur( int sayi )
{
while( sayi <= 13 ) {
printf(“%d “, sayi );
sayi += 4;
}
}

Bu fonksiyonu yazmak oldukça basitti. Şimdi aynı işi yapan rekürsif bir fonksiyonyazalım:
#include<stdio.h>
void seri_olustur( int );
int main( void )
{
seri_olustur( 1 );
}
void seri_olustur( int sayi )
{
if( sayi <= 13 ) {
printf(“%d “, sayi );
sayi += 4;
seri_olustur( sayi );
}
}

Son yazdığımız programla, bir önce yazdığımız program aynı çıktıları üretir.Ama birbirlerinden farklı çalışırlar. İkinci programın farkını akış diyagramınabakarak sizler de görebilirsiniz. Rekürsif kullanım, fonksiyonun tekrar tekrarçağrılmasını sağlamıştır.

[Recursive Sample 1]

Daha önce faktöriyel hesabı yapan program yazmıştık. Şimdi faktöriyel hesaplayanfonksiyonu, rekürsif olarak yazalım:
#include<stdio.h>
int faktoriyel( int );
int main( void )
{
printf( “%d\n”, faktoriyel(5) );
}
int faktoriyel( int sayi )
{
if( sayi > 1 )
return sayi * faktoriyel( sayi-1 );
return 1;
}

Yukardaki programın detaylı bir şekilde akış diyagramını vermeyeceğim. Ancakfaktöriyel hesaplaması yapılırken, adımları görmenizi istiyorum. Adım olarak geçenher kutu, fonksiyonun bir kez çağrılmasını temsil ediyor. Başlangıç kısmını geçerseniz fonksiyon toplamda 5 kere çağrılıyor.

[Recursive Sample 2]

Rekürsif yapılar, oldukça karmaşık olabilir. Fakat kullanışlı oldukları kesin.Örneğin silme komutları rekürsif yapılardan yararlanır. Bir klasörüaltında bulunan her şeyle birlikte silmeniz gerekiyorsa, rekürsif fonksiyonkaçınılmazdır. Ya da bazı matematiksel işlemlerde veya arama ( search )yöntemlerinde yine rekürsif fonksiyonlara başvururuz. Bunların dışında rekürsiffonksiyonlar, normal fonksiyonlara göre daha az kod kullanılarak yazılır. Bunlarrekürsif fonksiyonların olumlu yönleri… Ancak hiçbir şey mükemmel değildir.

Rekürsif fonksiyon kullanmanın bilgisayarınıza bindereceği yük daha fazladır.Faktoriyel örneğine bakın; tam 5 kez aynı fonksiyonu çağırıyoruz ve bu sıradabütün değerler bellekte tutuluyor. Eğer çok sayıda iterasyondan söz ediyorsak,belleğiniz hızla tükenecektir. Rekürsif yapılar, bellekte ekstra yer kapladığı gibi,normal fonksiyonlara göre daha yavaştır. Üstelik kısa kod yazımına karşın,rekürsif fonksiyonların daha karmaşık olduklarını söyleyebiliriz. Anlamakzaman zaman sorun olabiliyor. Kısacası bir programda gerçekten rekürsif yapıyaihtiyacınız olmadığı sürece, ondan kaçınmanız daha iyi!

Örnek Sorular

Soru 1: Aşağıdaki programa göre, a, b ve c’nin değerleri nedir?
#include<stdio.h>
int main( void )
{
float a, b, c;
float *p;
a = 15.4;
b = 48.6;
p = &a;
c = b = *p = 151.52;
printf( “a: %f, b: %f, c: %f\n”, a, b, c );
return 0;
}

Cevap için tıklayınız…

Soru 2: Fibonnacci serisinde herhangi bir seri elemanın değerini bulmak içinf( n ) = f( n – 1 ) + f( n – 2 )fonksiyonu kullanılır. Başlangıç değeri olarak f( 0 ) = 0 vef( 1 ) = 1’dir. Bu bilgiler ışığında, verilen n sayısına göre, seridekarşılık düşen değeri bulan fonksiyonu rekürsif olarak yazınız.

Cevap için tıklayınız…