İçeriğe geç
academia.sh

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, calloc ve realloc dönen bloğun sahibi her zaman çağırandır; bu dersin ölçtüğü bellek sınıfı çağırandır.
  • malloc içeriği sıfırlamaz, calloc sıfırlar — bu fark ölçümde değerler doğrudan okunarak gösterildi, tahmin edilmedi.
  • realloc yeni bir sahip doğurmaz; içerik korunur, sayaç yalnız ilk ayırmada artar.
  • Başarısız bir realloc eski 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) ve calloc ile 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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat