Kapat

10) C Programlama Dersi – X

Anasayfa C++ 10) C Programlama Dersi – X
10) C Programlama Dersi – X

Bazı Aritmetik Fonksiyonlar

Geçen dersimizde, fonksiyonları ve bunları nasıl kullanılacağını görmüştük. Ayrıca kütüphanelerin hazır fonksiyonlar içerdiğinden bahsetmiştik. Bazı matematiksel işlemlerin kullanımı sıkça gerekebileceği için bunları bir liste hâlinde vermenin uygun olduğuna inanıyorum. Böylece var olan aritmetik fonksiyonları tekrar tekrar tanımlayarak zaman kaybetmezsiniz.

  • double ceildouble 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 floordouble n ) : Virgüllü n sayısının, virgülden sonrasını atarak, bir tam sayıya çevirir. floor(51.4) işlemi, 51 sayısını döndürür.
  • double fabsdouble n ) : Verilen n sayısının mutlak değerini döndürür. fabs(-23.5), 23.5 değerini verir.
  • double fmoddouble a, double b ) : a sayı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 powdouble a, double b ) : Üstel değer hesaplamak için kullanılır; ab değerini verir.
  • double sqrtdouble 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, arka plâ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ğundan gerekli miktarda hücre, ilgili değişkene ayrılır. Gereken hücre adedi, değişken tipine 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çin uygulayalım. Değişken tiplerinden int’in 2 byte, float’un 4 byte ve char’ın 1 byte yer 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 4302 adresi 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ğunu unutmamak 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ğimizi söylemiştik. Bu doğru bir tanım ama eksik bir noktası var. Bellek hücrelerini değiştermemize rağmen, bunu direkt yapamaz; değişkenleri kullanırız. Bellek hü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 çevirirseniz mantıklı. Ancak terimlerin özünde olduğu gibi öğrenilmesinin daha yararlı olduğunu düşü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 ne olduğuna…

Değişkenler bildiğiniz gibi değer (sayı, karakter, vs…) tutar. Pointer’lar ise adres tutan değişkenlerdir. Bellekten bahsetmiştik; küçük hücrelerin oluşturduğu hafıza bloğunun adreslere ayrıldığını ve değişkenlerin bellek hücrelerine yerleş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şareti getiririz. Dikkat edilmesi gereken tek nokta; pointer’ı işaret edeceği değişken tipine uygun tanımlamaktır. Yani float bir değişkeni, int bir pointer ile iş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şkeninlerin saklayabildiği değerleri tutamazlar. Sadece değişkenleri işaret edebilirler. Herhangi bir değişkenin adresini pointer içersine atamak isterseniz, değişken adının önüne ‘&’ getirmeniz gerekir. Bundan sonra o pointer, ilgili değişkeni işaret eder. Eğer bahsettiğimiz değişkenin sahip olduğu değeri pointer ile göstermek veya değişken değerini değiştirmek isterseniz, pointer başına ‘*’ getirerek işlemlerinizi yapabilirsiniz. Pointer başına ‘*’ getirerek yapacağınız her 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 seferinde farklı sonuçlar doğurmuştur. Pointer kullanarak, değişkenlerin sakladığı değerleri de 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ında iş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 bir değişkendir ve onu da işaret edecek bir pointer yapısı kullanılabilir. Geçen sefer ki bildirimden farkı, pointer değişkenini işaret edecek bir değişken tanı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 pointer tanı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 iyi anlayabiliriz:

[Pointer to Pointer Structure]

Birbirini gösteren Pointer’ları ilerki derslerimizde, özellikle dinamik bellek tahsis 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 gibi parametrelere değer atıyorduk. Bu yöntemde, kullandığınız argümanların değeri değ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 isimleri bilmiyor 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ız değişikliklerin, argüman değişkene yansıması gereken durumlar olabilir. İşte bu gibi zamanlarda, “call by reference” veya “pass by reference” olarak isimlendirilen yöntem kullanılır. Argüman değer olarak aktarılmaz; argüman olan değişkenin adres bilgisi fonksiyona aktarılır. Bu sayede fonksiyon içersinde yapacağı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 yerlerini değiştiren bir fonksiyon yazalım. Yani kendisine a ve b adında iki değişken yollanı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 fonksiyon kullanarak değiştiremezdik. Eğer yazdığınız fonksiyon birden çok değer döndürmek zorundaysa, referans yoluyla aktarım zorunlu hâle geliyor. Çünkü daha önce işlediğimiz return ifadesiyle sadece tek bir değer döndürebiliriz. Örneğin bir bö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ısa bir fonksiyon için bu durum rahatsız etmez; ama uzun uzun 20 adet fonksiyon olduğunu düşünün. main(  ) fonksiyonu sayfalar dolusu kodun altında kalacak ve okunması güçleşecektir. Fonksiyon prototipleri burada devreye girer.

Bir üstte yazdığımız programı tekrar yazalım. Ama bu sefer, fonksiyon prototipi yapı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 önce yazmadık. Sadece böyle bir fonksiyon olduğunu ve alacağı parametre tiplerini bildirdik. ( İsteseydik parametre adlarını da yazabilirdik ama buna gerek yok. ) Daha sonra main(  ) fonksiyonu altına inip, fonksiyonu yazdık.

Öğrendiklerimizi pekiştirmek için yeni bir program yazalım. Fonksiyonumuz, kendisine argü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 çeviri olduğunu düşünmüyorum. Ama aklıma daha uygun bir şey gelmedi. Öneriniz varsa değ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 bir başka fonksiyon yerine kendisini çağırır ve şartlar uygun olduğu sürece bu tekrarlanır. Rekürsif, Recursive kelimesinden geliyor ve tekrarlamalı, yinelemeli anlamı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 serisini oluş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 fonksiyon yazalı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ına bakarak 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 hesaplayan fonksiyonu, 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. Ancak faktöriyel hesaplaması yapılırken, adımları görmenizi istiyorum. Adım olarak geçen her 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 fonksiyon kaçı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ürsif fonksiyonlar, normal fonksiyonlara göre daha az kod kullanılarak yazılır. Bunlar rekü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ırada bü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. Anlamak zaman zaman sorun olabiliyor. Kısacası bir programda gerçekten rekürsif yapıya ihtiyacı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;
}
0 0 0 0 0 0

Kimler Neler Demiş?

İlk Yorum Hakkı Senin!

Bildir
avatar