29 Aralık 2011 Perşembe

C ve C++'ta İşaretçiler (Pointers): İşaretçi Türleri ve Diziler

Evet işaretçilere kaldığımız yerden devam ediyoruz. Şöyle bir işaretçi değişken bildirimi yaptığımızı düşünelim:

   int *ptr;

Bu işaretçinin gösterdiği değişkenin türünü neden tanımlamamız gerektiği üzerine düşünerek konumuza başlayalım.

Bunun nedeni,

   *ptr = 2;

yazdığımızda derleyicinin ptr ile gösterilen bellek konumuna ne kadar bayt kopyalayacağını bilmesi içindir. Eğer ptr bir tamsayı (integer) işaretçisi olarak bildirilmişse 2 bayt kopyalanacak, eğer uzun tamsayı (long) olarak bildirilmişse 4 bayt kopyalanacaktır. Aynı şekilde ondalıklı (float) ve çift duyarlıklı ondalıklı (double) sayılar için de uygun miktarda bayt kopyalanacaktır. Fakat işaretçinin gösterdiği türün tanımlanması derleyicinin, kodu farklı yöntemleri kullanarak yorumlamasına izin verir. Mesela bellekte bir satırda on tamsayıdan oluşan bir bloğumuz olsun. Bu on tamsayıyı tutmak için 20 bayt bellek ayrılacaktır.

Diyelim ki bu tamsayıların ilkinde ptr tamsayı işaretçimizi gösterdik. Ayrıca bu tamsayı, adresi 100 (onluk tabanda) olan bellek bölgesinde bulunsun.

   ptr + 1;

Yazdığımızda neler olacağına bakalım. Derleyici bunun bir işaretçi (yani değerinin bir adres) olduğunu ve bir tamsayıyı (ptr'nin 100 olan adresi bir tamsayının adresidir) gösterdiğini bildiğinden ptr'ye 1 yerine 2 ekler böylece işaretçimiz 102. bellek konumunda bulunan bir sonraki tamsayıyı gösterir. Eğer ptr bir uzun tamsayı (long) işaretçisi olarak bildirilmiş olsaydı 1 yerine 4 eklenecekti. Aynı şey ondalık (float), çift duyarlı ondalık (double) ve hatta yapılar (structure) gibi kullanıcı tanımlı diğer veri türleri için de geçerlidir. Bu işlem bildiğimiz "toplama" işleminden oldukça farklı. C'de bu işlem sonradan tekrar dönüş yapacağımız bir terim olan "işaretçi aritmetiği" kullanarak toplama olarak bilinir.

++ptr ve ptr++ ifadelerinin ikisi de ptr+1'e eşittir. Bu yüzden işaretçiden önce veya sonra "++" operatörünü kullanarak bir işaretçiyi artırmak; işaretçinin sakladığı adresi sizeof(tür) ("tür" işaret edilen nesnenin türüdür) miktarı kadar artırır (yani tamsayı için 2, uzun tamsayı için 4). 10 tamsayılık bir blok bellekte ardışıl olarak yerleştirilir (tamsayı dizisi olarak tanımlanır). Bu yüzden bu durum işaretçiler ve diziler arasında ilginç bir ilişkiyi meydana getirir.

Bunun için aşağıdaki örnek kod parçasını inceleyelim:

   int my_array[] = {1, 23, 17, 4, -5, 100};

Yukardaki örneğimizde 6 tamsayı değer içeren bir değişken tanımladık. Bu tamsayıların her birine my_array değişkenimize vereceğimiz my_array[0] ve my_array[5] arasındaki alt indisler yoluyla erişebiliriz. Ancak alternatif olarak bu değerlere işaretçiler vasıtasıyla aşağıdaki gibi de erişebiliriz:

   int *ptr;
   ptr = &my_array[0];    /* işaretçimiz şimdi dizimizdeki ilk tamsayıyı gösteriyor */

Ardından dizimizi, dizi notasyonunu kullanarak ya da işaretçimizi dereferans ederek yazdırabiliriz. Aşağıdaki kod bunu göstermektedir:

   ----------- Program 2.1 -----------------------------------
   /* Program 2.1 from PTRTUT10.HTM 6/13/97 */
   #include <stdio.h>
   int my_array[] = {1,23,17,4,-5,100};
   int *ptr;
   
   int main(void)
   {
      int i;
      ptr = &my_array[0]; /* point our pointer to the first of the array */
      printf("\n\n");
      for (i = 0; i < 6; i++)
      {
         printf("my_array[%d] = %d ",i,my_array[i]); /*<-- A */
         printf("ptr + %d = %d\n",i, *(ptr + i)); /*<-- B */
      }
      return 0;
   }

Yukardaki programı derleyip çalıştıralım. A ve B satırlarını dikkatli incelersek her iki durumda da aynı değerlerin yazdırıldığını görürüz. Yine B ile gösterilen satırda işaretçimizin nasıl dereferans edildiğini de gözlemleriz, yani burada önce işaretçimize i ekliyoruz ardından bu yeni işaretçiyi dereferans ediyoruz. B satırını değiştirin:

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

Ve tekrar çalıştırın. Yine değiştirin:

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

Ve bir kez daha deneyin. Her denemede sonucu tahmin etmeye çalışın ve ardından gerçek sonuca bakın.

C'de &degisken_adi[0] standart durumunu kullanabildiğimiz her yerde bunu "degisken_adi" ile değiştirebiliriz. Böylece kodumuzda:

   ptr = &my_array[0];

yazdığımız yere aynı sonucu elde edebildiğimiz:

   ptr = my_array;

yazabiliriz.

Bu durum  dizi isimlerinin birer işaretçi olduğunu gösterir. Buradan "dizinin adı, dizideki ilk elemanın adresidir" sonucunu çıkarabiliriz. Yeni başlayan çoğu kişinin dizi adlarının bir işaretçi değişken olduğunu düşünerek kafaları karışabilir. Örneğin:

   ptr = my_array;

yazabiliyorken,

   my_array = ptr;

yazamayız. Sebebi ise ptr bir değişken olduğu halde my_array bir sabittir. Yani my_array dizisinin ilk elemanının tutulacağı konum, my_array[] bildirildiği için değiştirilemez. Daha önce "lvalue" teriminden bahsederken K&R-2'den şöyle bir alıntı yapmıştık:


   "Nesneler depolama bölgesi olarak adlandırılır; lvalue ise bir nesneye isnad edilen ifadedir."

Bu ilginç bir problem olarak karşımıza çıkmaktadır. my_array depolama bölgesi olarak isimlendirilmişse, neden yukardaki atama ifadesinde bir lvalue değil? Bu sorunu çözmek için my_array'i biraz "değiştirilemez lvalue" olarak adlandıracağız. Yukardaki örnek programımızda:

   ptr = &my_array[0];

satırını,

   ptr = my_array;

olarak değiştirelim ve çalıştıralım. Burada her iki ifadedeki sonuç da aynıdır.

ptr ve my_array isimleri arasındaki farka biraz daha derinlemesine bakalım. Bazı yazarlar dizileri sabit işaretçiler olarak adlandırırlar. Bununla ne demek istiyoruz? Bu anlamda "sabit" terimini anlamak için "değişken" teriminin tanımına geri dönelim. Bir değişken bildirdiğimizde bu değişkene uygun türde değeri tutmak için bellekte bir noktada yer ayırılır. Bu yapıldığında değişkenin adı iki farklı şekilde yorumlanır. Atama operatörünün sol tarafında kullanıldığında derleyici bunu atama operatörünün sağ tarafının değerlendirilmesinden çıkan sonucu taşıyacağı bellek konumu olarak yorumlar, ancak atama operatörünün sağ tarafında kullanıldığında değişkenin adı bu değişkenin değerini tutmak için ayrılmış bellek adresinde saklanan içerik olarak yorumlar.

Bunu aklımızda tutarak, aşağıdaki gibi, sabitlerin en basit şeklini düşünelim:

   int i, k;
   i = 2;

Burada i bir değişken olup belleğin veri bölümünde yer tutarken, 2 bir sabittir ve data segmentte bellek ayrımak yerine doğrudan belleğin kod segmentine gömülür.Yani k=i; gibi bir ifade yazarak derleyiciye kodu oluşturmak için çalışma zamanında k'ya taşınacak değeri belirlemek için &i bellek konumuna bakacağını söylerken, i=2; ile oluşturulan kod, 2'yi kod içindeki yerine koyar ve data segmentin referanslanması söz konusu olmaz. Yani, k ve i nesne iken 2 nesne değildir.

Benzer şekilde yukardaki my_array da bir sabit olduğundan, derleyici dizinin depolanacağı yeri belirler belirlemez my_array[0]'ın adresini bilir.

   ptr = my_array;

ifadesinin görülmesi üzerine bu adresi kod segment içinde bir sabit olarak kullanır ve data segmentin referanslanması da söz konusu olmaz.

Bir önceki yazıda verdiğimiz Program 1.1 başlıklı örnek programda kullandığımız (void *) ifadesinin kullanımına biraz daha detaylı bakalım. Görüldüğü üzere farklı türlerde işaretçilere sahip olabiliyoruz. Şimdiye kadar hep tamsayıları ve karaketerleri gösteren işaretçilerden bahsettik. İlerde yapıları gösteren işaretçilerden ve hatta işaretçileri gösteren işaretçilerden de bahsedeceğiz.

İşaretçilerin boyutlarının sistemden sisteme değişebileceğinden bahsetmiştik. Aynı zamanda bir işaretçinin boyutunun gösterdiği nesnenin veri tipine göre de değişmesi mümkün. Böylece nasıl kısa tamsayı türünde bir değişkene uzun tamsayı atamaya çalışmak sorun olabiliyorsa, başka türde işaretçi değişkenlere çeşitli türde işaretçilerin değerlerini atamak da sorun olabilir.

Bu sorunu minimize etmek için C, void türünde bir işaretçi sunmuştur. Böyle bir işaretçiyi,

   void *vptr;

yazarak bildirebiliriz.

Void işaretçi genel bir işaretçi çeşididir. Örneğin C, karakter türünde bir değişkenle tamsayı türünde bir değişkenin karşılaştırılmasına izin vermezken, bunların her ikisi de void türünde bir işaretçi ile karşılaştırılabilir. Tabiki diğer değişkenlerde olduğu gibi uygun şartlarda bir işaretçi türünü diğerine çevirmek için kast işlemi yapılabilir. Bir önceki yazıda verdiğimiz Program 1.1 örnek kodunda %p çevrim belirtimiyle uyumlu hale getirmek için tamsayı işaretçilerini void işaretçilere kast ettik. İlerleyen yazılarda diğer kast işlemlerini de göreceğiz.


Sonraki yazımızda işaretçiler karakter dizileri ve katarlar (string) üzerinde duracağız.




Hiç yorum yok: