"Enter"a basıp içeriğe geçin

C ve C++’ta İşaretçiler (Pointers)

Bir süredir C ve C++’ta bir bel kemiği konusu olan İşaretçiler (Pointer) konusunda araştırmalar yapıyordum. Gerek internetten gerek basılı kaynaklardan derlediğim bilgileri burada elimden geldiğince ve anladığımca paylaşmak istiyorum.

 

İşaretçiler birçokları için korkulu bir rüya. İşaretçiler konusu anlaşılmadan tam anlamıyla bir C programcısı olmak çok zor diyebilirim. İşaretçiler kapsam olarak da geniş bir konu ancak yazıyı elimden geldiğince kısa ve anlaşılır tutmaya çalıştım ve en basit ve temel konu olan değişken kavramından başlayarak aşama aşama ilerlemeye gayret gösterdim. Son olarak da fonksiyon işaretçileriyle yazıyı tamamladım.

E hadi o zaman başlayalım.

Değişmeyen Tek Şey Değişimin Kendisidir!

C’ye yeni başlayan veya işaretçileri anlamaya çalışan birisi için işaretçi kavramına girmeden önce değişken kavramının tam olarak anlaşılması gerekir.

Her değişkenin (adı üzerinde) değişebilen bir değeri ve bir de adı vardır. Bilgisayarımızın belleğindeki belli bir bellek bloğu bu değişkenin değerini tutmak için ayrılır. Bu bloğun boyutu değişkenin almasına izin verilen değer aralığına bağlı olarak değişir. Örneğin PC’lerde bir tamsayı (integer) değişkenin boyutu 2 bayt, uzun tamsayı (long integer) değişkenin boyutu ise 4 bayttır. Ancak bu boyut farklı donanım ve sistemlerde değişiklik arzedebilir.

Bir değişken tanımladığımız zaman derleyiciyi iki hususta bilgilendirmiş oluyoruz. Birincisi değişkenin adı, ikincisi değişkenin türü. Örneğin;

int k;

yazdığımızda k isminde, tamsayı (integer) türünde bir değişken tanımlamış oluyoruz. Bu ifadenin “int” kısmını gören derleyici, bu tamsayı (integer) değeri tutmak için bellekte 2 bayt yer ayırır. Ayrıca bir de sembol tablosu oluşturur. k sembolünün değerini ve bellekte 2 bayt olarak ayrılan yerdeki göreceli adresini bu tabloya ekler. Sonrasında;

k = 2;

yazıp ifadeyi çalıştırdığımızda k değerinin depolanması için ayrılmış olan bellek bölgesine 2 değeri yerleştirilecektir. C’de bu k tamsayısı gibi değişkenlere “nesne” adı verilir.

Sol Değer (Lvalue) ve Sağ Değer (Rvalue)

Yukarıdaki örnekte k nesnesi ile ilgili iki değer bulunmakta. Bu değerlerin ilki, tutulan tamsayının değeri (örneğimizde 2), diğeri ise bellek konumunun değeri yani k’nın adresidir. Bazı yerlerde bu iki değer sırasıyla Sağ Değer (Rvalue) ve Sol Değer (Lvalue) olarak adlandırmaktadır. Burada sağda veya solda olma durumu atama (=) operatörünün hangi tarafında olduğuna göre belirlenir.

Bazı dillerde Sol Değere atama (=) operatörünün yalnızca sol tarafında izin verilmiştir (yani adres sağ tarafın değerlendirme sonucunun bittiği yerdir). Sağ Değer ise atama ifadesinin sağ tarafındadır. Yukardaki örneğimizde Sağ Değer 2’dir. Sağ Değere de, Sol Değere benzer şekilde, atama ifadesinin yalnızca sağ tarafında izin verilmiştir, sol tarafında kullanılamaz. Yani 2=k; gibi bir ifade geçersiz olacaktır.

Aslında Sol Değerin tanımı tanımı C için biraz değiştirilmiştir. C dilinin babası Denis Ritchie’ye göre

“Bir nesne, depolama bölgesi olarak isimlendirilir, Sol Değer ise bir nesneye isnad edilen ifadedir.”

