Ders 05 / 15
`void` ve Opak İşaretçiler
Bir void işaretçinin gösterdiği tipin bilinçli olarak gizlenmesi ve opak bir tipin içeriğini tamamen saklayarak sahipliği çağrılan modülde tutması.
Yayın Güncelleme
Bu derste
- Bir void işaretçinin tip bilgisini nasıl taşıdığını ve nasıl geri kazandığını ölçer
- Opak bir tipin içeriğini gizleyerek sahipliği çağrılan modülde nasıl tuttuğunu açıklar
- void işaretçinin dolaylanamayacağını ve önce somut bir tipe dönüştürülmesi gerektiğini gösterir
İçindekiler
Buraya kadarki dört ders, bir işaretçinin gösterdiği değerin tipini hep açıkça biliyordu:
int *, double *. Bu ders, tipin bilinçli olarak gizlendiği iki durumu ölçüyor:
void * — herhangi bir nesne tipini gösterebilen ama kendisi dolaylanamayan bir işaretçi
— ve opak tip — içeriği tanımlandığı birimin dışına hiç sızmayan bir yapı. İkisi de
çekirdekteki sahip.h/sahip.c biriminin temelini oluşturuyor; sınıf burada çağrılan:
belleği tutan modül, çağırana yalnız ödünç veriyor.
void *: Tipi Taşıyan Ama Dolaylanamayan İşaretçi
// void_isaretci.c — void isaretci herhangi bir tipi tutar ama once somut tipe donusmelidir
#include <stdio.h>
static void yazdir_int(const void *veri) {
const int *p = veri;
printf("%s %d\n", "int olarak okunan deger", *p);
}
static void yazdir_double(const void *veri) {
const double *p = veri;
printf("%s %.1f\n", "double olarak okunan deger", *p);
}
int main(void) {
int sayi = 17;
double kesirli = 3.5;
void *genel = &sayi;
printf("%s %d\n", "genel isaretci sayinin adresine esit mi", (int)(genel == &sayi));
yazdir_int(genel);
genel = &kesirli;
yazdir_double(genel);
return 0;
}
genel isaretci sayinin adresine esit mi 1 int olarak okunan deger 17 double olarak okunan deger 3.5
IS21. genel bir void * değişkeni ve önce &sayiyi, sonra &kesirliyi
gösterebiliyor — aynı değişken, iki farklı tipteki nesnenin adresini taşıyabiliyor.
genel == &sayi karşılaştırması 1 veriyor; bu, void *’ın da diğer işaretçiler gibi
eşitlikle ölçülebildiğini gösteriyor. Ama genel’in kendisi hiçbir yerde *genel olarak
dolaylanmıyor — yazdir_int ve yazdir_double işlevleri önce onu kendi somut tiplerine
(const int *, const double *) atıyor, ancak ondan sonra dolaylıyorlar. void *’ın
kuralı budur: herhangi bir nesne işaretçisini tutabilir, ama gösterdiği değeri okumak
için önce bir tipe dönüştürülmesi gerekir.
void * Tipi Taşımaz: Dışarıdan Bir Etiket Gerekir
Önceki bölümdeki genel değişkeni her seferinde hangi tipi gösterdiğini kod okuyan
insana açıkça belliydi — önce &sayi, sonra &kesirli. Ama çalışma zamanında void *
kendisiyle birlikte hiçbir tip bilgisi taşımaz; bir void * değişkenine bakarak onun
int * mi double * mi olduğunu söylemenin bir yolu yok. Programlar bu boşluğu genellikle
ayrı bir etiket ile kapatır.
// etiketli_deger.c — void bir isaretci tip tasimadigi icin ayri bir etiket eslik eder
#include <stdio.h>
typedef enum { TIP_INT, TIP_DOUBLE } Tip;
typedef struct {
Tip tip;
void *veri;
} EtiketliDeger;
static void yazdir(const EtiketliDeger *e) {
if (e->tip == TIP_INT) {
const int *p = e->veri;
printf("%s %d\n", "int etiketli deger", *p);
} else {
const double *p = e->veri;
printf("%s %.1f\n", "double etiketli deger", *p);
}
}
int main(void) {
int sayi = 42;
double kesirli = 2.5;
EtiketliDeger degerler[2] = {
{ TIP_INT, &sayi },
{ TIP_DOUBLE, &kesirli }
};
for (int i = 0; i < 2; i++) {
yazdir(°erler[i]);
}
return 0;
}
int etiketli deger 42 double etiketli deger 2.5
IS22. EtiketliDeger yapısı void *veri’nin yanına bir de Tip tip alanı
taşıyor; yazdir işlevi doğru dönüşümü tip alanına bakarak seçiyor, veri’nin
kendisine bakarak değil — çünkü veri’nin kendisi bunu söyleyemez. degerler dizisinin
her elemanı farklı bir tipe işaret ediyor ama aynı EtiketliDeger tipinde tutulabiliyor;
bu, void *’ın neden çoğu zaman yalnız başına değil, bir etiketle birlikte kullanıldığını
gösteriyor. Etiket yanlış olsaydı (örneğin TIP_DOUBLE işaretlenmiş ama aslında int *
gösteren bir giriş), yazdir yanlış tipe dönüştürüp yanlış bir değer okurdu — bu tür bir
hata derleyici tarafından yakalanmaz, çünkü void * her iki durumda da aynı şekilde
derlenir.
Bu deseni C standart kütüphanesinde de görürüz: qsort ve bsearch karşılaştırma
işlevlerine const void * çiftleri geçirir, ama hangi tipi karşılaştırdığını hiçbir yerde
söylemez — bu bilgi çağıranın kafasında, karşılaştırma işlevinin kendi gövdesinde saklıdır.
EtiketliDeger bu örtük sözleşmeyi açık bir alana taşıyor: tip bilgisi artık işlevin
gövdesinde gömülü bir varsayım değil, verinin yanında duran, okunabilen bir değer. İki
yaklaşım da aynı riski taşıyor — etiket ya da varsayım yanlışsa sonuç tanımsız — ama
EtiketliDeger bu riski en azından görünür kılıyor, sizeof’un ya da parametre
sırasının kendiliğinden doğru olacağını varsaymak yerine.
Opak Tip: İçeriği Hiç Sızmayan Yapı
Bir önceki bölümde void * tipi gizledi ama boyutu gizlemedi — çağıran taraf hangi tipe
dönüştüreceğini biliyordu. Opak bir tip bir adım öteye gidiyor: yapının alanlarını
bile çağırana göstermiyor. Bu ders, çekirdeğin sahip.h/sahip.c birimini bu dersin
kendi dosyaları içinde yeniden kurarak bunu ölçüyor.
// 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_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_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;
}
// opak_kayit.c — Kayit yapisinin alanlari yalniz bu birimin disinda tanimli
#include <stdio.h>
#include "sahip.h"
#include "sayac.h"
int main(void) {
Kayit *k = kayit_olustur("envanter", 32);
if (k == NULL) {
printf("%s\n", "olusturma basarisiz");
return 1;
}
printf("%s %s\n", "kayit adi", kayit_ad(k));
char *kopya = kayit_ad_kopyasi(k);
printf("%s %s\n", "kopyalanan ad", kopya != NULL ? kopya : "yok");
say_birak(kopya);
kayit_yok_et(k);
printf("%s %d\n", "birakma sonrasi sayac farki", ayrilan_sayisi() - birakilan_sayisi());
return 0;
}
kayit adi envanter kopyalanan ad envanter birakma sonrasi sayac farki 0
IS23. opak_kayit.c dosyası struct Kayit’in hiçbir alanına doğrudan erişmiyor —
sahip.h yalnız Kayit * tipini ve dört işlevi bildiriyor, yapının içeriği sahip.c
biriminde kalıyor. kayit_ad(k) çağrısı yapının içindeki adı ödünç veriyor —
sahiplik.c’deki odunc verilen ad satırı bu çağrıyı kullanmıştı ve sayaç farkı 0
çıkmıştı, çünkü ödünç bir bellek ayırmıyor. kayit_yok_et(k) çağrısından sonra
ayrilan_sayisi() - birakilan_sayisi() 0 veriyor — kaydın hem kendisi hem içindeki ad
tamponu tam olarak bırakıldı. Sahiplik burada çağrılan sınıfına ait: Kayit’i tutan
birim (sahip.c), onu oluşturan da yok eden de kendisi, çağıran taraf yalnız kayit_ad
ile bir ödünç alıyor ve bu ödüncü bırakmıyor.
Aynı Sözleşme Birden Çok Örnekte
sahip.h’daki sözleşme tek bir Kayit örneği için değil, her örnek için aynı
şekilde geçerli olmalı — hem başarılı oluşturmalarda hem kayit_olustur’ün NULL
döndürdüğü başarısız çağrıda.
// coklu_kayit.c — ayni opak sozlesme birden fazla Kayit orneginde de gecerli kalir
#include <stdio.h>
#include "sahip.h"
#include "sayac.h"
int main(void) {
Kayit *birinci = kayit_olustur("depo", 32);
Kayit *ikinci = kayit_olustur("sevkiyat", 32);
Kayit *basarisiz = kayit_olustur("cokuzunbiradisimlimitiasandeger", 8);
printf("%s %d\n", "basarisiz olusturma bos mu", (int)(basarisiz == NULL));
if (birinci != NULL && ikinci != NULL) {
printf("%s %s\n", "birinci kayit adi", kayit_ad(birinci));
printf("%s %s\n", "ikinci kayit adi", kayit_ad(ikinci));
}
kayit_yok_et(birinci);
kayit_yok_et(ikinci);
kayit_yok_et(basarisiz);
printf("%s %d\n", "tum birakmalardan sonra sayac farki",
ayrilan_sayisi() - birakilan_sayisi());
return 0;
}
basarisiz olusturma bos mu 1 birinci kayit adi depo ikinci kayit adi sevkiyat tum birakmalardan sonra sayac farki 0
IS24. basarisiz, sinir (8) verilen addan (cokuzunbiradisimlimitiasandeger)
küçük olduğu için NULL dönüyor — basarisiz olusturma bos mu 1. birinci ve
ikinci bağımsız birer Kayit örneği; her biri kendi ad tamponunu ayırıyor ve
kayit_ad çağrıları birbirini etkilemiyor. kayit_yok_et(basarisiz) çağrısı NULL
üzerinde çalışıyor — sahip.c’deki kayit_yok_et bunu k == NULL denetimiyle zaten
karşılıyor, bu yüzden burada ayrı bir denetim yazmaya gerek yok. Üç çağrının (ikisi
başarılı, biri başarısız) hepsinden sonra ayrilan_sayisi() - birakilan_sayisi()
0 veriyor — başarısız çağrı kendi ayırdığı k bloğunu kayit_olustur’ün içinde
zaten bırakmıştı, bu yüzden dışarıdan gelen kayit_yok_et(NULL) çağrısının hiçbir şey
yapmaması sayaçları bozmuyor. Bu, çağrılan sınıfının bir başka yüzü: sözleşme yalnız
başarı yolunda değil, başarısızlık yolunda da kendi bıraktığı belleği kendisi
temizliyor.
Sınırlayıcı Ölçüm
void * tip bilgisini saklıyor ama gizlemiyor — çağıran taraf hangi somut tipe
dönüştüreceğini bilmek zorunda, aksi hâlde dönüştürme yanlış olur ve sonuç tanımsızdır.
Opak tip ise bunun ötesine geçiyor: çağıran taraf Kayit’in kaç alanı olduğunu, alanların
adlarını ya da boyutlarını hiç bilmiyor — yalnız sahip.h’ın bildirdiği işlevlerle
etkileşiyor. Bu, bir esneklik-güvenlik değişimidir: opak bir tip, iç yapısını değiştirmeyi
(yeni bir alan eklemeyi) dış arayüzü bozmadan mümkün kılar, ama her erişim bir işlev
çağrısı gerektirir — doğrudan alan erişimi yoktur.
Bu güvence, yalnız void * kullanan taraf disiplinli olduğu sürece geçerli. etiketli_deger.c’deki
yazdir, e->tip alanına bakıp doğru dönüştürmeyi seçiyor; ama derleyici bu eşleşmeyi
denetlemiyor — Tip alanını hiç okumadan bir TIP_DOUBLE değerini const int *’a
dönüştürüp dolaylasaydık, bellekteki bit örüntüsü double olarak yazılmışken int olarak
okunacaktı. Bu blok bilerek koşturulmuyor: sonucu tanımsızdır ve platforma, derleyiciye,
hatta optimizasyon düzeyine göre değişken. Kod hiçbir zaman çökmeyebilir de, sessizce yanlış
bir sayı basabilir de — tanımsız davranışın asıl tehlikesi budur, hatanın görünür bir sinyal vermeden
geçmesi. EtiketliDeger’in tip alanı bu riski ortadan kaldırmıyor, yalnızca görünür
kılıyor: yanlış dönüştürme hâlâ mümkün, ama artık if (e->tip == TIP_INT) gibi bir denetimle
önlenebilir bir hataya dönüşüyor. Ham bir void * parametre alan bir işlevde bu denetim
imkânsızdır, çünkü tip bilgisi hiçbir yerde tutulmuyor; etiketli_deger.c tipi işaretçinin
yanında taşıyarak bu boşluğu dolduruyor.
Opak tipte aynı risk farklı bir yerde duruyor. coklu_kayit.c’deki birinci ve ikinci
aynı Kayit * tipini taşısa da, ikisinin veri/ad alanları sahip.c’nin dosyasında
gizli — çağıran taraf birinci->ad yazmaya çalışamaz bile, çünkü Kayit yapısının
tanımı sahip.h’da yok, yalnız sahip.c içinde. Bu, void *’ın
tip-güvenliği sorununu tamamen farklı bir yoldan çözüyor: yanlış dönüştürme riskini
kaldırmak yerine, dönüştürmeye giden yolu derleme zamanında kapatıyor. İki yaklaşımın
ortak noktası, ikisinin de veriyi kim sahipleniyor sorusuna aynı yanıtı vermesi: sahip.c
içindeki kayit_olustur ve kayit_yok_et, birinci ve ikinci’nin ömrünü baştan
sona kendisi yönetiyor — çağıran taraf yalnız bir tutamak (Kayit *) alıyor, içeriğine asla
dokunmuyor. Bu yüzden dersin sahiplik sınıfı çağrılan: sahip.c hem ayırıyor hem
serbest bırakıyor, çağıran tarafın tek görevi elindeki tutamağı doğru işlevlere geçirmek.
Ama bu görev de bir yükümlülüktür ve opaklık onu gizlemez. Opak tip yapının içini gizler,
sahipliği gizlemez: kayit_olustur’un döndürdüğü tutamağı kayit_yok_et’e geri vermek
yine çağıranın işidir ve bu yükümlülük imzada değil, yalnız sahip.h’daki yorumda yazılıdır.
Çağıran bu çağrıyı unutursa derleyici hiçbir şey söylemez; iki blok da sahipsiz kalır.
Özet
void *herhangi bir nesne işaretçisini tutabilir ama kendisi dolaylanamaz; önce somut bir tipe (int *,const double *gibi) dönüştürülmesi gerekir.- Bir
void *değişkeni zaman içinde farklı tipteki nesnelerin adresini taşıyabilir; eşitlik karşılaştırması diğer işaretçiler gibi çalışır. Ama tipi kendisi taşımadığı içinEtiketliDegerörneğindeki gibi ayrı bir etiket olmadan hangi tipe dönüştürüleceği derleyici tarafından denetlenmez — yanlış dönüştürme tanımsız davranıştır ve koşturulmadan, yalnız anlatılarak gösterilir. - Opak bir tip (
Kayit) alanlarını tanımlandığı birimin dışına hiç sızdırmaz; çağıran yalnız bildirilen işlevlerle etkileşir.coklu_kayit.cbu sözleşmenin aynı program içinde birden çok bağımsız örnekte de geçerli kaldığını gösteriyor. - Opak bir tipi tutan birim onu hem oluşturur hem yok eder — sınıf çağrılan; çağıran
tarafa yalnız ödünç verilen değerler (
kayit_adgibi) bırakılmaz. Bu, başarısızlık yolunda da geçerli:kayit_olusturbaşarısız olduğunda kendi ayırdığı bloğu kendisi bırakıyor, çağıran tarafınkayit_yok_et(NULL)çağrısı yalnız güvenli bir hiçlik. - Bir ödünç (
const char *dönenkayit_ad) ile bir kopya (char *dönenkayit_ad_kopyasi) farklı sahiplik taşır — biri bırakılmaz, diğeri bırakılmalıdır.
Sonraki Adım
İşaretçiler konusunun son dersi, Fonksiyon İşaretçileri, veri işaretçilerinden ayrı bir türü ölçüyor:
bir işlevin kendisini gösteren ve başka bir işleve parametre olarak geçirilebilen
işaretçiler. Bu derste void *’ın tip bilgisini gizlemeden taşıması ile opak tipin onu
tamamen gizlemesi arasındaki fark ölçüldü; fonksiyon işaretçilerinde de benzer bir gerilim
var — bir geri çağrının imzası hangi bağlamı (context) alacağını söyler, ama bağlamın
içeriğini ya da sahipliğini garanti etmez. Bir geri çağrının hangi bağlamı taşıdığı ve bu
bağlamın sahipliğinin kime ait olduğu, bu topluluğun son sorusu.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.