---
title: '`void` ve Opak İşaretçiler'
source: 'https://academia.sh/tr/kurslar/c-bellek/void-ve-opak-isaretciler'
course: "C'de Bellek ve İşaretçiler"
language: tr
updated: '2026-09-26T03:02:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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'
---

# `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ı.

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

```c
// 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 `&sayi`yi, sonra `&kesirli`yi
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.

```c
// 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(&degerler[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.

```c
// 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
```

```c
// 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;
}
```

```c
// 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
```

```c
// 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;
}
```

```c
// 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.

```c
// 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çin
  `EtiketliDeger` ö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.c` bu 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_ad` gibi) bırakılmaz. Bu, başarısızlık
  yolunda da geçerli: `kayit_olustur` başarısız olduğunda kendi ayırdığı bloğu kendisi
  bırakıyor, çağıran tarafın `kayit_yok_et(NULL)` çağrısı yalnız güvenli bir hiçlik.
- Bir ödünç (`const char *` dönen `kayit_ad`) ile bir kopya (`char *` dönen
  `kayit_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.
