C

29 Ağustos 2006

C Programlama Dersi - VIII

Bu yazıda öğrenecekleriniz:


Kısa Devre Değerlendirme

Kısa devre değerlendirme, ne yazık ki pek iyi bir çeviri olmadı ve bu yüzden hiçbir anlam ifade etmeyebilir. İngilizce'de, Short Circuit Evaluation olarak geçen bu konu, mantıksal ifadelerle ilgilidir.

Hatırlarsanız, daha önce ki derslerimizde iki farklı AND ve OR operatörü görmüştük. Bu yapılardan biri AND için && işaretini kullanıyorken, diğeri sadece tek & simgesini kullanıyordu. Keza, OR ifadesi bir yerde, || şeklinde gösteriliyorken, diğer yerde, tek bir | simgesiyle ifade edilmekteydi. Bu işaretler, aynı sonucu üretiyor gibi görünseler de, farklı şekilde çalışırlar.

Çift şekilde yazılan operatörler, ( yani && ve || ) kısa devre operatörleridir. İngilizce, Short Circuit Operator olarak isimlendirilirler. Tek sembolle yazılan operatörlerden farkı, işlemleri kısaltmalarıdır.

Bir koşul içersinde AND ( && ) operatörü kullandığınızda, koşulun sol tarafı yanlışsa, sağ tarafı kontrol edilmez. Çünkü artık sağ tarafın doğru veya yanlış olmasının önemi yoktur; sonuç her şekilde yanlış olacaktır.

Benzer bir mantık OR ( || ) operatörü içinde geçerlidir. Eğer sol taraf doğruysa, sağ tarafın kontrol edilmesine gerek yoktur. Çünkü OR operatöründe taraflardan birinin doğru olması durumunda, diğerinin önemi kalmaz ve sonuç doğru döner.

Aşağıdaki örnekleri inceleyelim:

&& Operatörü & Operatörü
#include<stdio.h>
int main( void )
{
	int i, j;
	i = 0;
	j = 5;
	if( i == 1 && j++ ) {
		printf( "if içersine girdi\n" );
	}
	else {
		printf( "if içersine girmedi\n" );
		printf( "i: %d, j: %d\n", i, j );
	}
	return 0;
}
#include<stdio.h>
int main( void )
{
	int i, j;
	i = 0;
	j = 5;
	if( i == 1 & j++ ) {
		printf( "if içersine girdi\n" );
	}
	else {
		printf( "if içersine girmedi\n" );
		printf( "i: %d, j: %d\n", i, j );
	}
	return 0;
}
if içersine girmedi
i: 0, j: 5
if içersine girmedi
i: 0, j: 6

Gördüğünüz gibi, program çıktıları birbirinden farklıdır. Bunun sebebi, ilk örnekte, i == 1 koşulu yanlış olduğu için, && operatörünün ifadenin sağ tarafına bakmamasıdır. İkinci örnekteyse, & operatörü, koşulun her iki tarafına da bakar. Bu yüzden, j değişkenine ait değer değişir. Benzer bir uygulamayı, OR için || ve | kullanarak yapabilirsiniz.

ÖNEMLİ NOT: Özetle işlemlerinizi hızlandırmak istiyorsanız; AND kullanacağınız zaman, && operatörüyle çalışın ve yanlış olması muhtemel olan koşulu sol tarafa koyun. Eğer OR operatörü kullanacaksanız, doğru olma ihtimali fazla olan koşulu, ifadenin soluna koyun ve operatör olarak || ile çalışın. Bu şekillde yazılan bir program, kısa devre operatörleri sayesinde, gereksiz kontrolden kaçınarak işlemlerinizi hızlandıracaktır.

Elbette & veya | operatörlerinin kullanılması gereken durumlarda olabilir. Her n'olursa olsun, koşulun iki tarafınında çalışmasını istiyorsanız, o zaman & ve | operatörlerini kullanmanız gerekmektedir.

Önişlemci Komutları

