|
Hata Tespiti ve Çözümlenmesi |
|
Gönderiliyor lütfen bekleyin... |
|
|
"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.
Gösterici 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 göstericilerin 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 göstericisine atamak yerine ptr göstericisinin gösterdiği alana atama
işlemi yapılmasıdır. Ancak sorun, ptr göstericisinin 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 göstericisi 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 göstericiyi kullanmadan önce göstericiye 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 gösterici 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
gösterici 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
göstericilerimizi tekrar gözden geçirmemiz gerekir. Şimdi bunlardan sonra "Ne
yani gösterici kullanmayalım mı diyosun sen?" gibi sesler duyabilirim diye şunu
belirtmeliyim ki göstericiler C’nin en güçlü yönlerinden biridir ve ne kadar
hataya yol açarlarsa açsınlar göstericileri kullanabilme yeteneğimiz herşeye
değecektir. Göstericileri 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 gösterici 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 salak 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...
Makale:
Hata Tespiti ve Çözümlenmesi C ve Sistem Programlama Oğuz Yağmur
|
|
|
-
-
Eklenen Son 10
-
Bu Konuda Geçmiş 10
Bu Konuda Yazılmış Yazılmış 10 Makale Yükleniyor
Son Eklenen 10 Makale Yükleniyor
Bu Konuda Yazılmış Geçmiş Makaleler Yükleniyor
|
|