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

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

Merhaba sevgili ziyaretçiler önceki yazımızda C ve C++’ta değişkenler ve işaretçilere giriş yapmıştık. Bu yazımızda ise işaretçilere kaldığımız yerden devam ediyoruz. Bu yazımızda işaretçiler ve diziler arasındaki ilişkiyi irdeleyeceğiz ve kısaca void işaretçi türünden bahsedeceğiz.

Şö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.

Bu işaretçi değişkenimize şöyle bir atama yaptığımızda:

*ptr = 2;

Derleyicinin ptr ile gösterilen bellek konumuna ne kadar bayt kopyalanacağını bilmesi gerekir. Eğer ptr bir tamsayı (integer) işaretçisi olarak bildirilmişse sağdaki değer 2 bayt; eğer uzun tamsayı (long) olarak bildirilmişse 4 bayt olarak kopyalanır. Aynı şekilde ondalıklı (float) ve çift duyarlıklı ondalıklı (double) sayılar için de bu veri türlerine uygun miktarda bayt olarak kopyalanır. Fakat işaretçinin gösterdiği türün tanımlanması, derleyicinin kodu diğer bazı enteresan şekillerde yorumlamasını da sağar.

Örneğin bellekte bir satırda on tamsayıdan oluşan ardışıl verilerimiz olduğunu varsayalım. Bu on tamsayıyı tutmak için bellekte 20 bayt yer ayrılacaktır.

Bu tamsayıların ilkini ptr tamsayı işaretçimizle gösterdiğimizi düşünelim. Ayrıca bu tamsayı, adresi (onluk tabanda) 100 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 ve 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 farklıdır. C’de bu işlem işaretçi aritmetiği ile 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). Bu durum, bu 10 tamsayılık blok (yani tamsayı dizisi) bellekte ardışıl olarak yerleştirildiğinden, işaretçiler ve diziler arasındaki ilişkiyi meydana getirir.

Buna uygun olarak aşağıdaki örnek kod parçasını ele alacak olursak:

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