Bir program yazdığınızı düşünün... Bu programda, PI değerini birçok yerde kullanmanız gerekiyor. Siz de PI değeri olması gereken her yere, 3.14 yazıyorsunuz. Oldukça sıkıcı bir iş. İleride PI'yi, 3.141592653 olarak değiştirmek isterseniz daha da sıkıcı hâle dönüşebilir. Veya canınız istedi, printf(  ) fonksiyonu yerine ekrana_yazdir(  ) kullanmaya niyetlendiniz... İşte bu gibi durumlarda, Önişlemci Komutlarını ( Preprocessor ) kullanırız. Önişlemci komutlarının amacı, bir şeyi başka bir şekilde ifade etmektir.

Konuya devam etmeden önce ufak bir uyarı da bulunmakta yarar var. Önişlemci komutlarını, değişkenler veya fonksiyonlarla karıştırmamak gerekiyor. Değişkenlerin ve fonksiyonların daha dinamik ve esnek bir yapıları varken, önişlemci komutları statiktir. Programınıza direkt bir kod yazdığınızı düşünün. Bu kod herhangi bir şey (sembol, program parçası, sayı, karakter vs...) olabilir. Örneğin, her yerde PI'nin karşılığı olarak 3.14 girmek yerine, PI diye bir sembol tanımlarız ve bunun görüldüğü her yere 3.14'ü koyulmasını isteriz. Önişlemci komutları bu gibi işlerde, biçilmiş kaftandır.

#define Önişlemci Komutu

#define komutu, adından da anlaşılabileceği gibi tanımlama işlemleri için kullanılır. Tanımlama komutunun kullanım mantığı çok basittir. Bütün yapmamız gereken, neyin yerine neyi yazacağımıza karar vermektir. Bunun için #define yazıp bir boşluk bıraktıkan sonra, önce kullanacağımız bir isim verilir, ardından da yerine geçeceği değer.

Altta ki program, PI sembolü olan her yere 3.14 koyacak ve işlemleri buna göre yapacaktır:

/* Çember alanını hesaplar */

#include<stdio.h>
#define PI 3.14
int main( void )
{
	int yaricap;
	float alan;
	printf( "Çemberin yarı çapını giriniz> " );
	scanf( "%d", &yaricap );
	alan = PI * yaricap * yaricap;
	printf( "Çember alanı: %.2f\n", alan );
	return 0;
}

Gördüğünüz gibi, PI bir değişken olarak tanımlanmamıştır. Ancak #define komutu sayesinde, PI'nin aslında 3.14 olduğu derleyici (compiler) tarafından kabul edilmiştir. Sadece #define komutunu kullanarak başka şeylerde yapmak mümkündür. Örneğin, daha önce dediğimizi yapalım ve printf yerine, ekrana_yazdir; scanf yerine de, deger_al isimlerini kullanalım:

/* Yarıçapa göre daire alanı hesaplar */

#include<stdio.h>
#define PI 		3.14
#define ekrana_yazdir 	printf
#define deger_al 	scanf
int main( void )
{
	int yaricap;
	float alan;
	ekrana_yazdir( "Çemberin yarı çapını giriniz> " );
	deger_al( "%d", &yaricap );
	alan = PI * yaricap * yaricap;
	ekrana_yazdir( "Çember alanı: %.2f\n", alan );
	return 0;
}

#define komutunun başka marifetleri de vardır. İlerki konularımızda göreceğimiz fonksiyon yapısına benzer şekilde kullanımı mümkündür. Elbette ki, fonksiyonlar çok daha gelişmiştir ve sağladıkları esnekliği, #define tutamaz. Bu yüzden #define ile yapılacakların sınırını çok zorlamamak en iyisi. Ancak yine de bilginiz olması açısından aşağıda ki örneğe göz atabilirsiniz:

/* Istenen sayida, "Merhaba" yazar */

#include<stdio.h>
#define merhaba_yazdir( x ) int i; for ( i = 0; i < (x); i++ ) printf( "Merhaba\n" );
int main( void )
{	
	int yazdirma_adedi;
	printf( "Kaç defa yazdırılsın> " );
	scanf( "%d", &yazdirma_adedi );
	merhaba_yazdir( yazdirma_adedi );
	return 0;
}	

