C DİLİNDE İŞARETÇİ (POINTER) KAVRAMI — Volume 3

Mesut Topuzlu
5 min readDec 19, 2020

--

Merhaba sevgili okurlar. Bu yazımızda “C dilinde işaretçi kavramı” na devam edeceğiz. Hatırlarsanız ilk iki yazımızda biraz teorik gitmiş ve işaretçilerin bildirimlerinden, değişken adreslerini bellekte nasıl tuttuğundan, gösterdiği adresin genişliğinden ve adresin size_t gibi özel bir değişkene nasıl aktarılabileceğinden vb. bahsetmiş idik. Şimdi biraz uygulamaya yönelik olarak işaretçilerin nerede ve nasıl kullanılacaklarını anlatmaya çalışacağım.

Fonksiyonlarda işaretçi parametre kullanımı;

Şuna emimin ki işaretçiler en çok karşımıza fonksiyon parametresi olarak çıkar. Konuya bodoslama girmeden önce birazcık C dilinde fonksiyonlardan bahsedeyim. Fonksiyon bilindiği üzere belirli bir amaca hizmet etmek üzere bir araya getirilmiş kodlar bütünüdür diyebiliriz. Fonksiyonların da tıpkı değişkenler gibi bellekte bir adresleri vardır. Şimdi çok ama çok basit bir fonksiyon düşünelim ve main içerisinden çağıralım;

#include <stdio.h>

void degistir(int i)
{
i = 20;
printf("i'nin degeri = %d\n",i);
}

int main(void)
{
int i = 10;
degistir(i);
printf("i'nin degeri = %d\n",i);
}

Uygulamayı derleyip çalıştırdığımızda konsol çıktısı aşağıdaki gibi olur;

Peki ama neden? Biz fonksiyon içerisinde i’nin değerini 10’dan 20 yaptık, printf ile yazdırdık gayet normal bir şekilde 20 yazdı ve fonksiyonun dışında i’nin değerini tekrar yazdırdık, eski değeri olan 10’u yazdı…. mı acaba? Hayır…Aslında olay tam olarak şöyle cereyan etti;

  1. Fonksiyona i değişkeni parametre olarak geçirildiğinde i’nin belleğin bir bölgesinde (stack) kopyası oluşturuldu,
  2. Kopya değişkene 20 değeri aktarıldı,
  3. Kopya değişken ekrana yazdırıldı sonuç 20,
  4. Fonksiyon yaşam mahallinin (scope) dışına çıkıldı ve kopya nesne artık yok,
  5. i’nin değeri yazdırıldı.. sonuç 10

C dilinin en önemli kurallarından biri; eğer bir fonksiyona bir değişken değer tipi şeklinde geçirilirse bu değişkenin kopyası oluşturulur. Yani fonksiyon içerisindeki i‘nin farklı skoptaki i ile zerre kadar alakası kalmaz. Sadece klonlanmış olur ve değişikliler lokal i nesnesi üzerinde yapılır. Bu işleme değer ile geçirme (by pass value) denir. Şimdi sizin aklınızda bir soru oluşmuştur muhtemelen; ya fonksiyon içerisinden değiştirilebilmesini isteseydim? İşte o zaman bizim malum işaretçiler devreye giriyor. Kodları aşağıdaki gibi değiştirelim ve tekrar derleyip çalıştıralım;

#include <stdio.h>

void degistir(int* p)
{
*p = 20; //adresteki değişkene 20 aktarılıyor
printf("i'nin degeri = %d\n",*p);
}

int main(void)
{
int i = 10;
degistir(&i); //değişkenin adresi fonksiyona geçiriliyor
printf("i'nin degeri = %d\n",i);
}

Konsol çıktısını incelersek sonucun öncekinden farklı olduğunu görürüz. Çünkü olaylar şöyle gerçekleşti;

  1. i tanımla ve 10 aktar,
  2. i değişkeninin bellekteki adresini fonksiyona ver,
  3. Fonksiyon içerisinde bellek adresi yani işaretçi kullanarak aktarma işlemini yap,
  4. Adresteki veriyi ekrana yazdır
  5. Fonksiyon dışına çıkıldığında i’yi tekrar yazdır.

Görüldüğü üzere fonksiyon kendisine verilen i’nin bellek adresi üzerinden işlem yapmaktadır. Bu neticede i’nin başlangıçtaki orjinal değeri olan 10 yerini 20'ye bırakmıştır.

Kafa karışıklığına sebep olacak kodlamadan kaçınmak gerek… ben burada fonksiyon parametresi işaretçi olacağından p ile belirttim. Yani şunu düşünmeyin ben fonksiyon parametresi olarak int* p yazdığımdan böyle oldu… hayır… int* i de yazabilirdim. Yani fonksiyonun işlem yaptığı parametre ismiyle bizim değişken olarak tanımladığımız i arasında bir ilişki yoktur. Yine de beyin bazen off durumuna geçiyor ve bu ikisini birbirine karıştırmaya başlıyor. Neyse… Bu tip parametre kullanımına referans (ya da adres) ile geçirme (by pass reference) denmektedir ve karşımıza çok çıkar. Toparlarsak fonksiyonlara parametre iki şekilde verilir (yada tabir yerindeyse geçirilir);
1. değer olarak (by pass value)
2. referans olarak (by pass reference)