Burada 6 tamsayı değer içeren bir değişken tanımladık. Bu tamsayıların her birine bizim_dizi değişkenine vereceğimiz bizim_dizi[0] ve bizim_dizi[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 = &bizim_dizi[0]; /* işaretçimiz şimdi dizimizdeki ilk tamsayıyı gösteriyor */

Dizimizi, dizi notasyonunu (yani [] operatörünü) kullanarak ya da işaretçimizi dereferans ederek yazdırabiliriz. Aşağıdaki kod örneği bunu göstermektedir:

#include <stdio.h>
int bizim_dizi[] = {1,23,17,4,-5,100};
int *ptr;

int main(void)
{
 int i;
 ptr = &bizim_dizi[0]; /* göstericimiz şimdi dizinin birinci elemanını gösteriyor */
 printf("\n\n");
 for (i = 0; i < 6; i++)
 {
  printf("bizim_dizi[%d] = %d \t", i,bizim_dizi[i]); /*<<-- A */
  printf("ptr + %d = %d\n",i, *(ptr + i));        /*<<-- B */
 }
 return 0;
}

Yukarıdaki kod örneğimizi derleyip çalıştırırsak aşağıdaki gibi bir çıktı elde ederiz.

bizim_dizi[0] = 1    ptr + 0 = 1
bizim_dizi[1] = 23   ptr + 1 = 23
bizim_dizi[2] = 17   ptr + 2 = 17
bizim_dizi[3] = 4    ptr + 3 = 4
bizim_dizi[4] = -5   ptr + 4 = -5
bizim_dizi[5] = 100  ptr + 5 = 100

Yukarıdaki kod örneğimizde A ve B satırlarını dikkatlice 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.

Şimdi B satırını aşağıdaki gibi değiştirelim ve ne olacağını görelim:

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

Tekrar çalıştıralım.

bizim_dizi[0] = 1    ptr + 0 = 1
bizim_dizi[1] = 23   ptr + 1 = 23
bizim_dizi[2] = 17   ptr + 2 = 17
bizim_dizi[3] = 4    ptr + 3 = 4
bizim_dizi[4] = -5   ptr + 4 = -5
bizim_dizi[5] = 100  ptr + 5 = 100

Sonuç değişmedi değil mi.

Şimdi aynı satırı bir de aşağıdaki gibi değiştirelim:

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

Ve bir kez daha derleyelim.

bizim_dizi[0] = 1    ptr + 0 = 23
bizim_dizi[1] = 23   ptr + 1 = 17
bizim_dizi[2] = 17   ptr + 2 = 4
bizim_dizi[3] = 4    ptr + 3 = -5
bizim_dizi[4] = -5   ptr + 4 = 100
bizim_dizi[5] = 100  ptr + 5 = 0

O da ne ilk ve son değerlerimiz nerede? Bunun sebebi ptr gösterici değişkenimizin değerini önce artırıp sonra dereferans etmemiz. Bu yüzden artırma operatörlerinin kullanımına dikkat etmek gerekir.

C’de “&degisken_adi[0]” standart ifadesini kullanabildiğimiz her yerde bunu “degisken_adi” ifadesi ile değiştirebiliriz. Böylece kodumuzda:

ptr = &bizim_dizi[0];

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

ptr = bizim_dizi;

yazabiliriz.

Bu durum dizi isimlerinin birer işaretçi olduğunu gösterir. Buradan “dizinin adı, dizideki ilk elemanın adresidir” sonucunu çıkarabiliriz. C’ye yeni başlayanların dizi adlarının bir işaretçi değişken olduğunu düşünerek kafaları karışabilir. Örneğin:

ptr = bizim_dizi;

yazabiliyorken,

bizim_dizi = ptr;

yazamayız. Sebebi ise ptr bir değişken olduğu halde bizim_dizi’nin bir sabit olmasıdır. Yani bizim_dizi dizisinin ilk elemanının tutulacağı konum önceden bildirildiği için değiştirilemez. Daha önce sol değer teriminden bahsederken Denis Ritchie’den şöyle bir alıntı yapmıştık:

“Nesneler isimlendirilmiş bellek bölgeleridir; sol değer ise bir nesneye atfedilen ifadedir.”

Bu durum ilginç bir problem olarak karşımıza çıkmaktadır. bizim_dizi bir depolama bölgesi olarak isimlendirilmişse, neden yukarıdaki atama ifadesinde bir sol değer değil? Bu sorunu çözmek için bizim_dizi‘yi “değiştirilemez sol değer” olarak adlandıracağız. Yukarıdaki örnek programımızda:

ptr = &bizim_dizi[0];

satırını,

ptr = bizim_dizi;

olarak değiştirip çalıştırırsak her iki ifadedeki sonucun da aynı olduğunu görürüz.

ptr ve dizim_dizi isimleri arasındaki farka biraz daha derinlemesine bakalım. Bazı yazarlar dizileri sabit işaretçiler olarak adlandırırlar. 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 ayrılır. Burada 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 data segmentinde yer tutarken, 2 bir sabittir ve data segmentte bellek ayırmak 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 segment içindeki yerine koyar ve data segmentin referans edilmesi söz konusu olmaz. Yani, k ve i nesne iken 2 nesne değildir.

Aynı şekilde yukardaki bizim_dizi de bir sabit olduğundan, derleyici dizinin depolanacağı yeri belirler belirlemez bizim_dizi[0]‘nin adresini bilir.

ptr = bizim_dizi;

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

Önceki yazımızda verdiğimiz ö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 karakterleri gösteren işaretçilerden bahsettik. Ancak diğer türlerde de işaretçiler tanımlanabilir. Sonraki yazılarda yapıları gösteren işaretçilerden ve hatta işaretçileri gösteren işaretçilerden de bahsetmeyi düşünüyorum.

İş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, başka türde işaretçilerin değerlerini atamak da sorun olabilir.

Bu sorunu aşmak için C programcısına void türünde bir işaretçi sunulmuştur. Void işaretçisini,

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.

Yazımızın sonuna geldik. Kısmet olursa bir sonraki yazımızda işaretçiler karakter dizileri ve katarlar (string) üzerinde durmayı düşünüyorum.

İlk Yorumu Siz Yapın

Bir yanıt yazın

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