27 Aralık 2011 Salı

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

Merhabalar, uzun zamandan beri blogumla ilgilenemedim. Bir süredir C ve C++'da önemli bir konu bir bel kemiği konusu olan İşaretçiler (Pointers) konusunda internette araştırmalar yapıyordum. İşaretçiler birçokları için korkulu bir rüya. Ancak işaretçiler konusu anlaşılmadan tam anlamıyla bir C programcısı olmak çok zor hatta imkansız. Bu konuda internete bulduğum bir dökümandan biraz yazmak istiyorum.

İşaretçiler kapsam olarak çok geniş. Ancak yazı kısa tutulmuş ve en basit konu olan değişken kavramından başlayarak aşama aşama ilerlenmiş. Son olarak fonksiyon işaretçileriyle konu tamamlanmış.

Hazırsak başlayalım.


İşaretçi Nedir?
İşaretçiler veya göstericiler (Pointers) kavramının C ve C++'ta anlaşılması zor konulardan birisi olduğuna değinmiştik. 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ı gerekmektedir.

Her değişkenin, değişebilen bir değeri ve bir ismi vardır. Derleyici ve bağlayıcı bilgisayarımızın belleğindeki belli bir bellek bloğunu bu değişkenin değerini tutmak için ayırır. Bu bloğun boyutu değişkenin üzerinde değişimine izin verilen aralığa 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öreli 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.

Burada k nesnesi ile ilgili iki değer bulunmakta. Bu değerlerin ilki tutulan tamsayının değeri (örneğimizde 2), diğeri bellek konumunun değeridir (k'nın adresi). Bazı yerlede bu iki değer rvalue (sağ değer) ve lvalue (sol değer) olarak adlandırmaktadır.

Bazı dillerde lvalue, "=" atama operatörünün sol tarafında izinlidir (yani adres sağ tarafın değerlendirme sonucunun bittiği yerdir). Rvalue atama ifadesinin sağ tarafındadır. Yukardaki örneğimizde Rvalue 2'dir. Rvalue atama ifadesinin sol tarafında kullanılamaz. Yani 2=k; gibi bir ifade geçersizdir.

Aslında "lvalue"nin tanımı C için biraz değiştirilmiştir. K&R II (s. 97)[1]'e göre:

"Bir nesne, depolama bölgesi olarak isimlendirilir, lvalue ise bir nesneye isnad edilen ifadedir."

Orjinal 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


Yukarda, derleyici 1. satırdaki j'yi j değişkeninin adresi olarak yorumlar (lvalue) ve 7 değerini bu adrese kopyalamak için kod üretir. 2. satırdaki j, rvalue 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 lvalue değeri ile gösterilen adrese kopyalanır.

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

lvalue 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 önüne yıldız 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 belirtiyoruz. Örneğin şöyle bir değişken bildirimi düşünelim:

   int *ptr;

Burada ptr, değişkenimizin adıdır (tıpkı tamsayı değişkenimizin adının k olduğu gibi). '*', 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, işaretçi değişkenimizi bir tamsayının adresini tutmak için kullanacağımızı söyler. Böyle bir işaretçiye bir tamsayıyı "gösterir" 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.

Yeni 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 tekli & operatörünü kullanıyoruz ve şu şekilde yazıyoruz:

    ptr = &k;

& Operatörünün yaptığı şey, k, '=' atama operatörünün sağında olsa bile, k'nın lvalue değerini (adres) getirmek ve bunu ptr işaretçimizin içine kopyalamak. Şimdi biraz daha sabır son bir operatör daha kaldı.

Son operatörümüz "Dereferans operatörü" olan asterisk ve 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 neyi gösteriyorsa onun değerini kastediyoruz, işaretçinin kendisinin değerini değil.

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

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

şeklinde yazabiliriz.

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.

   ------------ Program 1.1 ---------------------------------
   /* Program 1.1 from PTRTUT10.TXT 6/10/97 */

   #include <stdio.h>
   int j, k;
   int *ptr;
   int main(void)
   {
      j = 1;
      k = 2;
      ptr = &k;
      printf("\n");
      printf("j has the value %d and is stored at %p\n", j, (void *)&j);
      printf("k has the value %d and is stored at %p\n", k, (void *)&k);
      printf("ptr has the value %p and is stored at %p\n", ptr, (void *)&ptr);
      printf("The value of the integer pointed to by ptr is %d\n", *ptr);
      return 0;
   }


Not: C'nin burada kullandığımız (void *) ifadesinin kullanımını gerektiren yönlerinden henüz bahsetmedik. Şimdilik sadece test 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 "lvalue" değeri onun adresidir, yani bellekte deoplandığı yerdir. Bir değişkenin "rvalue" değeri bu değişken içinde depolanan değerdir (bu adresteki).

Referanslar
  1. "The C Programming Language" 2nd Edition, B. Kernighan, D. Ritchie, Prentice Hall, ISBN 0-13-110362-8
  Bir sonraki yazımıza İşaretçi türleri ve Diziler başlığıyla devam edeceğiz.

Hiç yorum yok: