İçeriğe geç
academia.sh

Ders 08 / 15

Serbest Bırakma Disiplini

Bir işlev bir metni döndürdüğünde, o metnin sahibinin kim olduğu imzada yazmaz — yazan, başlık dosyasındaki yorumdur. Aynı kaydı gösteren iki işlev, biri ödünç verip biri kopya döndürerek, aynı sözleşmenin iki farklı sahibe nasıl bağlanabildiğini gösteriyor.

Yayın Güncelleme

Bu derste

  • Bir başlık dosyasındaki yorumun sahiplik sözleşmesini nasıl taşıdığını gösterir
  • kayit_ad ile kayit_ad_kopyasi'nin aynı metni iki farklı sahiple döndürdüğünü sayaç farkıyla ayırt eder
  • Bu dersteki yükümlülük sorusunu M10/K02'deki ömür sorusundan ayırır
İçindekiler

Önceki ders dört ayırma işlevinin dördünün de aynı sözleşmeyi taşıdığını gösterdi: ayıran bırakır. Ama bu, sözleşmenin tek biçimi değil. Bir işlev bir işaretçi döndürdüğünde, o işaretçinin sahibinin kim olacağına imza karar vermez — char * döndüren iki işlev, aralarında hiçbir görünür fark olmadan, birbirinden tamamen farklı bir yükümlülük taşıyabilir. Bu ders, aynı kaydı gösteren iki işlevle bu farkı ölçüyor: biri belleği modülün içinde tutuyor ve yalnız ödünç veriyor, öteki çağırana yeni bir blok döndürüyor.

Sözleşme Başlık Dosyasında Yazılıdır

Ölçüm, kursun geri kalanında yeniden kullanılan sahip.h ve sahip.c çiftiyle yapılıyor; ikisi de sayaçlı sarmalayıcının (sayac.h/sayac.c) üzerine kurulu, o yüzden aynı birim içinde onlar da yer alıyor. Kaydın içi (struct Kayitin alanları) sahip.hde görünmüyor — yalnızca bir isim (Kayit) ve dört işlev imzası var. Sahiplik bilgisi, imzanın üzerindeki yorumda.

// 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;
}

BE4. kayit_ad ile kayit_ad_kopyasi‘nin imzaları neredeyse aynıdır — ikisi de bir const Kayit * alır, ikisi de bir char dizisine işaretçi döndürür. Aralarındaki fark derleyicinin denetleyebileceği hiçbir yerde durmuyor; yalnız yorumda duruyor. Bu, C’nin bellek sahipliğini nasıl taşıdığının kısa özetidir: tip sistemi bir yükümlülüğü zorlamaz, yalnız bir insanın yazdığı ve bir başka insanın okuduğu bir cümle zorlar.

Ödünç Alınan ile Sahiplenilen

kayit_ad kaydın içindeki ad alanına doğrudan bir işaretçi döndürüyor — yeni bellek ayırmıyor. Bu işaretçi ödünçtür: geçerliliği kaydın kendisine bağlıdır, kayıt kayit_yok_et ile yok edilene kadar sürer. kayit_ad_kopyasi ise say_ayir ile yeni bir blok ayırıp içine aynı metni kopyalıyor; bu blok kaydın ömründen bağımsızdır ve sahibi artık çağırandır.

// sozlesme.c — odunc verilen ad ile donen kopyanin karsitligi
#include <stdio.h>
#include <string.h>
#include "sahip.h"
#include "sayac.h"

static void gozle(const char *ad, const char *sinif, const char *kanit) {
    printf("%-30s%-10s%s\n", ad, sinif, kanit);
}

int main(void) {
    char tampon[64];
    int once, sonra;

    printf("%-30s%-10s%s\n", "gozlem", "sahip", "kanit");

    Kayit *k = kayit_olustur("sozlesme", 16);

    /* --- cagrilan: modul sahiplenir, cagiran birakmaz --- */
    once = ayrilan_sayisi() - birakilan_sayisi();
    const char *odunc = kayit_ad(k);
    sonra = ayrilan_sayisi() - birakilan_sayisi();
    snprintf(tampon, sizeof tampon, "ad %s, sayac farki %d", odunc, sonra - once);
    gozle("odunc verilen ad", "cagrilan", tampon);

    /* --- cagiran: donen kopyanin sahibi cagirandir --- */
    once = ayrilan_sayisi() - birakilan_sayisi();
    char *kopya = kayit_ad_kopyasi(k);
    int esit = (kopya != NULL) && (strcmp(kopya, odunc) == 0);
    say_birak(kopya);
    sonra = ayrilan_sayisi() - birakilan_sayisi();
    snprintf(tampon, sizeof tampon, "esit metin %d, sayac farki %d", esit, sonra - once);
    gozle("donen kopya", "cagiran", tampon);

    /* --- odunc hala gecerli: kopya birakildi, kayit hala ayakta --- */
    const char *odunc_sonra = kayit_ad(k);
    snprintf(tampon, sizeof tampon, "ad %s, kayittan once mi %d", odunc_sonra, odunc_sonra == odunc);
    gozle("odunc hala gecerli", "cagrilan", tampon);

    /* --- kayit yok edilince odunc de kaydin omrunu takip eder --- */
    once = ayrilan_sayisi() - birakilan_sayisi();
    kayit_yok_et(k);
    sonra = ayrilan_sayisi() - birakilan_sayisi();
    snprintf(tampon, sizeof tampon, "sayac farki %d", sonra - once);
    gozle("kaydin yok edilmesi", "cagrilan", tampon);

    return 0;
}
gozlem                        sahip     kanit
odunc verilen ad              cagrilan  ad sozlesme, sayac farki 0
donen kopya                   cagiran   esit metin 1, sayac farki 0
odunc hala gecerli            cagrilan  ad sozlesme, kayittan once mi 1
kaydin yok edilmesi           cagrilan  sayac farki -2

BE5. odunc verilen ad gözleminde sayaç farkı 0’dır — kayit_ad hiçbir şey ayırmadı, hiçbir şey bırakmadı; yalnız var olan bir bloğa işaret eden bir işaretçi kopyaladı. donen kopya gözleminde de fark 0, ama nedeni farklı: burada bir ayırma oldu, ardından say_birak(kopya) ile hemen bırakıldı. İki sıfır, iki farklı öyküyü gizliyor — biri hiç ayırma yapmadığı için sıfır, öteki ayırdığını bıraktığı için sıfır. kaydin yok edilmesi gözlemindeki -2 ise beklenen: kayit_yok_et iki bloğu (adı ve kaydın kendisini) bıraktı, hiçbirini yeniden ayırmadı; sayaç farkı negatife döndü çünkü bu kez ölçülen yalnız bırakma tarafı.

odunc hala gecerli gözlemi, ödünç verilen işaretçinin kaydın ömrüne bağlı olduğunu gösteriyor: kayit_ad_kopyasi çağrısı ayrı bir blok ayırıp bıraktı, ama kaydın kendisine hiç dokunmadı — bu yüzden kayit_ad’in ikinci çağrısı aynı adresi (odunc_sonra == odunc) döndürdü. Ödünç verilen işaretçiyi geçersiz kılan tek şey kayit_yok_et’in kendisidir.

İki Bağımsız Kopya

kayit_ad_kopyasi her çağrıldığında yeni bir blok ayırıyor; bu, aynı kayıttan iki kopya istenirse iki ayrı sahibin doğduğu anlamına gelir. Kopyalardan biri erken bırakılırsa öteki bundan hiç etkilenmez — çünkü ikisi arasında adresten başka hiçbir bağ yok.

// iki_kopya.c — ayni kayittan alinan iki kopyanin bagimsiz omru
#include <stdio.h>
#include <string.h>
#include "sahip.h"
#include "sayac.h"

int main(void) {
    char tampon[96];
    Kayit *k = kayit_olustur("bagimsiz", 16);

    int once = ayrilan_sayisi() - birakilan_sayisi();
    char *kopya_a = kayit_ad_kopyasi(k);
    char *kopya_b = kayit_ad_kopyasi(k);
    int ara_fark = (ayrilan_sayisi() - birakilan_sayisi()) - once;

    say_birak(kopya_a);
    int b_ayakta_mi = (strcmp(kopya_b, "bagimsiz") == 0);
    int orta_fark = (ayrilan_sayisi() - birakilan_sayisi()) - once;

    say_birak(kopya_b);
    int son_fark = (ayrilan_sayisi() - birakilan_sayisi()) - once;
    kayit_yok_et(k);

    snprintf(tampon, sizeof tampon,
             "iki ayirma sonrasi fark %d, a birakilinca %d, b hala okunuyor %d, ikisi de birakilinca %d",
             ara_fark, orta_fark, b_ayakta_mi, son_fark);
    printf("%-30s%s\n", "iki bagimsiz kopya", tampon);

    return 0;
}
iki bagimsiz kopya            iki ayirma sonrasi fark 2, a birakilinca 1, b hala okunuyor 1, ikisi de birakilinca 0

BE6. kopya_a ile kopya_b aynı kaynaktan (k->ad) türedi, ama ikisi de kayit_ad_kopyasi üzerinden ayrı bir say_ayir çağrısıyla doğdu — bu yüzden sayaç farkı iki ayırmadan sonra 2. say_birak(kopya_a) çağrısı yalnız kopya_a’nın bloğunu etkiliyor; kopya_b hâlâ geçerli ve strcmp bunu doğruluyor (b hala okunuyor 1). Bu, kayit_ad’in tek bir ödünç işaretçisi döndürmesiyle tam tersi bir örüntü: orada aynı kaydın iki çağrısı aynı adresi verirdi (odunc_sonra == odunc), burada aynı kaydın iki kayit_ad_kopyasi çağrısı farklı iki blok verir. Sahiplik sınıfı ikisinde de çağırandır, ama her kopya kendi başına, birbirinden bağımsız bir sahiplik taşır — birini bırakmak ötekinin sözleşmesini hiç etkilemez.

Sınır Aşımında Temizlik

kayit_olustur’un başlık dosyasındaki yorumu bir üçüncü olasılığı da kayda geçiriyor: ad verilen sınıra sığmazsa işlev NULL döner, ama o ana kadar ayırdığı belleği kendisi bırakır. Bu, çağrılan sınıfının yalnızca başarılı dönüşte değil, başarısız dönüşte de bir yükümlülük taşıdığını gösteriyor — modül kendi yarım kalan işini kendi temizliyor, çağırana hiçbir şey bırakmıyor.

// sinir.c — kayit_olustur sinira sigmayinca kendi ayirdigini kendisi birakir
#include <stdio.h>
#include "sahip.h"
#include "sayac.h"

int main(void) {
    char tampon[64];
    int once = ayrilan_sayisi() - birakilan_sayisi();

    Kayit *basarisiz = kayit_olustur("cok-uzun-bir-ad", 4);
    int null_mu = (basarisiz == NULL);
    int fark = (ayrilan_sayisi() - birakilan_sayisi()) - once;

    snprintf(tampon, sizeof tampon, "NULL mu %d, sayac farki %d", null_mu, fark);
    printf("%-30s%s\n", "sinira sigmayan ad", tampon);

    return 0;
}
sinira sigmayan ad            NULL mu 1, sayac farki 0

BE7. "cok-uzun-bir-ad" on beş karakter uzunluğunda, sınır ise 4 — kayit_olustur k için bir blok ayırdıktan sonra uzunluk denetimini başarısız buluyor ve k->adı hiç ayırmadan say_birak(k) çağırıp NULL döndürüyor. Sayaç farkı yine de 0: tek ayırma, tek bırakma, ikisi de işlevin kendi içinde oldu. Çağıran taraf basarisiz’in NULL olduğunu görüyor ama hangi bloğun ayrılıp hangisinin bırakıldığını hiç bilmiyor — ve bilmesine gerek de yok, çünkü sözleşme başarısızlık durumunda da modülün üzerinde.

Sınırlayıcı Ölçüm

sahip.hdeki yorum iki yönde de bağlayıcıdır. Çağıran kayit_ad’in döndürdüğü işaretçiyi say_birak ile bırakırsa, kaydın kendi bırakma çağrısı (kayit_yok_et içindeki say_birak(k->ad)) aynı bloğu ikinci kez bırakır — bu, tanımsız davranıştır ve bu derste koşturulmaz. Tersi de kırılabilir: çağıran kayit_ad_kopyasi’nin döndürdüğü bloğu hiç bırakmazsa, blok kaydın ömründen bağımsız olduğu için kaydın yok edilmesi de onu kurtarmaz — sızar. Sözleşmenin iki yönü de aynı yorumda yazılı, ama derleyici hiçbirini denetlemez; denetleyen, yalnız kodu okuyan kişidir.

Bu iki kırılma biçiminin ortak noktası, ikisinin de imzadan görünmemesidir. char * kayit_ad(const Kayit *k) ile char *kayit_ad_kopyasi(const Kayit *k) derleyicinin gözünde eş değerdedir — ikisi de bir char dizisine işaretçi döndürür, ikisi de const Kayit * alır. Çağıran taraf yanlış işlevi yanlış şekilde ele alsa (kayit_ad’in dönüşünü bırakmaya kalksa ya da kayit_ad_kopyasi’nin dönüşünü hiç bırakmasa) derleme yine de başarılı olur; hata yalnız çalışma zamanında, çift bırakma ya da sızıntı olarak ortaya çıkar. Bu, önceki dersteki realloc’un başarısız dönüşünü üzerine yazmanın aynı sınıftan bir hatadır: ikisi de sözdizimsel olarak geçerli, ikisi de derleyicinin yakalayamadığı bir sahiplik ihlalidir. Aradaki fark kaynaktır — orada hata bir dönüş değerinin göz ardı edilmesinden, burada bir yorumun okunmamasından doğuyor.

sahip.cnin kendi içinde bu iki kırılmaya karşı hiçbir savunması yok; savunmayı üstlenen, modülün kullanıcısıdır. kayit_olustur ile kayit_yok_et çifti bir kaydın tüm ömrünü modülün kendi kontrolünde tutuyor — çağıran yalnız bu çifti simetrik kullandığı sürece güvende. kayit_ad‘in ödünç verdiği işaretçiyi bir değişkende saklayıp kaydı yok ettikten sonra o değişkeni okumak da aynı aileden bir hatadır (sarkan işaretçi); bu ders bunu ayrıca koşturmuyor çünkü zaten M10/K02’nin ömür sorusuna geri dönüyor — burada asıl konu kimin bırakacağı, orada konu ne zaman bittiğiydi ve ikisi burada aynı anda kırılıyor.

Bu ders, M10/K02’de ölçülen ömür sorusuyla karışmaz. Orada bir değerin ömrünün nerede bittiği ölçülmüştü ve o yeri dil belirliyordu. Burada ölçülen, o ömrü bitirmekle kimin yükümlü olduğudur ve bunu belirleyen dil değil, sahip.hdeki sözleşmedir. kayit_ad ile kayit_ad_kopyasi aynı metni verir, ikisinin de ömrü sonunda biter — ama biri o sonu kaydın sahibine bırakır, öteki çağırana devreder.

Özet

  • Bir işlevin döndürdüğü işaretçinin sahibi imzadan değil, başlık dosyasındaki yorumdan anlaşılır; bu dersin ölçtüğü bellek sınıfı çağrılandır.
  • kayit_ad ödünç verir: sayaç farkı 0, geçerliliği kaydın ömrüne bağlıdır.
  • kayit_ad_kopyasi yeni bir blok ayırır: sayaç farkı da 0, ama bu kez ayırma ve bırakma gerçekten oldu — sahibi artık çağırandır.
  • Sözleşmeyi kırmak (ödünç alınanı bırakmak ya da kopyayı hiç bırakmamak) çift bırakma ya da sızıntı doğurur; ikisi de bu derste koşturulmadı, yalnız tarif edildi.
  • Bu dersteki yükümlülük sorusu, M10/K02’deki ömür sorusundan ayrıdır: biri kim bırakacakı sorar, öteki ne zaman biteri.

Sonraki Adım

kayit_ad_kopyasi’nin döndürdüğü kopya bırakılmazsa ne olur, bu derste yalnız tarif edildi — koşturulmadı. Sıradaki ders tam bunu ölçüyor: dönüşü hiç tutulmayan bir kopya, hiçbir işaretçinin artık ulaşamadığı bir blok bırakır ve bu blok, ne bir hata mesajıyla ne bir çökmeyle görünür — yalnız sayaç farkıyla.

İ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