İçeriğe geç
academia.sh

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.
  • errno yalnı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 temizle etiketini yazan, say_birak’ın NULL’da boşa düştüğünü garanti eden ve errno’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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat