İçeriğe geç
academia.sh

Ders 04 / 15

Boş ve Sarkan İşaretçiler

Boş işaretçinin denetlenebilir bir durum olduğu, sarkan bir işaretçinin ise bırakılmış bir bloğu göstermeye devam ettiği ve bu yüzden hiç koşturulmadığı; ikisinin de sahipsiz sınıfının iki farklı yüzü olduğu.

Yayın Güncelleme

Bu derste

  • Boş bir işaretçiyi dolaylamadan önce nasıl denetleyeceğini örnekle gösterir
  • Sarkan bir işaretçinin bırakılmış bir bloğu gösterdiğini ve bu durumun neden hiç koşturulmadığını açıklar
  • NULL atamanın bir işaretçiyi denetlenebilir kılarak sahipsiz sınıfından nasıl çıkardığını ölçer
İçindekiler

Önceki dersin son satırı bir bırakma çağrısından hemen sonra işaretçiye NULL atadı ve bunu “bir sonraki dersin konusu” diye bıraktı. Bu ders o atamanın neden zorunlu bir alışkanlık olduğunu ölçüyor: bırakılmış bir bloğu göstermeye devam eden bir işaretçi sarkan işaretçi olur ve bu durum çekirdeğin sahipsiz sınıfına giren iki gözlemden biridir — kimse bu işaretçiyi geçerli tutmakla yükümlü değildir, ama kimse de onu NULL’a çekmeyi garanti etmez.

Boş İşaretçi: Denetlenebilir Bir Durum

// bos_isaretci.c — bos isaretci, dolaylamadan once denetlenerek guvenli hale gelir
#include <stdio.h>
#include <stdlib.h>

static int *belki_ayir(int basarili) {
    if (!basarili) {
        return NULL;
    }
    int *p = malloc(sizeof *p);
    if (p != NULL) {
        *p = 9;
    }
    return p;
}

int main(void) {
    int *basarisiz = belki_ayir(0);
    printf("%s %d\n", "basarisiz cagri bos mu", (int)(basarisiz == NULL));

    int *basarili = belki_ayir(1);
    if (basarili != NULL) {
        printf("%s %d\n", "basarili cagridan gelen deger", *basarili);
    } else {
        printf("%s\n", "basarili cagri beklenmedik sekilde bos");
    }

    free(basarili);
    return 0;
}
basarisiz cagri bos mu 1
basarili cagridan gelen deger 9

IS17. belki_ayir(0) çağrısı NULL döndürüyor ve basarisiz == NULL karşılaştırması 1 veriyor — bu, boş işaretçinin beklenen ve denetlenebilir bir durum olduğunu gösteriyor. belki_ayir(1) çağrısı gerçek bir blok döndürüyor; ama bu blok da kullanılmadan önce basarili != NULL ile denetleniyor, çünkü malloc her koşulda başarılı olacağının garantisini vermiyor. Boş bir işaretçiyi dolaylamadan önce denetlemek, bu ölçümün tek kuralı: *basarisiz hiç yazılmadı, çünkü basarisiz’in boş olduğu zaten biliniyordu.

Sarkan İşaretçi: Bırakılmış Bir Bloğu Göstermeye Devam Etmek

Bir işaretçi free ile bırakıldıktan sonra, o işaretçinin taşıdığı değer değişmiyor — hâlâ aynı bloğu gösteriyor, ama artık o blok geçerli değil. Bu duruma sarkan işaretçi deniyor. Sarkan bir işaretçiyi dolaylamak (*p ile okumak ya da yazmak) ya da onu tekrar free’ye vermek (çift bırakma) tanımsız davranıştır: sonuç çoğu zaman sessizce yanlış bir değer, bazen bir çökme, hiçbir zaman güvenilir bir davranıştır. Bu yüzden bu ders hiçbir sarkan işaretçiyi dolaylamıyor ya da tekrar bırakmıyor — yalnız bunun neden tehlikeli olduğunu betimliyor ve tanımlı karşılığını çalıştırıyor.

// tanimli_karsilik.c — birakma sonrasi NULL atamasi, sarkan durumu denetlenebilir kilar
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof *p);
    if (p == NULL) {
        printf("%s\n", "ayirma basarisiz");
        return 1;
    }
    *p = 21;
    printf("%s %d\n", "birakmadan once deger", *p);

    free(p);
    p = NULL;
    printf("%s %d\n", "birakmadan sonra bos mu", (int)(p == NULL));

    if (p != NULL) {
        printf("%s %d\n", "hic ulasilmayan dal", *p);
    } else {
        printf("%s\n", "dolaylama denetim sayesinde atlandi");
    }
    return 0;
}
birakmadan once deger 21
birakmadan sonra bos mu 1
dolaylama denetim sayesinde atlandi

IS18. free(p) çağrısından hemen sonra p = NULL atanıyor; bu iki satır arasına hiçbir başka işlem girmiyor. p == NULL karşılaştırması 1 veriyor ve son if bloğunda p != NULL yanlış olduğu için *p satırına hiç ulaşılmıyor — “dolaylama denetim sayesinde atlandi” satırı bunun kanıtı. Eğer p = NULL ataması atlanmış olsaydı, p bırakılmış bloğu göstermeye devam eden bir sarkan işaretçi olurdu ve p != NULL denetimi onu yakalayamazdı — çünkü sarkan bir işaretçinin değeri boş değildir, yalnızca geçersizdir. Bu fark, boş işaretçiyle sarkan işaretçi arasındaki temel ayrımdır: biri denetlenebilir bir değer (boş), diğeri denetlenemeyen bir durumdur (geçersiz ama boş görünmeyen).

Sarkan bir işaretçinin ikinci bir tehlikesi, onu tekrar free’ye vermektir — bu duruma çift bırakma deniyor. free(p) çağrısından sonra, eğer p hâlâ eski adresi taşıyorsa, ikinci bir free(p) çağrısı aynı bloğu iki kez bırakmaya çalışır; bu da tanımsız davranıştır ve genellikle ayırıcının kendi iç durumunu bozar. Yukarıdaki örnekte bu ikinci çağrı hiç yazılmadı, çünkü p = NULL atamasından sonra free(NULL) çağırmak zararsızdır — standart, boş bir işaretçiyi bırakmayı hiçbir işlem yapmama olarak tanımlıyor. Bu, NULL atamasının ikinci bir yararı: yalnız dolaylamayı değil, olası bir ikinci bırakma çağrısını da zararsız hale getiriyor.

// guvenli_ikinci_cagri.c — free(NULL) standart tarafindan zararsiz tanimlanir
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof *p);
    if (p == NULL) {
        printf("%s\n", "ayirma basarisiz");
        return 1;
    }
    *p = 4;
    printf("%s %d\n", "birakmadan once deger", *p);

    free(p);
    p = NULL;

    free(p);
    printf("%s\n", "ikinci free NULL uzerinde calisti");
    return 0;
}
birakmadan once deger 4
ikinci free NULL uzerinde calisti

IS19. Bu kod free(p)’yi iki kez çağırıyor, ama aralarına p = NULL satırı giriyor. İlk çağrı gerçek bloğu bırakıyor; ikinci çağrı NULL üzerinde çalışıyor ve standart bunu açıkça “hiçbir işlem yapma” olarak tanımladığı için program çökmeden “ikinci free NULL uzerinde calisti” satırına ulaşıyor. Eğer ikinci satırdaki p = NULL ataması olmasaydı, bu aynı kod ikinci free(p) çağrısında aynı bloğu iki kez bırakmaya çalışırdı — bu da yukarıda tanımlanan çift bırakma hatası olurdu ve bu yüzden burada denenmiyor.

Bu derste sahipsiz sınıfının iki üyesi de aynı kökten geliyor: birinci dersteki çekirdekte uzerine yazilan isaretci gözlemi, bir bloğun adresini tutan tek işaretçinin bırakılmadan üzerine yazılmasıyla o bloğu erişilemez bırakmıştı — bu bir sızıntı. Sarkan işaretçi ise tersi yönde bir hatadır: blok bırakılmıştır ama işaretçi hâlâ ona işaret etmeye devam eder. İkisi de “kimse bu durumu garanti altına almadı” anlamına geliyor, bu yüzden ikisi de sahipsiz sınıfında.

Birden Çok Kopya İşaretçi: NULL Ataması Neden Yetmez

Şimdiye kadarki iki örnek de tek bir işaretçi üzerinden ilerledi. Gerçek kodda aynı bloğu birden çok işaretçi gösterebilir — biri bırakma çağrısını yapar, ama diğerleri p = NULL satırından habersiz kalır.

// kopya_isaretci.c — ayni blogu gosteren iki isaretciden yalniz biri NULL'a cekilir
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof *p);
    if (p == NULL) {
        printf("%s\n", "ayirma basarisiz");
        return 1;
    }
    *p = 7;

    int *q = p;
    printf("%s %d\n", "p ve q ayni degeri gosteriyor mu", (int)(*p == *q));
    printf("%s %d\n", "p ve q ayni adresi mi tasiyor", (int)(p == q));

    free(p);
    p = NULL;
    printf("%s %d\n", "p birakma sonrasi bos mu", (int)(p == NULL));
    return 0;
}
p ve q ayni degeri gosteriyor mu 1
p ve q ayni adresi mi tasiyor 1
p birakma sonrasi bos mu 1

IS20. int *q = p; satırından sonra p ve q aynı adresi taşıyor — hem *p == *q hem p == q karşılaştırması 1 veriyor. free(p) ve ardından p = NULL çalıştığında yalnız p değişkeninin kendisi etkileniyor; q bu iki satırda hiç geçmiyor, dolayısıyla hâlâ free ile bırakılmış bloğun eski adresini taşıyor — q şimdi bir sarkan işaretçi. Bu kod q’yu bu noktadan sonra ne dolaylıyor ne de NULL ile karşılaştırıyor, çünkü q’nun taşıdığı değer artık geçersiz ve onu okumaya çalışmak (bir karşılaştırma dahil) tanımsız davranıştır. Gösterilmek istenen şey kod çalışırken görülmüyor — tam olarak da bu, sınırın kendisi: p = NULL yalnızca p adlı değişkenin durumunu değiştirir, q’nun durumunu değil.

Sahiplik Ekseninde Sarkan İşaretçi: Üç Farklı Kaynak

Bu derse kadarki iki örnek de sarkan durumu tek bir kaynaktan üretti: bir malloc bloğunun free ile bırakılması. Ama sarkan işaretçi, kursun dört sahiplik sınıfının (dil, çağıran, çağrılan, sahipsiz) her birinde farklı bir yoldan ortaya çıkabilir; kod burada çalıştırılmıyor çünkü her biri tanımsız davranışa yol açar, ama üçü de bu dersin ölçtüğü aynı köke bağlanıyor.

dil sınıfında, bir işlev yerel bir değişkenin adresini döndürürse (return &yerel; gibi), döndürülen işaretçi işlev geri döndüğü anda sarkan olur — çünkü yerel’in ömrünü yöneten dilin kendisidir ve bu ömür işlevin gövdesiyle sınırlıdır. Burada hiçbir free çağrısı yok; blok bir malloc’la değil, çağrı çerçevesinin kendisiyle ortadan kalkıyor. Sonuç yine de aynı: çağıran taraf geçersiz bir adresi gösteren bir işaretçiyle baş başa kalıyor.

çağıran sınıfında sarkan durum, bu dersin iki örneğinde de görülen biçimdir: belleği ayıran ve free çağırma yükümlülüğü taşıyan taraf, bırakma sonrası NULL atamayı unutursa (ya da kopya_isaretci.c’deki gibi ikinci bir kopya taşıyorsa), o işaretçi sarkan kalır.

çağrılan sınıfında ise sarkan durum ters yönden gelir: bir işlev kendi tuttuğu bir bloğu (sahip.c gibi bir çekirdeğin sahiplendiği veriyi düşün) kendi içinde free ile bırakırsa, ama çağırana bu bloğa daha önce verdiği bir işaretçiyi geri çekmezse, çağıranın elindeki kopya artık sarkan bir işaretçidir — çağıran bunu bilmez, çünkü çağrılan bunu ona hiçbir şekilde bildirmedi. Bu yüzden çağrılan sınıfındaki işlevlerin sözleşmesi açık olmalı: bir bloğu ne zaman bıraktığını ve bu bırakmanın çağırandaki kopyaları geçersiz kıldığını belgelemek çağrılan tarafın sorumluluğudur.

Üçü de aynı gözleme varıyor: sarkan işaretçi, bir işaretçinin taşıdığı adresin hâlâ geçerli bir bloğu gösterip göstermediğiyle ilgili bir sorudur ve bu soru hangi sınıftan geldiğine bakılmaksızın dilin kendisi tarafından yanıtlanmaz — yanıt her zaman programcının izlemesine kalır. Üç kaynak arasındaki fark, sorunun ne zaman fark edilebilir olduğudur: dil sınıfındaki hata genellikle işlev döner dönmez, ilk kullanımda ortaya çıkar; çağıran sınıfındaki hata bu dersteki gibi bir NULL atamasının unutulmasıyla gecikebilir; çağrılan sınıfındaki hata ise en geç fark edilenidir, çünkü çağıran tarafın kendi kodunda hiçbir şey değişmemiştir — yalnız çağırdığı işlevin iç davranışı değişmiştir.

Rust’ın Ödünç Denetleyicisiyle Farkı

C’de bırakılmış belleği kimin gözettiği sorusu, Sahiplik ve Bellek Modeli kursunda (M10/K02) ele alınan Rust’ın ödünç denetleyicisinden farklı bir yerde duruyor: Rust’ta bir değerin ömrü derleme zamanında belirlenir ve ömrü biten bir referans derleyici tarafından reddedilir, C’de ise p = NULL gibi bir alışkanlık dışında hiçbir mekanizma bir işaretçinin hâlâ geçerli bir bloğu gösterip göstermediğini denetlemez. Rust’ta yukarıdaki kopya_isaretci.c örneğinin bir benzeri derlenmezdi — q’ya p’nin ödünç verildiği an, ödünç denetleyici p‘nin serbest bırakılmasını q hâlâ yaşarken engellerdi. C’de bu denetim yok; q’nun sarkan olduğunu bilmek tamamen programcının derste burada yapılan türden bir izlemesine kalır.

Sarkan İşaretçiyi Derleyici Değil, Araç Yakalar

Bu derste kullanılan tüm denetimler (== NULL, != NULL) elle yazılan, programcının kendi eklediği kontrollerdir — derleyicinin kendisi bir işaretçinin sarkan olup olmadığını bilmez ve bunu asla otomatik olarak işaretlemez. Gerçek projelerde bu boşluk çalışma zamanı araçlarıyla kapatılır: dinamik çözümleyiciler bir programı çalıştırırken her malloc/free çiftini izler ve bir blok bırakıldıktan sonra o bloğa erişilirse (dolaylama, çift bırakma) bunu raporlar. Bu araçlar derleme zamanında değil, çalışma zamanında ve yalnız o koşumda gerçekten tetiklenen yollar için çalışır — Rust’ın derleme zamanı ödünç denetleyicisinin tersine, bir sarkan işaretçi hatası yakalanmadan önce en az bir kez gerçekten koşmuş olmalıdır. Bu, C’deki savunmanın doğasını özetliyor: dil kendisi hiçbir garanti vermiyor, ama disiplin (bu derste görülen NULL atama alışkanlığı) ve araçlar (çalışma zamanı denetleyicileri) birlikte bu garantinin eksikliğini telafi ediyor.

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

p = NULL ataması, sarkan işaretçi sorununu ortadan kaldırmıyor — yalnız o tek işaretçi için görünür kılıyor. Yukarıdaki kopya_isaretci.c bunu somut olarak gösterdi: q, p’yle aynı adresi taşımasına rağmen p = NULL satırından hiç etkilenmedi. Bu ders tek işaretçili ve iki işaretçili durumu ayrı ayrı ölçtü; üç ya da daha fazla işaretçinin aynı bloğu paylaştığı, ya da bir işaretçinin bir yapı alanında saklandığı daha karmaşık paylaşım örüntüleri, kursun Dinamik Bellek konusundaki Serbest Bırakma Disiplini dersinin konusu. Oradaki temel soru aynı kalıyor: bir bloğu bırakma yetkisine kim sahip ve bırakıldığını diğer kopyalara kim bildirecek.

Özet

  • Boş işaretçi (NULL) denetlenebilir bir değerdir; `dolaylamadan önce == NULL ile sınanabilir ve bu ders bunu her ayırma çağrısından sonra uyguladı.
  • Sarkan işaretçi, bırakılmış bir bloğu göstermeye devam eden bir işaretçidir; bu ders onu hiç dolaylamadı ya da tekrar bırakmadı, yalnız betimledi.
  • free çağrısından hemen sonra işaretçiye NULL atamak, sarkan durumu denetlenebilir bir boş duruma dönüştürüyor.
  • Boş ve sarkan işaretçi, çekirdeğin sahipsiz sınıfının iki farklı yüzüdür: biri bırakılmamış bir sızıntı, diğeri bırakılmış ama hâlâ gösterilen bir blok. p = NULL yalnız o tek işaretçi için geçerlidir; kopya_isaretci.c’nin gösterdiği gibi aynı bloğu gösteren başka bir kopya varsa o kopya hâlâ sarkandır.
  • Sarkan durum yalnız çağıran’ın unuttuğu bir NULL atamasından değil, dil sınıfında bir yerel değişkenin adresinin döndürülmesinden ya da çağrılan’ın kendi bıraktığı bir bloğu çağırana bildirmemesinden de doğabilir — üçü de aynı soruya (adres hâlâ geçerli mi) farklı bir yoldan varır.
  • Çift bırakma (free’yi aynı bloğa iki kez çağırmak) da sarkan işaretçinin bir sonucudur; free(NULL)’ın standart tarafından zararsız tanımlanması, p = NULL alışkanlığını bu ikinci tehlikeye karşı da koruyucu kılar — guvenli_ikinci_cagri.c bunu somut olarak gösterdi.

Sonraki Adım

Bu derse kadarki dört ders, işaretçilerin int *, double * gibi somut tiplerini kullandı — tip her zaman gösterilen değerin ne olduğunu söylüyordu. Bir sonraki ders, void ve Opak İşaretçiler, bir işaretçinin tipinin bilinçli olarak gizlendiği durumları ölçüyor: çekirdekteki opak kayit gözlemi, Kayit yapısının içeriğini tamamen saklarken sahipliği nasıl çağrılan sınıfına taşıdığını gösteriyor. Bu derste görülen sarkan işaretçi tehlikesi orada da geçerli kalıyor — bir opak kaydın adresini elinde tutan çağıran, o kaydı bırakma yükümlülüğü çağrılan’a ait olduğu için, kaydın ne zaman geçersiz olduğunu ancak çağrılan’ın sözleşmesinden öğrenebilir; kaydın içeriği gizli olduğu için bu sözleşme, tip tanımının kendisinden değil, yalnız dokümantasyondan gelir.

İ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