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_kopyasiyeni 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.