Ders 07 / 15
Bellek Ayırma İşlevleri
malloc, calloc, realloc ve free aynı öbekten çalışır ve dördü de aynı soruyu bırakır: ayırdığın bloğu kim bırakacak? Yanıt hep aynıdır — ayıran çağırandır; ders bunu sayaç farkıyla, sıfırlamayı doğrudan okuyarak, yeniden boyutlandırmayı ise eski sahipliğin değişmediğini göstererek ölçüyor.
Yayın Güncelleme
Bu derste
- malloc, calloc ve realloc'un ayırdığı bloğun sahibinin her durumda çağıran olduğunu sayaç farkıyla ölçer
- calloc'un ayırdığı bloğu sıfırladığını değerleri doğrudan okuyarak gösterir
- Bir yeniden boyutlandırmanın eski bloğun sahipliğini değiştirmediğini ve başarısızlıkta eski bloğun geçerli kaldığını açıklar
İçindekiler
Önceki altı ders bir işaretçinin gösterdiği belleğin nereden geldiğini hep önceden verilmiş
kabul etti: bir yerel değişkenin adresi, bir dizinin ilk elemanı, bir kaydın içindeki bir
alan. Fonksiyon İşaretçileri dersi bile bir geri çağrının bağlamını taşıyordu ama o
bağlamın kendisinin nasıl var olduğunu sormadı. Bu ders o boşluğu kapatıyor: bir blok, kimse
onu istemeden önce, önce ayrılmak zorundadır — ve ayıran taraf, dört işlevden hangisini
çağırırsa çağırsın, aynı yükü üstlenir.
C’nin standart kitaplığı dört ayırma ve bırakma işlevi verir: malloc istenen boyutta
sıfırlanmamış bir blok döndürür, calloc eleman sayısı ve eleman boyutunu ayrı parametre
alıp bloğu sıfırlayarak döndürür, realloc var olan bir bloğu büyütür ya da küçültür,
free bir bloğu bırakır. Dördü de aynı sözleşmeyi taşır: dönen işaretçinin sahibi
çağırandır — bloğu isteyen taraf, onu bırakmakla da yükümlüdür. Bu kursun ölçü ekseninde
bu, dört sınıftan en dolaysız olanıdır; burada bir sözleşme aramaya gerek yoktur, sözleşme
işlevin kendi imzasındadır.
Ayıran Bırakır
Ölçüm, kursun geri kalanında yeniden kullanılan aynı sayaçlı sarmalayıcıyla yapılıyor.
sayac.c her say_ayir ve say_buyut çağrısını, her say_birak çağrısını sayıyor; bir
gözlemin başındaki ve sonundaki fark 0 ise, o gözlemde ayrılan her şey bırakılmış demektir.
// 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;
}
Bu iki dosya kursun kalan on üç dersinde de aynı adla ve aynı içerikle dönüyor; burada ilk
kez göründükleri için tam hâlleriyle yazıldı. say_ayir malloc’ı, say_buyut realloc’ı
sarmalıyor — ikisi de yalnız sayacı artırıp gerçek işlevi çağırıyor, davranışı değiştirmiyor.
// tahsis.c — malloc, calloc ve realloc'un cagiran sinifiyla olcumu
#include <stdio.h>
#include <stdlib.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");
/* --- malloc: ham blok, ayiran birakir --- */
once = ayrilan_sayisi() - birakilan_sayisi();
int *ham = say_ayir(3 * sizeof *ham);
int toplam = 0;
if (ham != NULL) {
for (int i = 0; i < 3; i++) {
ham[i] = i + 1;
toplam += ham[i];
}
}
say_birak(ham);
sonra = ayrilan_sayisi() - birakilan_sayisi();
snprintf(tampon, sizeof tampon, "toplam %d, sayac farki %d", toplam, sonra - once);
gozle("ham blok", "cagiran", tampon);
/* --- calloc: sifirlanmis blok, degerler dogrudan okunur --- */
int *sifirlanan = calloc(4, sizeof *sifirlanan);
int hepsi_sifir = 1;
if (sifirlanan != NULL) {
for (int i = 0; i < 4; i++) {
if (sifirlanan[i] != 0) {
hepsi_sifir = 0;
}
}
}
snprintf(tampon, sizeof tampon, "hepsi sifir %d", hepsi_sifir);
gozle("sifirlanan blok", "cagiran", tampon);
free(sifirlanan);
/* --- realloc: yeniden boyutlandirma yeni bir sahip dogurmaz --- */
once = ayrilan_sayisi() - birakilan_sayisi();
int *buyuyen = say_buyut(NULL, 2 * sizeof *buyuyen);
int korunan = 0;
if (buyuyen != NULL) {
buyuyen[0] = 7;
int *yeni = say_buyut(buyuyen, 8 * sizeof *yeni);
if (yeni != NULL) {
buyuyen = yeni;
}
korunan = buyuyen[0];
}
say_birak(buyuyen);
sonra = ayrilan_sayisi() - birakilan_sayisi();
snprintf(tampon, sizeof tampon, "ilk eleman %d, sayac farki %d", korunan, sonra - once);
gozle("yeniden boyutlandirma", "cagiran", tampon);
printf("ayrilan %d birakilan %d fark %d\n", ayrilan_sayisi(), birakilan_sayisi(),
ayrilan_sayisi() - birakilan_sayisi());
return 0;
}
gozlem sahip kanit ham blok cagiran toplam 6, sayac farki 0 sifirlanan blok cagiran hepsi sifir 1 yeniden boyutlandirma cagiran ilk eleman 7, sayac farki 0 ayrilan 2 birakilan 2 fark 0
BE1. Üç gözlemin üçü de sayaç farkını 0’a döndürdü ve bunun tek nedeni var: her
ayırma çağrısının hemen ardından ya da işlev sonunda karşılığı olan bir bırakma çağrısı var.
Bu, dört işlevin ortak bir davranışı değil, çağıranın verdiği bir söz: malloc,
calloc ve realloc bir sözleşme dayatmaz, yalnız bir blok döndürür; bloğu bırakmayı
unutmak derleyicinin uyarısını da tetiklemez, bağlayıcının hatasını da. ham blok üç
elemanlık bir dizi gibi kullanıldı ve toplam 6 verdi — bu, önceki dersteki bozunmuş
dizilerden farklı olarak, tipi belli ve uzunluğu kaynakta sabit bir bloktu; farkı, bu
uzunluğun bu kez sabit bir tanımdan değil bir çalışma zamanı isteğinden gelmesiydi.
calloc’un Sıfırlaması
malloc istediği boyutu ayırır ama içeriğine dokunmaz: döndürdüğü bellek, önceden orada ne
duruyorsa onu taşır ve bu değer tanımsızdır — okunması güvenli değildir, bu yüzden ders
bu değeri hiçbir yerde basmıyor. calloc aynı isteği iki parametreyle alır — eleman sayısı
ve eleman boyutu — ve döndürdüğü bloğu sıfırlar. sifirlanan blok gözlemi bunu doğrudan
okuyarak kanıtlıyor: dört int’in dördü de, hiçbir atama yapılmadan, sıfır değerinde
bulundu.
Bu fark yalnız bir kolaylık değildir. malloc ile ayrılan bir blok, ilk kullanımdan önce
elle sıfırlanmazsa, üzerinde okunan her değer önceki bir ayırmanın artığı olabilir —
calloc bu riski işlevin kendi sözleşmesine taşır. İki elemanın çarpımı taştığında
(eleman_sayisi * eleman_boyutu bir tam sayı sınırını aşarsa) calloc’un bunu kendisinin
denetlemesi de aynı nedenle önemlidir: malloc(eleman_sayisi * eleman_boyutu) yazmak bu
denetimi çağırana bırakır, calloc(eleman_sayisi, eleman_boyutu) yazmak denetimi işlevin
içine alır. Sahiplik burada da değişmiyor — sıfırlanan blok da çağıranındır ve free
ile bırakılması gerekiyor, ders bunu son satırda yapıyor.
Yeniden Boyutlandırma Yeni Bir Sahip Doğurmaz
realloc (burada say_buyut sarmalayıcısı üzerinden) dört işlevin en tuzaklısıdır, çünkü
tek çağrıda üç farklı şey olabilir: blok yerinde büyür, blok başka bir adrese taşınır ve
eski adres bırakılır, ya da istek karşılanamaz ve NULL döner. yeniden boyutlandirma
gözlemi ilk iki durumu tek akışta gösteriyor: buyuyen iki elemanlık ayrıldı, ilk elemanına
7 yazıldı, sonra sekiz elemana büyütüldü. Çıktıdaki ilk eleman 7 değeri, büyütme
sırasında adresin değişip değişmediğinden bağımsız olarak, içeriğin taşındığını
kanıtlıyor — realloc yeni bir yere geçtiyse bile eski içeriği yeni bloğa kendisi kopyaladı.
Sahiplik açısından belirleyici olan şudur: büyütme öncesi buyuyen’in sahibi çağırandı,
büyütme sonrası da hâlâ çağırandır — yeni bir sahip doğmadı, yalnız aynı sahibin elindeki
adres değişmiş olabilir. Sayaç bunu doğruluyor: say_buyut(NULL, ...) çağrısı yeni bir blok
sayıldığı için ayrılan sayacını bir artırdı, ikinci say_buyut(buyuyen, ...) çağrısı ise
yeni bir ayırma saymadı — sayac.cdeki yeni_blok denetimi tam bunun için var. Gözlemin
sonunda say_birak(buyuyen) çağrısı tek bir bloğu bırakıyor ve fark 0’a dönüyor.
Kodun yapısı, yeni değişkenine dikkat çekiyor: say_buyut’un dönüşü doğrudan buyuyen’in
üzerine yazılmıyor, önce ayrı bir değişkene alınıyor ve NULL olup olmadığı denetleniyor.
Bunun nedeni, başarısız bir yeniden boyutlandırmada eski bloğun hâlâ geçerli ve hâlâ
çağıranın olmasıdır: realloc isteği karşılayamazsa orijinal bloğu bırakmaz, olduğu gibi
bırakır ve NULL döner. Dönüşü doğrudan buyuyen = say_buyut(buyuyen, ...); biçiminde
yazmak, başarısızlık durumunda buyuyen’in kendisini NULL’a çevirir — o andan sonra eski
bloğa ulaşacak hiçbir işaretçi kalmaz ve blok sahipsiz hâle gelir, sızıntı olarak
görünürdü. Bu dersin kodu bu hatayı işlemiyor, çünkü istek burada her zaman karşılanabilecek
kadar küçük; ama akış — ayrı değişkene alma, NULL denetimi, yalnız başarıyla üzerine yazma
— tam olarak bu ikinci kalıbı da engelleyecek biçimde kuruldu. Doğru kalıp ile yanlış
kalıp arasındaki fark, bir satırlık bir atamadır, ama sonucu bir sahibin bloğu hâlâ
bulabilmesi ile tamamen kaybetmesi arasındaki farktır.
Zincirleme Büyütme
Önceki gözlem buyuyen’i tek adımda büyüttü. Ama bir dizinin dolarken tekrar tekrar
büyütülmesi de yaygın bir kalıptır — her eleman eklendiğinde bloğu bir eleman daha büyütmek
yerine, dolduğunda iki katına çıkarmak. Bu kalıbın sahiplik açısından ilginç yanı, kaç kez
büyütülürse büyütülsün bloğun hâlâ tek bir ayırma sayılmasıdır.
// zincir.c — art arda buyutmenin sayaci yalniz bir kez artirmasi
#include <stdio.h>
#include "sayac.h"
int main(void) {
char tampon[128];
int once = ayrilan_sayisi() - birakilan_sayisi();
int kapasite = 1;
int *dizi = say_buyut(NULL, kapasite * sizeof *dizi);
int dolu = 0;
for (int i = 0; i < 5; i++) {
if (dolu == kapasite) {
kapasite *= 2;
int *yeni = say_buyut(dizi, kapasite * sizeof *dizi);
if (yeni == NULL) {
break;
}
dizi = yeni;
}
dizi[dolu] = i + 1;
dolu++;
}
int toplam = 0;
for (int i = 0; i < dolu; i++) {
toplam += dizi[i];
}
int ara_fark = (ayrilan_sayisi() - birakilan_sayisi()) - once;
say_birak(dizi);
int son_fark = (ayrilan_sayisi() - birakilan_sayisi()) - once;
snprintf(tampon, sizeof tampon,
"dolu %d, kapasite %d, toplam %d, buyutme sonrasi fark %d, birakma sonrasi fark %d",
dolu, kapasite, toplam, ara_fark, son_fark);
printf("%-30s%s\n", "zincirleme buyutme", tampon);
return 0;
}
zincirleme buyutme dolu 5, kapasite 8, toplam 15, buyutme sonrasi fark 1, birakma sonrasi fark 0
BE2. Döngü kapasite’yi 1 → 2 → 4 → 8 olmak üzere üç kez büyüttü, yani say_buyut
toplam dört kez çağrıldı (ilk ayırma dahil). Buna karşın büyütmeler bittiğinde sayaç farkı
hâlâ 1: sayac.cdeki yeni_blok denetimi yalnız eski == NULL olduğu ilk çağrıda
ayrilan’ı artırıyor, sonraki üç çağrı aynı bloğun devamı sayılıyor. Bu, “Yeniden
Boyutlandırma Yeni Bir Sahip Doğurmaz” bölümünde tek adımda gösterilen kuralın, kaç adıma
yayılırsa yayılsın değişmediğini kanıtlıyor: sahiplik zincirin başında kaç kez el
değiştirdiğine değil, kaç kez yeni bir ayırmayla başladığına bakar. say_birak(dizi)
çağrısından sonra fark 0’a dönüyor — zincirin sonunda da, tıpkı başında olduğu gibi, tek
bir sahip vardı.
Küçülen Blok
Zincirleme büyütme bloğu hep genişletti; ama realloc aynı sözleşmeyle bloğu
küçültebilir de. Küçültme, büyütmenin aynadaki görüntüsü değildir: uygulama isterse
bloğu yerinde küçültür, isterse daha küçük bir adrese taşır, ama iki durumda da sahiplik
sorusunun yanıtı değişmez — küçülen blok da hâlâ çağırana aittir.
// kucul.c — realloc ile kucultme de ayni cagiran sahipligini korur
#include <stdio.h>
#include "sayac.h"
int main(void) {
char tampon[96];
int once = ayrilan_sayisi() - birakilan_sayisi();
int *genis = say_buyut(NULL, 10 * sizeof *genis);
for (int i = 0; i < 10; i++) {
genis[i] = i;
}
int ilk_fark = (ayrilan_sayisi() - birakilan_sayisi()) - once;
int *dar = say_buyut(genis, 2 * sizeof *dar);
int korunan_ilk = -1, korunan_ikinci = -1;
if (dar != NULL) {
korunan_ilk = dar[0];
korunan_ikinci = dar[1];
}
int kuculme_farki = (ayrilan_sayisi() - birakilan_sayisi()) - once;
say_birak(dar);
int son_fark = (ayrilan_sayisi() - birakilan_sayisi()) - once;
snprintf(tampon, sizeof tampon,
"ilk eleman %d, ikinci eleman %d, buyume farki %d, kuculme farki %d, birakma sonrasi %d",
korunan_ilk, korunan_ikinci, ilk_fark, kuculme_farki, son_fark);
printf("%-30s%s\n", "kuculen blok", tampon);
return 0;
}
kuculen blok ilk eleman 0, ikinci eleman 1, buyume farki 1, kuculme farki 1, birakma sonrasi 0
BE3. dar, genis’in ilk on elemanından yalnız ikisini tutacak kadar küçük ayrıldı;
buna karşın korunan_ilk ve korunan_ikinci değerleri, küçültmeden önce yazılmış 0 ve
1’i aynen taşıyor — realloc bloğu küçültürken kalan içeriği bozmuyor, yalnız artık
alanı serbest bırakıyor. Sayaç farkı ilk_fark’tan kuculme_farki’ya hiç değişmedi: ayırma
tek kaldı, büyüme de küçülme de aynı bloğun devamıydı. Bu, “Yeniden Boyutlandırma Yeni Bir
Sahip Doğurmaz” bölümündeki kuralın yönden bağımsız olduğunu gösteriyor — kural büyütmeye
özgü değil, realloc’un boyutu her iki yönde de değiştirebilmesine dayanıyor.
Sınırlayıcı Ölçüm
Boyutu sıfır olan bir malloc ya da calloc çağrısı standart tarafından iki yönde de
bırakılmıştır: uygulama ya NULL döndürebilir ya da free ile bırakılabilecek, boyutu
sıfır olan geçerli bir işaretçi döndürebilir. Bu, ortama bağlı bir davranıştır ve hangisinin
seçildiği burada basılmaz — kaynakta okunan tek şey, çağıranın bu iki olasılığın da
gerçekleşebileceğini bilerek yazması gerektiğidir. Dört ayırma işlevinin sahiplik
yönünden verdiği güvence değişmez — dönen işaretçi NULL değilse sahibi yine çağırandır —
ama bu güvence, boyutu sıfır olan bir istekte dönüşün ne olacağını söylemez.
Aynı belirsizlik realloc(blok, 0) çağrısı için de geçerlidir: bazı uygulamalarda bu çağrı
free(blok) ile aynı etkiyi yapıp NULL döner, bazılarında boyutu sıfır olan geçerli bir
blok döner. İkisi arasındaki fark, çağıranın bloğu hâlâ elinde tutup tutmadığını
etkiler — birinci durumda blok zaten bırakılmıştır, ikinci durumda hâlâ çağıranındır ve ayrı
bir free çağrısı gerekir. Bu kararsızlık yüzünden bu kursun kodu realloc’u hiçbir yerde
sıfır boyutla çağırmıyor; boyutu sıfıra indirmek istendiğinde blok doğrudan say_birak ile
bırakılıyor ve işaretçi ayrı olarak NULL’a atanıyor. Sahiplik sorusunun ortama bağlı iki
farklı yanıtı olabildiği bir yerde, ders o soruyu hiç sormamayı tercih ediyor.
Özet
malloc,callocvereallocdönen bloğun sahibi her zaman çağırandır; bu dersin ölçtüğü bellek sınıfı çağırandır.mallociçeriği sıfırlamaz,callocsıfırlar — bu fark ölçümde değerler doğrudan okunarak gösterildi, tahmin edilmedi.reallocyeni bir sahip doğurmaz; içerik korunur, sayaç yalnız ilk ayırmada artar.- Başarısız bir
realloceski bloğu bırakmaz; dönüşü doğrudan eski değişkenin üzerine yazmak eski bloğu sahipsiz bırakır, bu yüzden dönüş önce ayrı bir değişkende denetlenir. malloc(0)vecallocile boyutu sıfır olan bir istek uygulamaya bağlıdır ve bu dersin ölçümünde basılmaz.
Sonraki Adım
Bu ders sahipliğin nasıl başladığını ölçtü: bir blok ayrılır ayrılmaz, onu bırakma
yükümlülüğü çağırana geçer. Ama bu yükümlülüğün nerede yazılı olduğu henüz sorulmadı —
say_ayir ve say_birak’ın kendisi hiçbir yerde “bunu sen bırakacaksın” demiyor, bu bilgi
yalnızca işlevin adından ve alışkanlıktan çıkarılıyor. Sıradaki ders bu yükümlülüğü bir
başlık dosyasına yazılı hâle getiriyor ve sahipliğin çağırandan çağrılana geçtiği ilk
düzeneği gösteriyor: aynı metni döndüren iki işlevden birinin sahibi bırakan taraf, ötekinin
sahibi çağıran olacak.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.