#undef Önişlemci Komutu

Bazı durumlarda, #define komutuyla tanımladığımız şeyleri, iptal etmek isteriz. Tanımlamayı iptal etmek için, #undef komutu kullanılır. Örneğin #undef PI yazdığınız da, o noktadan itibaren PI tanımsız olacaktır. #define ile oluşturduğunuz sembolleri belirli noktalardan sonra geçerliliğini iptal etmek veya yeniden tanımlamak için #undef komutunu kullanabilirsiniz.

#ifdef ve #ifndef Önişlemci Komutları

Önişlemci komutlarında bir sembol veya simgenin daha önce tanıtılıp tanıtılmadığını kontrol etmek isteyebiliriz. Tanıtılmışsa, şöyle yapılsın; yok tanıtılmadıysa, böyle olsun gibi farklı durumlarda ne olacağını belirten yapılar gerekebilir. Bu açığı kapatmak için #ifdef (if defined - şayet tanımlandıysa) veya #ifndef (if not defined - şayet tanımlanmadıysa) operatörleri kullanılır.

#include<stdio.h>
#define PI 3.14
int main( void )
{
	// Tanımlı PI değeri, tanımsız hâle getiriliyor.
	#undef PI

	int yaricap;
	float alan;
	printf( "Çemberin yarı çapını giriniz> " );
	scanf( "%d", &yaricap );

	// PI değerinin tanımlı olup olmadığı kontrol ediliyor.
	#ifdef PI
		//PI tanımlıysa, daire alanı hesaplanıyor.
		alan = PI * yaricap * yaricap;
		printf( "Çember alanı: %.2f\n", alan );
	#else
		//PI değeri tanımsızsa, HATA mesajı veriliyor.
		printf("HATA: Alan değeri tanımlı değildir.\n");
	#endif
	
	return 0;
}

Yukardaki örneğe bakacak olursak, önce PI değeri tanımlanıyor ve sonrasında tanım kaldırılıyor. Biz de sürprizlerle karşılaşmak istemediğimizden, PI değerinin tanım durumunu kontrol ediyoruz. Tek başına çalışan biri için gereksiz bir ayrıntı gibi gözükse de, ekip çalışmalarında, bazı şeylerin kontrol edilmesi ve istenmeyen durumlarda, ne yapılacağı belirlenmelidir. Yukarda ki programı şöyle de yazabilirdik:

#include<stdio.h>
int main( void )
{
	int yaricap;
	float alan;
	printf( "Çemberin yarı çapını giriniz> " );
	scanf( "%d", &yaricap );

	// Şu noktaya kadar tanımlı bir PI değeri bulunmuyor.
	// #ifndef opertörü bu durumu kontrol ediyor.
	// Eğer tanımsızsa, PI'nin tanımlanması sağlanıyor.
	#ifndef PI		
		#define PI 3.14
	#endif

	alan = PI * yaricap * yaricap;
	printf( "Çember alanı: %.2f\n", alan );

	return 0;
}

#if, #else, #endif, #elif Önişlemci Komutları

Bazen bir değerin tanımlanıp, tanımlanmadığını bilmek yetmez. Bazı değerler, bayrak (flag) olarak kullanılır. Yani eğer doğruysa, böyle yapılması lâzım, aksi hâlde böyle olacak gibi... Bazı programlar, önişlemci komutlarından yararlanır. Değişken yerine, önişlemcileri kullanarak tanımlanan simgeler, bu programlarda flag görevi görür.

Konumuza dönersek, #if, #else, #endif yapısı daha önce işlemiş olduğumuz if-else yapısıyla hemen hemen aynıdır. if-elif yapısı da if-else if yapısına benzer. Her ikisinin de genel yazım kuralı aşağıda verilmiştir:

#if - #else Yapısı: #if - #elif Yapısı:
#if koşul
	komut(lar)
#else 
	komut(lar)
#endif
#if koşul 1
	komut(lar) 1
#elif koşul 2
	komut(lar) 2
.
.
.
#elif koşul n-1
	komut(lar) n-1
#else 
	komut(lar) n
#endif

Bir program tasarlayalım. Bu programda, pi sayısınının virgülden sonra kaç basamağının hesaba katılacağına karar veren bir mekanizma olsun. Soruyu, şu ana kadar gördüğümüz, if - else gibi yapılarla rahatça yapabiliriz. Önişlemci komutuyla ise, aşağıdakine benzer bir sistem oluşturulabilir:

/* Daire alanını hesaplar */

#include<stdio.h>
#define HASSASLIK_DERECESI 2
int main( void )
{
	int yaricap;
	float alan;
	printf( "Çemberin yarı çapını giriniz> " );
	scanf( "%d", &yaricap );

	// Hassaslık derecesi, pi sayısının virgülden kaç 
	// basamak sonrasının hesaba katılacağını belirtir.
	// Eğer hassaslık derecesi bunlara uymuyorsa, alan 
	// değeri -1 yapılır.
	
	#if ( HASSASLIK_DERECESI == 0 )
		alan = 3 * yaricap * yaricap;
	#elif ( HASSASLIK_DERECESI == 1 )
		alan = 3.1 * yaricap * yaricap;
	#elif ( HASSASLIK_DERECESI == 2 )
		alan = 3.14 * yaricap * yaricap;
	#else
		alan = -1;
	#endif

	printf( "Çember alanı: %.2f\n", alan );
	return 0;
}

#include Önişlemci Komutu

#include oldukça tanıdık bir operatördür. Her programımızda, #include önişlemci komutunu kullanırız. Şayet kullanmasak, printf(  ) veya scanf(  ) gibi fonksiyonları tekrar tekrar yazmamız gerekirdi. #include komutu, programımıza bir başlık dosyasının (header file) dâhil edileceğini belirtir. Bu başlık dosyası, standart giriş çıkış işlemlerini içeren bir kütüphane olabileceği gibi, kendimize ait fonksiyonların bulunduğu bir dosya da olabilir.

Eğer sistem kütüphanelerine ait bir başlık dosyasını programınıza dâhil edeceksek, küçüktür ( < ) ve büyüktür ( > ) işaretlerini kullanırız. Örneğin stdio.h sisteme ait bir kütüphane dosyasıdır ve Linux'ta /usr/include/stdio.h adresinde bulunur. Dolayısıyla stdio.h kütüphanesini programımıza eklerken, #include<stdio.h> şeklinde yazarız. Kendi oluşturduğumuz başlık dosyaları içinse, durum biraz daha farklıdır.

Çalışma ortamımızla aynı klasörde olan bir başlık dosyasını, programımıza eklemek için #include "benim.h" şeklinde yazarız. İlerki derslerimizde, kendi başlık dosyalarımızı oluşturmayı göreceğiz. Ancak şimdilik burada keselim...

Önemli Noktalar...

Konuyu noktalarken, söylemek istediğim bazı şeyler bulunuyor. Olabildiğince, önişlemci komutlarından - #include komutu hariç - uzak durun. Çünkü bu komutlar, esnek bir yapıya sahip değiller ve bu durum, bir noktadan sonra başınızı ağrıtacaktır. Önişlemci komutlarıyla yazılmış kodları takip etmek oldukça zordur ve debug edilemezler. Java gibi gelişmiş dillerde, #define komutu bulunmaz. Modern dillerde, bu yapıdan uzaklaşılmaya başlanmıştır.

Yukarda saydıklarıma rağmen, bazı durumlarda, önişlemci komutlarını kullanmak uygun olabilir. Kaldı ki bu komutların kullanıldığı birçok yer bulunuyor ve biz kullanmasak bile, bilmeye mecbur durumdayız. Sözün özü; bu konuyu es geçmek uygun değil. Ancak üzerine düşmek oldukça gereksiz.

Çağatay ÇEBİ


<< Geri İleri >>