Orijinal haliyle alınan bu ifade durumu açıklamaya yeterlidir. İşaretçileri anlamak için biraz daha detaya ineceğiz.

Şöyle bir örnekle devam edelim:

int j, k;
k = 2;
j = 7; // <--- 1. satır
k = j; // <--- 2. satır

Yukarıda, derleyici 1. satırdaki j’yi, j değişkeninin adresi olarak yorumlar (Sol Değer) ve 7 değerini bu adrese kopyalamak için kod üretir. 2. satırdaki j, Sağ Değer olarak yorumlanır (‘=’ atama operatörünün sağ tarafında olduğundan). Yani buradaki j, j için ayrılmış bellek bölgesinde saklanan değeri gösterir. Örneğimizde bu değer 7’dir. Böylece 7, k Sağ Değeri ile gösterilen adrese kopyalanır.

Tüm bu örneklerde 2 bayt tamsayıları (integer) kullandığımız için Sağ Değer bir konumdan diğerine 2 bayt olarak kopyalandı. Uzun tamsayıları (long integer) kullanmış olsaydık, kopyalama 4 bayt olarak gerçekleşecekti.

Gelelim İşaretçilere

Şimdi de Sol Değerimizi (yani bir adresi) tutmak için tasarlanmış bir değişkene ihtiyacımız olduğunu düşünelim. Böyle bir değeri tutmak için gereken boyut sisteme bağlı olarak değişecektir. Toplam 64 K belleğe sahip eski masaüstü bilgisayarlarda, bellekteki herhangi bir noktada 2 bayt bulunabilir. Bellek miktarı arttıkça, bellekteki bir adresi tutmak için gereken bayt miktarı da artacaktır. IBM PC gibi bazı bilgisayarlar segment ve ofset tutmak için belli koşullar altında özel işlem gerektirebilir. İhtiyacımız olan gerçek boyut çok önemli değil. Önemli olan derleyiciye depolamak istediğimiz şeyin bir adres olduğunu bildirme yöntemine sahip olmamız.

Böyle bir değişken işaretçi değişkeni olarak adlandırılır. C’de bir işaretçi değişkeni, değişkenin sol tarafına yıldız işareti getirirerek tanımlıyoruz. Aynı zamanda işaretçimize, işaretçimiz içinde saklayacağımız adreste depolanan verinin türünü gösteren bir tip de belirtiyoruz. Aşağıdaki kod örneği buna uygun bir işaretçi değişkeni bildirimidir.

int *ptr;

Burada ptr, değişkenimizin adıdır (aynen tamsayı değişkenimizin adının k olduğu gibi). ‘*’ işareti, derleyiciye bir işaretçi değişkene ihtiyacımız olduğunu, yani bellekte bir adresi depolamak için kaç bayt yer ayrılacağını; int ise işaretçi değişkenimizi bir tamsayının adresini tutmak için kullanacağımızı söyler. Bu şekilde tanımladığımız bir işaretçiye tamsayı işaretçisi denir. Ancak dikkat edelim int k; yazdığımızda k’ya bir değer vermedik. Eğer bu tanım herhangi bir fonksiyonun dışında yapılırsa ANSI uyumlu derleyiciler bu değişkene sıfır değerini atar. Benzer şekilde yukardaki bildirimde içinde herhangi bir adres bulunmayan ptr’nin de değeri yoktur. Bu durumda yine eğer bildirim herhangi bir fonksiyonun dışında ise işaretçi herhangi bir C nesnesini veya fonksiyonunu göstermeyen bir değerle başlatılır. Bu şekilde başlatılmış bir işaretçi “null (boş)” işaretçi olarak adlandırılır.

Bir null gösterici için kullanılan gerçek bit deseninin sıfır olarak değerlendirilip değerlendirilemeyeceği kodun geliştirildiği sisteme bağlıdır. Kaynak kodun farklı sistemler üzerindeki farklı derleyiciler arasında uyumlu olması için boş işaretçiyi gösteren bir makro kullanılır. Bu makronun adı NULL’dur. Böylece bir göstericinin değerinin, ptr = NULL şeklinde bir atama ifadesiyle olduğu gibi, NULL makro kullanılarak ayarlanması işaretçinin boş bir işaretçi olmasını sağlar. Tıpkı if (k==0) ifadesinde sıfır tamsayı değeri için test yapılabildiği gibi, if(ptr==NULL) kullanarak boş bir gösterici için test yapabiliriz.

ptr değişkenimizin kullanımına tekrar dönelim. Diyelim ki ptr içinde k tamsayı değişkenimizin adresini tutmak istiyoruz. Bunun için Referans (&) operatörünü kullanıyoruz. Referans operatörü şu şekilde kullanılır:

ptr = &k;

& Operatörünün yaptığı şey, k ifadesi, atama (=) operatörünün sağında olsa bile, k’nın Sol değerini (yani adresini) getirmek ve bunu ptr işaretçi değişkenimizin içine kopyalamak.

Son operatörümüz olan Dereferans operatörünü (*) aşağıdaki gibi kullanıyoruz.

*ptr = 7;

Bu ifade ptr değişkeniyle gösterilen adrese 7 değerini kopyalayacaktır. Böylece eğer ptr, k’yı gösterirse, yukarıdaki ifade k’nın değerini 7 yapacaktır. Yani, ‘*’ operatörünü bu şekilde kullandığımızda ptr hangi değişkenin adresini ya da hangi bellek bölgesinin adresinin gösteriyorsa onun değerini kastediyoruz, işaretçinin kendisinin değerini değil.

Böylece ptr ile gösterilen adreste tutulan tamsayı değeri ekrana basmak için

printf("%d\n", *ptr);

ifadesini kullanabiliriz.

Buraya kadar tüm gördüklerimizi bir arada görmek için aşağıdaki programı çalıştıralım ve kaynak kodu ve çıktıyı dikkatlice inceleyelim.

#include <stdio.h>

int j, k;
int *ptr;
int main(void)
{
 j = 1;
 k = 2;
 ptr = &k;
 printf("\n");
 printf("j'nin degeri %d dir ve %p adresli bellek bolgesinde tutulmaktadir\n", j, (void *)&j);
 printf("k'nin degeri %d dir ve %p adresli bellek bolgesinde tutulmaktadir\n", k, (void *)&k);
 printf("ptr'nin degeri %p dir ve %p adresli bellek bolgesinde tutulmaktadir\n", ptr, (void *)&ptr);
 printf("ptr ile gosterilen tamsayinin degeri %d dir\n", *ptr);
 
 return 0;
}

Not: C’nin burada kullandığımız (void *) ifadesinin kullanımını gerektiren yönlerinden henüz bahsetmedik. Şimdilik sadece örnek kodumuza dahil ettik. İlerde bu ifadeyi daha detaylı şekilde inceleyeceğiz.

Özetleyecek olursak

Değişkenler bir isim ve bir tür ile bildirilir (örneğin int k;).

İşaretçi değişkenler de bir isim ve bir tür ile bildirilir (örneğin int * ptr;). Asterisk işareti ptr adlı değişkenin bir işaretçi değişken olduğunu ve türü, işaretçinin derleyiciye hangi türü gösterdiğini söyler (örneğimizde bir tamsayı).

Bildirilmiş bir değişkenin adresini isminin önüne, &k’daki gibi, & operatörü getirerek alabiliriz.

İşaretçileri “dereferans” edebiliriz, yani ‘*’ operatörünü *ptr şeklinde kullanarak işaretçinin gösterdiği adresteki değeri alabiliriz.

Değişkenlerin Sol değeri onun adresidir, yani bellekte depolandığı yerdir; Sağ değeri ise bu değişken içinde depolanan değerdir (bu adresteki).

Bu yazımızın burada sonuna geldik. Faydalı bir paylaşım olmasını diliyorum umarım istifade edilir. Bir sonraki yazıma İşaretçi türleri ve Diziler başlığıyla devam etmeyi düşünüyorum.

 

İlk Yorumu Siz Yapın

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir