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çiyeNULLatamak, 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 = NULLyalnı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 birNULLatamasından değil,dilsı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 = NULLalışkanlığını bu ikinci tehlikeye karşı da koruyucu kılar —guvenli_ikinci_cagri.cbunu 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.