Ders 13 / 15
Hata Yönetimi Kalıpları
Bir işlev başarısız olduğunda, o ana kadar ayırdığı bloğu kimin bırakacağı derleyicinin denetlediği bir kural değil, işlevin kendi sözleşmesidir; ders bunu dönüş kodu, hata numarası ve tek çıkış noktalı temizlik kalıbıyla koşturarak ölçer.
Yayın Güncelleme
Bu derste
- Kısmi başarısızlıkta ayrılmış bir bloğu işlevin kendisinin bırakmasını dönüş koduyla ölçer
- errno tabanlı hata bildirimini yalnız sıfır olup olmadığını basarak kullanır
- Birden çok kaynağı tek çıkış noktalı bir goto temizle kalıbıyla temizler
İçindekiler
Önceki ders sözleşmenin kırıldığı dört anı ele aldı ve hiçbirini koşturmadı. Bu ders sözleşmenin kırılmadığı, ama işlevin başarısız olduğu bir anı ölçüyor: bir işlev bir iş için bellek ayırmaya başlar, ortasında bir koşul tutmaz ve geri dönmesi gerekir. O ana kadar ayrılmış blok artık kimsenin elinde değildir — kimin bırakacağı, kaynaktaki bir kuraldan değil, o işlevin kendi sözleşmesinden çıkar.
Bu iki ders arasındaki fark ince ama bağlayıcıdır: tanımsız davranış sözleşmenin kırıldığı andır, kısmi başarısızlık ise sözleşmenin hâlâ ayakta olduğu bir andır — işlev başarısız oluyor, ama bu başarısızlık standartta tanımsız bırakılmış bir şey değil, işlevin kendisinin öngördüğü bir yoldur. Aradaki fark ölçüme de yansıyor: önceki derste ölçülen şey “sözleşme kırılınca hiçbir tarafın koruyamayacağı” bir bloktu; bu derste ölçülen şey, sözleşmeyi yazan tarafın, kendi başarısızlığını nasıl temizlediğidir. Kalıp üç düzeyde ilerliyor: tek kaynaklı hata yolu, çok kaynaklı hata yolu ve son olarak standart kütüphanenin kendi hata bildirim yolu.
Hata Yolunda Temizlik
Ölçüm çekirdeğinde bu örneği zaten gördük: kayit_olustur, verilen ad sinir parametresine
sığmazsa, o ana kadar ayırdığı kaydı kendisi bırakır ve çağırana NULL döndürür.
Çağırana temizlenecek hiçbir şey kalmaz.
Bu kural yalnız tek bir bloğa uygulanıyor gibi görünse de, aslında bir işlevin dışarıya
verdiği sözdür: NULL dönen bir işlev, hiçbir şey ayırmamış gibi davranmayı taahhüt
ediyor demektir — çağıran kayit_yok_et çağırmak zorunda değildir, çünkü yok edecek bir şey
yoktur. Bu söz kaynak kodunda hiçbir anahtar sözcükle işaretlenmez; yalnız sahip.h’deki
yorumda yazılıdır ve kayit_olustur’un gövdesi o sözü tutar. Sözü tutmayan bir işlev —
örneğin ara bloğu ayırıp NULL döndükten sonra o bloğu bırakmayı unutan bir işlev — dilde
hiçbir uyarı üretmez; sonuç sessiz bir bellek sızıntısıdır ve yalnız sayaç farkıyla ortaya
çıkar.
Bu kesitin ölçümü, dersin tek main’ini içeren son kod bloğunda, öbür iki kalıpla birlikte
gösteriliyor. Ölçüm çekirdeğinin dört dosyası bu derste de aynen kullanılıyor:
// sayac.h — her ayirmayi ve her birakmayi sayan sarmalayici #ifndef SAYAC_H #define SAYAC_H #include <stddef.h> void *say_ayir(size_t boyut); void *say_buyut(void *eski, size_t boyut); void say_birak(void *blok); int ayrilan_sayisi(void); int birakilan_sayisi(void); #endif
// sayac.c — sayaclar yalniz bu birimde tutulur
#include <stdlib.h>
#include "sayac.h"
static int ayrilan = 0;
static int birakilan = 0;
void *say_ayir(size_t boyut) {
void *blok = malloc(boyut);
if (blok != NULL) { ayrilan++; }
return blok;
}
void *say_buyut(void *eski, size_t boyut) {
int yeni_blok = (eski == NULL);
void *yeni = realloc(eski, boyut);
if (yeni != NULL && yeni_blok) { ayrilan++; }
return yeni;
}
void say_birak(void *blok) {
if (blok != NULL) { birakilan++; }
free(blok);
}
int ayrilan_sayisi(void) { return ayrilan; }
int birakilan_sayisi(void) { return birakilan; }
// sahip.h — kaydin ici gizli, sahiplik sozlesmesi yorumlarda yazili #ifndef SAHIP_H #define SAHIP_H #include <stddef.h> typedef struct Kayit Kayit; /* Cagiran sahiplenir ve kayit_yok_et ile birakir. Ad sinira sigmazsa NULL doner; o ana kadar ayrilan bellegi islev kendisi birakir. */ Kayit *kayit_olustur(const char *ad, size_t sinir); /* Kaydi ve kaydin icindeki ad tamponunu birlikte birakir. */ void kayit_yok_et(Kayit *k); /* Kayit sahiplenir: donen isaretci kayit yok edilene dek gecerlidir, cagiran onu birakmaz. */ const char *kayit_ad(const Kayit *k); /* Cagiran sahiplenir: donen kopya say_birak ile birakilir. */ char *kayit_ad_kopyasi(const Kayit *k); #endif
// sahip.c — sozlesmenin uygulandigi birim
#include <string.h>
#include "sahip.h"
#include "sayac.h"
struct Kayit {
char *ad;
size_t uzunluk;
};
Kayit *kayit_olustur(const char *ad, size_t sinir) {
Kayit *k = say_ayir(sizeof *k);
if (k == NULL) { return NULL; }
k->uzunluk = strlen(ad);
if (k->uzunluk >= sinir) {
say_birak(k);
return NULL;
}
k->ad = say_ayir(k->uzunluk + 1);
if (k->ad == NULL) {
say_birak(k);
return NULL;
}
memcpy(k->ad, ad, k->uzunluk + 1);
return k;
}
void kayit_yok_et(Kayit *k) {
if (k == NULL) { return; }
say_birak(k->ad);
say_birak(k);
}
const char *kayit_ad(const Kayit *k) { return k->ad; }
char *kayit_ad_kopyasi(const Kayit *k) {
char *kopya = say_ayir(k->uzunluk + 1);
if (kopya != NULL) { memcpy(kopya, k->ad, k->uzunluk + 1); }
return kopya;
}
Birden Çok Kaynağın Tek Çıkış Noktalı Temizliği
kayit_olustur tek bloğu kendi bırakıyor çünkü tek blok ayırıyor. Bir işlev birden çok bloğu
sırayla ayırıyorsa ve ortadaki biri geçersizse, önce ayrılmış olanları geri sarmak
gerekir. Bu kalıbın adı tek çıkış noktalı temizliktir: işlev tek bir temizle: etiketine
goto ile atlar ve o etiketten itibaren, o ana kadar ayrılmış olan her şeyi bırakır.
goto C’de genelde önerilmeyen bir yapıdır, çünkü kontrol akışını izlemeyi zorlaştırabilir.
Bu kalıp, o genel önerinin kabul edilmiş tek istisnasıdır: goto yalnız ileri yönde,
yalnız işlevin sonundaki tek bir etikete, yalnız temizlik amacıyla kullanılıyor. Alternatif
yazım — her başarısızlık noktasında o ana kadar ayrılmış her bloğu tek tek bırakan tekrarlı
kod — aynı işi yapar ama ayrılan blok sayısı arttıkça satır sayısı da katlanarak artar ve her
yeni kaynak eklendiğinde önceki tüm başarısızlık dallarının güncellenmesi gerekir. goto temizle bunun yerine geri sarma mantığını tek bir yerde toplar; yeni bir kaynak
eklendiğinde yalnız yeni bir if ve yeni bir ayırma satırı eklenir, temizle: etiketinin
kendisi değişmez.
// uclu.h — uc kaynakli ayirmanin bildirimi
#ifndef UCLU_H
#define UCLU_H
#include <stddef.h>
int uclu_ayir(size_t a, size_t b, size_t c, size_t sinir,
int **p1, int **p2, int **p3);
#endif
// uclu.c — goto temizle kalibiyla kismi basarisizlikta geri sarma
#include <stddef.h>
#include "uclu.h"
#include "sayac.h"
enum Durum { DURUM_TAMAM = 0, DURUM_HATA = 1 };
int uclu_ayir(size_t a, size_t b, size_t c, size_t sinir,
int **p1, int **p2, int **p3) {
int durum = DURUM_TAMAM;
*p1 = NULL;
*p2 = NULL;
*p3 = NULL;
if (a > sinir) {
durum = DURUM_HATA;
goto temizle;
}
*p1 = say_ayir(sizeof **p1);
if (b > sinir) {
durum = DURUM_HATA;
goto temizle;
}
*p2 = say_ayir(sizeof **p2);
if (c > sinir) {
durum = DURUM_HATA;
goto temizle;
}
*p3 = say_ayir(sizeof **p3);
return durum;
temizle:
say_birak(*p3);
say_birak(*p2);
say_birak(*p1);
*p1 = *p2 = *p3 = NULL;
return durum;
}
SL5. İlk çağrıda b değeri (9) sınırı (5) aştığı için *p1 zaten ayrılmışken
temizle: etiketine atlanacak, o tek blok geri bırakılacak; ikinci çağrıda üçü de sınırın
altında kalacağı için hiçbir zaman temizle:e uğranmayacak, üç blok da çağırana geçecek ve
çağıran onları kendisi bırakacak. Aşağıdaki tek main, bu iki yolu da kayit_olustur’un
hata yolu ve errno’nun okunuşuyla birlikte tek koşumda ölçüyor.
uclu_ayir’ın temizle: etiketi yalnız o ana kadar ayrılmış olanı bırakır — say_birak
işlevi NULL işaretçiyle çağrıldığında hiçbir şey yapmadığı için (sayac.c’deki if (blok != NULL) denetimi), henüz ayrılmamış *p2 ve *p3 için say_birak(*p2) ve say_birak(*p3)
çağrıları güvenle boşa düşer. Bu, geri sarma kalıbının neden hep aynı sırayla, hep aynı üç
çağrıyla yazılabildiğini açıklıyor: kod, hangi bloğun gerçekten ayrıldığını ayrı ayrı
izlemek zorunda değil, çünkü say_birak zaten “ayrılmamışsa dokunma” garantisini taşıyor.
Garantiyi taşıyan say_birak’tır, goto değil — goto yalnız akışı tek bir noktaya toplar.
Dönüş Kodu ve Hata Numarası
uclu_ayir bir int döndürüyor ve bu değer yalnız başarılı mı başarısız mı sorusuna
yanıt veriyor; hangi kaynağın reddedildiğini söylemiyor — o bilgi isteniyorsa ayrı bir
parametreyle taşınmalıdır. Standart kütüphanenin kendi hata bildirimi ise errno üzerinden
işler: bir işlev başarısız olduğunda errno’yu sıfırdan farklı bir değere ayarlar, ama bu
değerin kendisi hedefe bağlıdır ve basılmaz; yalnız sıfır olup olmadığı okunur.
Dönüş kodu ve errno aynı sorunun iki farklı çözümüdür ama aynı katmanda çalışmazlar. Dönüş
kodu o işlevin kendi tasarımıdır — uclu_ayir’ın int dönüşü, o işlevi yazan kişinin
seçtiği bir sözleşmedir ve başka bir işlevin dönüş değeriyle hiçbir ortak kuralı paylaşmaz.
errno ise standart kütüphanenin ortak kanalıdır: strtol de, dosya işlemleri de, bellek
ayırma da aynı errno değişkenini kullanır — bu yüzden hangi çağrının hangi hatayı bıraktığını
ayırt etmek, her çağrıdan hemen önce sıfırlamayı zorunlu kılar.
// olcum.c — hata yolu, goto temizle ve errno tek kosumda olculuyor
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include "sahip.h"
#include "sayac.h"
#include "uclu.h"
static int fark(void) {
return ayrilan_sayisi() - birakilan_sayisi();
}
static void gozle(const char *ad, const char *sonuc) {
printf("%-28s%s\n", ad, sonuc);
}
int main(void) {
char tampon[64];
printf("%-28s%s\n", "gozlem", "sonuc");
/* --- hata yolunda temizlik: kayit_olustur kendi ayirdigini kendi birakir --- */
int once = fark();
Kayit *uzun = kayit_olustur("cok uzun bir kayit adi", 8);
snprintf(tampon, sizeof tampon, "bos %d, sayac farki %d", uzun == NULL, fark() - once);
gozle("hata yolunda temizlik", tampon);
/* --- goto temizle: kismi basarisizlikta geri sarma --- */
int *a = NULL, *b = NULL, *c = NULL;
once = fark();
int durum = uclu_ayir(2, 9, 2, 5, &a, &b, &c);
snprintf(tampon, sizeof tampon, "durum %d, sayac farki %d", durum, fark() - once);
gozle("ikinci kaynak reddedildi", tampon);
once = fark();
durum = uclu_ayir(1, 2, 3, 5, &a, &b, &c);
say_birak(a);
say_birak(b);
say_birak(c);
snprintf(tampon, sizeof tampon, "durum %d, sayac farki %d", durum, fark() - once);
gozle("ucu de kabul edildi", tampon);
/* --- errno: yalniz sifir olup olmadigi okunuyor --- */
errno = 0;
char *bitis = NULL;
long tasan = strtol("99999999999999999999", &bitis, 10);
(void)tasan;
gozle("tasan sayi ayristirma", errno != 0 ? "hata numarasi sifir degil" : "hata numarasi sifir");
errno = 0;
long normal = strtol("42", &bitis, 10);
(void)normal;
gozle("normal sayi ayristirma", errno == 0 ? "hata numarasi sifir" : "hata numarasi sifir degil");
return 0;
}
gozlem sonuc hata yolunda temizlik bos 1, sayac farki 0 ikinci kaynak reddedildi durum 1, sayac farki 0 ucu de kabul edildi durum 0, sayac farki 0 tasan sayi ayristirma hata numarasi sifir degil normal sayi ayristirma hata numarasi sifir
SL6. kayit_olustur içinde k->uzunluk >= sinir denetimi tutunca işlev kendi ayırdığı
k’yı say_birak ile bırakıp NULL döner; sayaç farkının 0 çıkması, ayrılan tek bloğun
aynı işlev içinde bırakıldığının kanıtıdır. Her iki strtol çağrısından önce errno açıkça
sıfırlanıyor: errno yalnız bir hata olduğunda ayarlanır, başarıda sıfırlanacağı garanti
değildir — önceki bir çağrıdan kalan değeri okumamak için denetimden önce sıfırlamak
zorunludur.
Tabloda görünen beş satırın hiçbiri birbirinin devamı değildir; her biri kendi once/fark
çiftiyle ya da kendi errno sıfırlamasıyla bağımsız başlıyor. Bu ayrım kasıtlı: iki
ölçümü art arda koşturup aradaki farkı tek bir sayaç okuyarak yorumlamak, hangi ölçümün hangi
değişikliğe yol açtığını karıştırabilir. fark() işlevinin her gözlemden önce çağrılması —
once = fark(); satırı — bu karışmayı önlüyor: her satırın sayaç farkı yalnız o satırın
kendi çağrısından geliyor, öncekilerin birikimli etkisinden değil.
Sınırlayıcı Ölçüm
Kısmi başarısızlıkta ayrılmış olanı kimin bıraktığı her işlevde ayrı ayrı yazılmak
zorundadır. kayit_olustur kendi ayırdığını kendi bıraktı; uclu_ayir da öyle yaptı — ama
bu bir dil kuralı değil, o işlevlerin kendi seçtiği sözleşmedir. Başka bir işlev, kısmi
başarısızlıkta ayırdığını bırakmadan NULL dönebilir ve derleyici bunu engellemez; sözleşme
yalnız başlık dosyasındaki yorumda yazılıdır ve çağıran onu okumak zorundadır.
Bu, dersin ölçtüğü şeyin sınırını da çiziyor: kayit_olustur ve uclu_ayir’ın kendi
başarısızlık yollarını doğru temizlediğini gösterdik, ama bu ikisinin doğru yazılmış olması
başka bir işlevin de aynı disiplini izleyeceğinin kanıtı değildir. Her yeni işlev için aynı
soru yeniden sorulmalıdır: kısmi başarısızlıkta ne ayrıldı, kim bırakacak ve bu sözleşme
nerede yazılı? Ölçüm burada durur; sözleşmenin var olup olmadığını denetleyen bir araç değil,
sözleşmenin bir örneğini koşturan bir derstir bu.
Bu sınır, errno için de aynı şekilde geçerlidir: standart, bir işlevin başarısızlıkta
errno’yu ayarlayacağını garanti eder, ama hangi işlevlerin onu okuyup okumadan
üzerine yazabileceğini garanti etmez. Aradaki farkı gözlemlemenin tek yolu, her çağrıdan
hemen önce errno’yu sıfırlamak ve hemen sonra okumaktır — iki çağrı arasına başka bir
kütüphane işlevi girerse, o işlev başarılı olsa bile errno’yu değiştirmiş olabilir. Bu
derste ölçülen iki kalıp da (goto temizle ve errno sıfırlama) aynı ilkeye dayanıyor:
dil hiçbir temizliği ya da hiçbir sıfırlamayı kendiliğinden yapmaz, yapan taraf her
zaman kod satırının kendisidir.
Özet
- Bir işlev kısmi başarısızlıkta ayırdığı bloğu kendi bırakabilir (
kayit_olustur) ya da bırakmadan geri dönebilir; hangisi olduğu işlevin kendi sözleşmesindedir. - Tek çıkış noktalı temizlik (
goto temizle) birden çok kaynağı sırayla geri sarmanın standart kalıbıdır ve iki farklı başarısızlık noktasında da sayaç farkını 0’a getirdi. errnoyalnız sıfır olup olmadığıyla okunur; numara değeri ve ileti metni hiçbir gözlemde basılmadı.- Bu dersin ölçtüğü bellek çağrılan sınıfındadır: hata yolunda temizliği modülün kendisi üstlendi, çağırana kalan bir şey olmadı.
- Üç kalıbın da ortak noktası, dilin hiçbirini kendiliğinden sağlamamasıdır —
goto temizleetiketini yazan,say_birak’ınNULL’da boşa düştüğünü garanti eden veerrno’yu her çağrıdan önce sıfırlayan, hep programcının kendi satırlarıdır.
Sonraki Adım
kayit_olustur hata yolunda temizlik yaparken bir şeyi de reddetti: verilen adın sınıra
sığmaması. O sınır — sinir parametresi — kaynaktan gelmiyor, çağıran tarafından
veriliyor. Sonraki ders bu parametreyi merkeze alıyor: uzunluğu çağıranın vermesi, bir
arabellek taşmasını nasıl önlüyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.