"Oof offf offffff!!!! yine ne oldu! nerde yanlış lık yaptım ben şimdi? yok yok bu derleyicide bi yanlışlık var sapıtıyor bazen. Şeytan diyor sil baştan yaz şunları şimdi". Birçok defa buna benzer serzenişlere figüranlık etmişiz ya da başrolde biz oynamışızdır. Malum hatasız kod pardon programcı olmaz. Aslında programlamanın %10’u esin %90’nı ise hata ayıklamaktan ibaret olduğunu kabullenmek gerekiyor. Tanıdığım çok iyi programcıların en güçlü özelliklerinin başında çok iyi hata ayıklama becerilerinin gelmesidir. Bu yazımızda en sık karşılaşılan hatarın nedenlerini inceleyerek hatalarımızı en aza indirmeye çalışacak metodları gözden geçireceğiz.


Artırma ve Eksiltme Operatörleriden Kaynaklanan Hatalar


En sık kullandığımız operatörlerin başında ++ ve -- operatörleri gelir. Ama çoğu zaman operatör öncelik sırasına, değişkenin önünde (prefix) veya arkasında (postfix) kullanılmasına dikkat etmeyiz. Tabiki bunlarda hatalara yol açacaktır. Örneğin;


1.Örnek 2.Örnek
a = 10; a = 10;
b = a++; b = ++a;
Dikkat ettiyseniz yukardaki iki örnek aynı sonucu vermeyecektir. Birinci örnekte 10 değerini b’ye atar ve sonrasında a’yı artırır. İkinci örnekte ise a’ nın değerini artırır ve 11 değerini b’ye atar. Eğer ++ veya -- operatörlerinin önek ya da sonek olmasından kaynaklanan değişiklikleri gözardı edilmesi durumdan problemler ortaya çıkacaktır.


Opertör önceliklerinin dikkate alınmaması yine benzer problemlere yol açacaktır. Örneğin; 1.Örnek 2.Örnek
c = a + b; c = ++a + b;
a = a + 1;
Yukardaki iki örnek farklı sonuçlar üretecektir. Nedeni ise ikinci örnekte a ile b toplanmadan önce a’nın değerinin artırılmasıdır (operatör önceliğinden dolayı).


Bazen bu tür hataların bulunması nerdeyse işkence haline gelebilir. Bu tür hataların yakalanmasında düzgün çalışmayan döngüler ya da normal değerinden 1 fazla olan rutinler bize ipucu olabilir. Eğer bu tür ifadelerde bir şüphe duyuyorsanız bunları emin olacağınız şekilde yeniden kodlamak hataları çözmeye katkıda bulunacaktır.


+++++++++ Hataları


"xxx.exe bir sorunla karşılaştı ve kapatılması gerekiyor..." Ne çok görmüşüzdür bu programımızın çöktüğünü belgeleyen sevimsiz hata mesajını. C programcılarının en çok karşılaştığı hatalardan biri de +++++++++lerin yanlış kullanılmasıdır. Bir örnek ile incelemeye başlayalım: #include <stdio.h>
#include <stdlib.h>