Peki aklımıza şöyle bir soru gelmesi gerekir; Ben neden fonksiyona parametre olarak verdiğim değişken üzerinde değişiklik yapılmasını isteyeyim? Cevabı şöyle açıklayayım; bir fonksiyonun birden fazla değeri döndürmesi gereken durumlar ile karşılaşılabilir. Örneğin bir bölme fonksiyonumuz var ve biz hem bölüm sonucunu hem de kalanı görmek istiyoruz. Fakat C de (gerçi birçok dilde) fonksiyonlar sadece tek bir değer döndürebilir. Peki ne yapmalıyım? Ben hem bölümü hem de kalanı görmek istiyorum… Hesapla fonksiyonunu aşağıdaki gibi yazarsak işimizi görür mü?

#include <stdio.h>

int hesapla(int bolunecek, int bolen, int* bolum_P)
{
int kalan = bolunecek % bolen;
*bolum_P = (bolunecek - kalan)/ bolen;
return kalan;
}

int main(void)
{
int kalan, bolum;
kalan = hesapla(10, 3, &bolum);

printf("bolum : %d\n",bolum);
printf("kalan : %d\n",kalan);
}

Yukarıdaki hesapla fonksiyonunun şunu yapmasını istedim; parametre olarak girilen bölünecek sayıya ve bölene göre sonucu hesapla, sonucu yani bölümü sana adresi verilen değişkene aktar ve kalanı bana geri döndür. Bunu işaretçi kullanmadan yapabilmemizin bu örnekte imkanı yoktu. Yani bölüm değişkeninin adresini fonksiyona referans olarak vermeseydik değişkenin bölme işlemi sonucundan pekte haberi olmayacaktı.

Ayrıca fonksiyonu farklı varyasyonlarda da yazabiliriz. Mesela kullanıcının böleni sıfır girdiğinde işlemin geçersiz olduğunu bildirebilirdik ve kalan sonucunu da tıpkı bölüm gibi işaretçi parametre yapabilirdik, işlem geçerli ise bize 1 (TRUE), geçersiz ise 0 (FALSE) döndürmesini isteyebilirdik;

int hesapla(int bolunecek, int bolen, int* bolum_P, int* kalan_P)
{
…..
}

gibi gibi…

Gel gelelim parametreyi işaretçi olarak tanımlamanın diğer güzelliğine. Biliyoruz ki biz fonksiyona parametreyi değer olarak geçirdiğimizde kopyasını alıyor idi. Peki ben fonksiyona böyle 4-byte genişliğinde int değilde 50-byte genişliğinde bir struct geçirsem? Bu yapının kopyasının alındığını düşünün. İşlemciye ne kadar vakit kaybı değil mi 50-byte’lık bir kopyalama? Fakat ben bu struct değişkenini referans ile fonksiyona geçirdiğimde böyle bir kopyalama işlemine ihtiyaç duyulmayacak çünkü fonksiyon değişkenin adresi üzerinden (yani direkt olarak değişkenin kendisiyle) işlem yapacaktır. Şunun gibi;

#include <stdio.h>typedef struct{
int i;
float f;
char c;
}MyStruct;

void goster(MyStruct * st)
{
printf("i : %d\n",st->i);
printf("f : %f\n",st->f);
printf("c : %c\n",st->c);
}

void main()
{
MyStruct stc;
stc.i=20;
stc.f=35.5;
stc.c='Z';
goster(&stc);
}

İlk bakışta bizim için bir fark yok gibi fakat CPU’yu ekstra yükten kurtardık (işaretçinin struct üyelerine -> operatörü ile eriştiğine dikkat edin). Fakat burada sanki biraz eksiklik var. Düşünün ki bu fonksiyon kütüphanesini oluşturan geliştirici GK, kütüphanedeki fonksiyonu kullanarak uygulama geliştiren de GU olsun. GU’nun şöyle bir korkusu var; ya fonksiyon içerisinde benim nesnem değişikliğe uğratılırsa? GK’nın cevabı ise şöyle oluyor “ben senin nesneni değiştirmeyeceğime garanti veririm”. GK fonksiyonu o halde şöyle tanımlıyor;

void goster(const MyStruct_t * st);

const anahtar kelimesiyle uygulama geliştirene adresiyle de olsa fonksiyona verilen parametrenin içeride bilerek veya kazayla değişime uğrama ihtimali yok. Yani siz şunu yapamazsınız fonksiyon içinde ;

st->C = 'A'; //bu satırda derleyici read-only olduğuna dair hata verecektir

Zaten C’nin standart veya string kütüphane fonksiyonlarını incelerseniz (strcpy vb.) orada da bazı işaretçi parametrelerin const olarak tanımlandığını görürsünüz. Bunlar sadece bilgi aktarımı, const olmayanlar ise muhtemelen sonuç parametreleridir. Bunu veri tabanı ile uğraştıysanız eğer stored procedure (saklı yordam) tanımlarken karşılaşmıssınızdır. Prosedüre dış dünyadan bilgi getiren ve içeriği değişmeyecek parametre in, değişecek yani dış dünyaya sonuç verecek olan out parametre olarak tanımlanır. Buradaki mantık da tamamen aynı.

Sonuç;

Demek ki neymiş fonksiyonlarda işaretçi parametre kullanımının iki amacı varmış; birincisi değişkene adresi üzerinden erişerek değişiklikleri yansıtma (daha Türkçesi return yetmediği durumlarda parametreyi out olarak kullanma), ikincisi parametre kopyalama işinden CPU’yu kurtararak performans artışı sağlama.

Bu yazımı burada tamamlıyorum. Bir şeylerin kafaya iyice oturması için yazıların kısa ve öz olması daha iyi diye düşünüyorum. Hepsini tek yazıda anlatmaya kalksam ben yazarken, siz okurken bunalırsınız :) Bir sonraki yazımızda görüşmek üzere.

Sağlıklı günler.

--

--