int main(void)
{
char *ptr;


*ptr = (char *) malloc(1000); /*hatalı kod * /
gets(ptr);
printf(ptr);


return 0;
}
Yukardaki program çok büyük ihtimalla çökecektir. (yine o sevimsiz hata mesajını göreceğiz!) Nedeni ise malloc tarafında tahsis edilen alanın adresini ptr +++++++++sine atamak yerine ptr +++++++++sinin gösterdiği alana atama işlemi yapılmasıdır. Ancak sorun, ptr +++++++++sinin gösterdiği alan kesinlikle bilinmemekte ve bizim kontrolümüz dışındadır. Bu hata genelde C ile programlamaya yeni başlayanlarda (ki büyük olasılıkla * operatörü yanlış anlaşılmıştır) ve büyük olasılıkla o an ya TV seyrederken kod yazan ya da uykusuzluktan gözünü açamayacak halde iken kod yazan profesyonel programcılar tarafından yapılır. Programızı düzeltmek için aşağıdaki değişikliği yapalım.


ptr = (char *) malloc(1000); /* Geçerli kod */


Peki bu değişikliği yapmakla acaba kodumuz bütün hatalardan arındı mı? Hayır diyenler büyük çoğunlukta olsa da "hmm daha ne var ki düzeltilecek" diyenler de olacaktır. O zaman bende "Evet!" diyenlere şunu sormak istiyorum. malloc ile tahsis etmek istediğiniz alanı gerçekten tahsis edebildiğinizden emin misiniz? malloc ile tahsisat yapmak istediğimizde eğer yeterli alan yoksa malloc NULL değerini döndürecektir. ptr +++++++++si NULL değere sahip olacak ve ptr yi kullanmak istediğimizde programımız hemen çökecektir. Bu hatanın önlenmesi için malloc fonksiyonunun işlemi başarı ile tahsisat yapıp yapmadığını kontrol etmemiz gerekir. Kodumuzun düzeltilmiş hali aşağıdaki gibi olmalıdır. #include <stdio.h>
#include <stdlib.h>


int main(void)
{
char *ptr;


ptr = (char *) malloc(1000);
if(!ptr) {
printf("Yetersiz bellek alanı!!!\n");
exit(1);
}
gets(ptr);
printf(ptr);


return 0;
}
Bir başka yaygın hata ise, bir +++++++++yi kullanmadan önce +++++++++ye ilk değer verme işleminin yapılmamamış olmasıdır. Örneğin;


int *pX;
*pX = 100; /* Hatalı kod*/


Yukardaki kod muhtemelen 5 - 10 satır aşağıda sorunlara neden olacaktır. Çünkü pX in nereyi gösterdiğini bilmiyoruz ve bilmediğimiz bir alana atama işlemi yapıyoruz(Ben ambulans sesleri duymaya başladım ya siz?). Bilmediğimiz alan belki başka bir kod bölgesi ya da veri ise o zaman neler olabileceğini siz düşünün! Kontrolümüzde olmayan işaretçilerin farkına varılması ve takip edilmesi çok zor ve zahmetli işlerdir. Bazen +++++++++ hatası yapsak bile tamamen raslantısal olarak programımız düzgün çalışabilir ve hatanın farkında olmayabilirsiniz(aslında işletim sistemi 112 yi çoktan aramış, ambulanslar yoldadır bile). Ama programımız büyümeye başlayınca hele de programınıza yeni fonksiyonlar,elemanlar eklemişseniz iş daha da karmaşıklaşacak, hataları bunlarda aramaya başlayacaksınız ki işler iyice arap saçına dönecektir. Bir +++++++++ hatası olduğunu nasıl tespit edeceğiz o zaman? Genellikle bu gibi durumlarda programımız tutarsız davranacak, bazen doğru bazen de yanlış çalışacaktır. Bazen de sonuçları hiç alakasız değerlerle görebiliriz(Mesela biz ekranda "abc" yazısını görmeyi beklerken "xnj8399-9ş*7ş87*..." gibi bir değer görebiliriz). Bu gibi durumlarla karşılaştığımızda öncelikli olarak +++++++++lerimizi tekrar gözden geçirmemiz gerekir. Şimdi bunlardan sonra "Ne yani +++++++++ kullanmayalım mı diyosun sen?" gibi sesler duyabilirim diye şunu belirtmeliyim ki +++++++++ler C’nin en güçlü yönlerinden biridir ve ne kadar hataya yol açarlarsa açsınlar +++++++++leri kullanabilme yeteneğimiz herşeye değecektir. +++++++++leri tam anlamı ile kavramak birçok hatının önlenmesinde katkı sağlayacaktır.


Söz Dizimi Hataları


Çoğu zaman öyle hatalarla karşılaşırız ki "ya bu derleyici hangi dilden konuşuyo acaba" dedirtecek türden hatalardır. Derleyici tarafından size gösterilen hata sizin yazdığınız kod ile alakası yoktur. Ama şunu da unutmamak gerekir ki derleyici her zaman haklıdır. Tamam hata mesajları çok da mükemmel değildir ama tespitte haklıdır. Mesela aşağıdakli örnekte can sıkıcı bir hata alacağız; #include <stdio.h>


char *func(void);


int main(void)
{
/**********/
return 0;
}
int func(void)
{
printf("func\n");
return 1;
}
VC7 ile alınan mesaj :
error C2040: ’func’ : ’int (void)’ differs in levels of indirection from ’char *(void)’
GCC ile alınan mesaj :
errmsg.c:12: error: new declaration `int func()’
errmsg.c: 3: error: ambiguates old declaration ’char* func()’
Bazı derleyicilerde de : Type mismatch in redeclaration of func(void)


şeklinde hata mesajları alabilirsiniz. Peki nasıl oluyorda bu mesajları alıyoruz? Bizim iki tane func fonksiyonuzmuz yok ki!. O zaman nerden çıktı bu mesajlar! Kodumuzu yakından incelersek, kodumuzun başında func fonksiyonun geri dönüş değerinin char türünden bir +++++++++ olduğunu görürüz. Bu durumda derleyici prototip bildirimini gördüğünde bu bilgileri sembol tablosuna yazacaktır. Daha sonra programımızın içinde func ile karşılaştığında geri dönüş değerinin int olduğunu görecek ve bize "yeniden bildirimini yapıyorsun bu fonksiyonun" ya da "yeniden tanımlıyorsun bu fonksiyonu" diyecektir. Buna benzer bir söz dizimi hatası da şöyledir. #include <stdio.h>


void func(void);


int main(void)
{
/**********/
func();
return 0;
}


void func(void); /* Hatalı kod */
{
printf("func\n");
}
Buradaki hata ise func fonksiyonunun tanımlanmasından sonra gelen ; kullanılmasından dolayı oluşan hatadır. Yine derleyiciden derleyiciye farklı hata mesajları alabilirsiniz. Ama çoğu derleyici ; kullanılmasından dolayı bunu bir bildirim sanacak ve ; den sonra gelen açılış küme parantezini işaret ederek "bad decleration syntax" gibi bir hata mesajı ile sizi uyaracaktır. Genelde ifadelerden sonra noktalı virgül görmeye alışık olduğumuz için bu gibi hataları saptamak çok zor olacaktır. (Birdefasında if(a == 0) yerine if(a = 0) yazılan bir kodda, 2 saat boyunca hata aradığım olmuştur!!!)


Dizi Uzunluğunda Yapılan Hatalar


Bildiğimiz gibi C’de dizilerin indeksleri 0 dan başlar. Ama çoğu zaman deneyimli programcıların bile bu özelliği unuttuğuna şahit olmuşuzdur. Örneğin uzunluğu 100 olan int türünden bir diziye değer atayan aşağıdaki örneğe bakalım. #include <stdio.h>


int main(void)
{
int ind;
int dizi[100];


for (ind = 1; ind <= 100; ++ind)
dizi[ind] = ind;< BR >
return 0;
}
Tabiki bu örneğin çalışmayacağını farketmişsinizdir. Programımızda iki tane yanlışımız var. İlk yanlışımız dizi[0]’a ilk değerin verilmemiş olması, ikincisi ise dizinin sonundan bir adım ileriye değer ataması yapılmıştır çünkü dizi[99] dizimizin son öğesidir. n elemanlı bir dizinin 0 dan n-1 e kadar elamanı olduğunu aklımızdan çıkarmazsak hataları önlemiş oluruz.


Sınır Hataları


C’nin standart kütüphane fonksiyonları ve çalışma ortamı çok az sınır kontrolü gerçekleştirir ya da hiç gerçekleştirmez diyebiliriz. Mesela bir önceki konudaki gibi dizi sınırlarını çok rahat bir şekilde aşabiliriz. Mesela, bir programımız olsun, programımız klavyeden bir karekter katarı alsın ve onu ekranda görüntülesin. Programımız şu şekilde olacaktır:
#include <stdio.h>


int main(void)
{
int x;
char dizi[10];
int y;


x = 10;
y = 10;
gets(dizi);
printf("%s %d %d", dizi, x, y);


return 0;
}
Yukardaki örnekte ilk bakışta bir kodlama hatası yok gibi görünse de gets()’i dizi ile kullanarak çağırmak ilerde hatalara neden olabilir. Programımızda dizi 10 karakter alacak şekilde bildirimi yapılmıştır. Ama kullanıcı 10 dan fazla karakter girince ne olur? Tabiki bu dizi’nin taşmasına neden olacak ve x yada y’yi veya herikisini birden ezecek sonuçta x ve y doğru değerleri içermeyecektir. Bunun nedeni ise bütün C derleyicileri yerel değikenleri depolamak için yığını (stack) kullanıyor olmalarıdır. Muhtemelen x,y,dizi bellekte sırası ile x, dizi, y şeklinde sıralanacaktır. Bu sıralamayı gözönüne alırsak, dizi taştığında fazladan girilen bilgiler y’ye ait olan alana yerleştirilecek böylece eski bilgilerin bozulmasına neden olacaktır. Tabi bunları hesap etmediğimiz takdirde, ekrana her iki değer için 10 yazmasını beklerken alaksız değerler yazılacak ve hataları başka yerde arayacağız(muhtemelen +++++ derleyicide yine bir sapıtma belirtileri olduğunu düşüneceğiz!!!). Bu sorunu ortadan kaldırmak için gets() yerine fgets() kullanmamız bir çözüm yolu olabilir(fgets() ile okunacak maksimum karekter sayısını belirleyebiliyoruz).


Fonksiyon Prototiplerinin Yapılmaması


Aslında programcılar biraz tembel insanlardır bunu kabullenmek gerekir heralde. Çoğu zaman yazdığı fonksiyonların prototip bildirimlerini yapmaktan üşenirler. Bu çok büyük hatalara neden olan bir karardır.C eğitimi aldığım (www.csystem.org) süreçte hocalarım sık sık bu konu üzerine eğilir öneminden bahsederler bu konuyu dikkate almamızı şiddetle tavsiye ederlerdi. Tabi ilk başlarda pek anlamasak da hatalı kodlarla boğuşurken bazı şeyleri kulağımıza küpe yaptık. Peki nedir bu kadar önemli kılan bu konuyu. Hemen bir örnek üzerinde incelemeye başlayalım. #include <stdio.h>


int main(void)
{
float a, b;


scanf("%f%f", &a, &b);
printf("%f", carp(a, b));


return 0;
}


double carp(float a, float b)
{
return a * b;
}
Şimdi biz 2.2, 3.3 değerlerini girdiğimizde sonuç olarak 7.26 dödürmesini mi bekliyoruz? Malesef hayattan çok şey bekliyoruz. Biz carp() için prototip kullanmadığımızdan dolayı main(), carp()’tan bir tamsayı değeri döndürmesini bekler. Ama carp() double değeri döndürecek şekilde yazılmıştır. Peki biraz daha ayrıntıya girecek olursak, tamsayıların 4 byte, double’ların ise 8 byte olduğunu düşünürsek, printf() bu durumda 8 byte lık double için yalnızca 4 byte ını kullanacak böylece de biz ekranda yanlış değerlerle karşılaşmış olacağız. O zaman carp fonksiyonumuzun prototipi kullanarak main() e carp fonksiyonun double türünden değer döndüreceğini bildirerek problemi ortadan kaldırabiliriz. ( double carp(float a, float b); )


Argüman Hataları


Davul bile dengi dengine boşuna dememiş atalarımız. Bir fonksiyonun beklediği parametre tipi ile, fonksiyone verdiğiniz parameterelerin eşlendiğinden emin olmamız gerekir. Fonksiyon prototipleri kullanarak bu hataları öneleyebiliriz ama tüm hataları yakalamamız biraz zor. Nedeni ise, değişken sayıda parametre alan fonksiyonlarda tip uyumsuzlukların yakalanması mümkün değildir. Örneğin scanf() fonksiyonu. Bildiğimiz gibi scanf(), paramatrelerinin değerlerini değil adreslerini almayı bekler ancak bizi bunun için zorlamaz.


{
int x;
scanf("%d", x);
}


Yukardaki örnek kod bir güzel derlenip program çalışabilir hale gelecektir. Ama program çalıştığında hata üretmesine neden olacaktır. Çünkü bu kod ile x’in adresi değil x’in değeri aktarılıyor.
Yığın (Stack) Taşması


Bildiğimiz üzere derleyiciler, yerel değişkenleri, fonksiyonların dönüş adresleri ve fonskiyonlara aktarılan parametreleri depolamak için yığını kullanırlar. Hayatta hiçbir şey sonsuz olmadığı gibi yığında sonsuz büyüklükte değildir. Yığın taşmasının olduğu bir durumda bazen program tuhaf bir şekilde çalışıyor olsa da bazen de program çökecektir. Yığın taşmasındaki en büyük problem ansızın ve anlaşılmaz bir şekilde ortaya çıkmasıdır. Haliylende böyle hataları saptamak çin işkencesine dönebilir. Bu durumda şüpheleneceğimiz konularında başında kendi kendini çağıran (recursive) fonksiyonlarımızın yanlış kodlanmış olabileceğidir. Programımızda kendi kendini çağıran fonksiyonlar kullanıyorsak ve anlamsız hatalar ile karşılaşıyorsak, bu tür fonksiyonların çıkış noktalarını birkez daha gözden geçirme vakti geldiğine karar verebiliriz. Eğer bunda da hata yok ise programımız hatasız bir şekilde kodlandığında eminsek bu sefer de derleyicimiz destekliyor ise yığın için ayrılan bellek miktarını artırarak bir çözüm üretebiliriz.


Son Söz


Birçok derleyici beraberinde hata ayıklayıcı (debugger) bulundurur. Bu sayede kodumuzu adım adım çalıştırarak, durak noktalarını (break point) ayarlamamıza ve değişkenlerimizin içeriklerini görebilmemize olanak sağlar. Gerçi hata ayıklayıcısını kullanmak o kadar da kolay bir iş olmamasına karşın bize sağladığı faydaları göz önüne aldığımızda, hata ayıklayıcısını etkin bir biçimde kullanmak için harcayacağımız emek ve zamana değecektir. Ama bizler iyi bir programcı isek hata ayıklayıcısına güvenmeyip, sağlam tasarım ve ustalığı herzaman tercih etmeliyiz.


Bunlardan ziyade, sizlerinde hata tespiti ve ayıklama konusunda teknikleriniz vardır muhakkak. Program geliştirme sürecinde her zaman hata kontrolü yaparak ilerlemek başta programın gelişim sürecini olumsuz yönde etkiliyor gibi görünse de zaman ve maliyeti göz önüne aldığımızda aslında en iyi yöntem olduğu ortaya çıkacaktır. Eğer çalışan bir kodumuz varsa, daha sonra bu koda her eklentide, yeni oluşan kodu test edip hatalardan ayıklamak iyi bir yöntem olacaktır. Bu sayede programcının (yani bizler) hataları yakalaması kolaylaşacaktır. Çünkü muhtemelen hata yeni eklenen koda ait olacaktır.Bu konu ile ilgili Kaan Aslan’ın Sistem Yayıncılık’tan çıkmış C Yanlışları adlı kitabı okumanızı tavsiye ederim.


Hatasız kodlar yazmamız dileğiyle...