Standart C Programlama Dili


3.1.3. Gösterge Aritmetiği

Şimdi, bu kısım başında verilen örneğe geri dönelim ve ne olduğuna bakalım. Dizi bildirimi, derleyicinin dizi için, ana bellekte yer ayırmasını sağlar. Bir int değişkeninin 4 bayt tuttuğunu ve N’nin 10 olarak #define ile tanımlandığını varsayın. Bu durumda z dizisi 40 bayt tutacaktır. İlk eleman, adresi &z[0] ifadesi ile elde edilen yerde saklanır. (Tanım gereği &z[0] ile z aynı şeydir ve buna dizinin temel adresi denir. Diğer bir deyişle, bir ifade içinde indissiz kullanılan dizi isimleri belirli bellek bölgelerini gösterirler, yani göstergedirler.)

Bir dizinin i’inci elemanını elde etmek için (örneğin z[i] yazıldığında), derleyici şunu yapar: Dizinin temel adresini (yani ilk elemanının adresini) alır (örneğin 8566), sonra indisin değerini alır (örneğin 3) ve eğer dizinin eleman tipi n tane bayt kullanıyorsa (bizim örneğimizde tamsayılar için bu 4 bayttır) i’inci elemanın adresini bulmak için TemelAdres+n×i hesaplar. Şimdi dizi indislerinin neden sıfırdan başladığı daha iyi anlaşılıyor; eğer i sıfıra eşitse, o zaman ikinci terim yok olur ve, olması gerektiği gibi, dizinin ilk elemanının adresinin gerçekte dizinin temel adresi olduğu ortaya çıkar.

for deyimlerimize geri dönersek, ilkinde bu hesaplamanın her yinelemede tekrarlandığını görürüz. İkinci durumda ise, açıkça bir gösterge kullanılmıştır. Tamamen

for (g=z; g<&z[N]; g++)
  *g = 0;

deyimiyle eşdeğer olan bu deyim, önce g int göstergesine z dizisinin temel adresini koyar (örneğin 8566); daha sonra g ile işaretlenmiş bölgeye sıfır koyar. Bu adreste (yani 8566’da) saklanacak olan değerin tipinin derleyici tarafından bilinmesi gerekir, int, char, double, yoksa başka bir şey mi? Her tipin bellekte farklı sayıda bayt kapladığını anımsayın. Bunu göstergenin tipinden belirler. Bu bir tamsayı göstergesi olduğuna göre, bu gösterge tarafından işaret edilen bölgede (dört baytta) bir tamsayı değişkeni bulunmalıdır. Bundan dolayı 8566, 8567, 8568 ve 8569 adresli baytlara sıfır yerleştirir.

Bundan sonra gösterge artırılır. Şimdi yine bir sorunumuz var demektir, çünkü sayılar için artırma (veya azaltma) elimizdeki değere bir sayısı eklenerek (veya çıkarılarak) yapılır. Aynı şey göstergelere uygulanamaz. g’nin gösterdiği adresin değerine bir eklenmesi, göstergenin dizideki bir sonraki elemana (yani 8570 adresine) işaret etmesini sağlayamayacaktır; onun yerine, ilk elemanı içeren “sözcüğün” ikinci baytını (yani 8567 adresini) gösterecektir. Gösterge ile gösterilen elemanın boyuna eşit bir sayı (yani örneğimizde tamsayılar için 4) eklemeliyiz. Bu, derleyici tarafından bizim için yapılır. Yani, int göstergesi “++” işleci ile artırıldıktan sonra 8570 adresini göstermeye başlayacaktır. Değişik tipteki göstergelerin uyumlu olmamalarının nedenleri işte bu farklılıklardır.

Gösterge aritmetiğinin diğer şekillerinde de benzer bir sorun ortaya çıkar. Örneğin, eğer g’de 8566 (&z[0]) adresi varsa, g+3 ifadesi nereyi göstermelidir? 8569 adresini mi? Hayır! Bu, z[0]’i içeren sözcüğün son baytının adresi demektir ki fazla anlamlı bir şey değildir. Daha iyisi &z[3] (yani 8578) adresine işaret etmesi olacaktır. Yine, derleyici bize bunu sağlar: Bir adrese (göstergeye), z, bir tamsayı, i, eklediğimizde (veya çıkardığımızda), derleyici z tarafından işaret edilen değişken tipinin boyunu i ile çarpar ve sonra toplama (veya çıkarmayı) yapar. Bunun nedeni adreslerin her zaman baytlara işaret etmelerine karşılık, göstergelerin değişik büyüklüklere sahip tipte nesneleri göstermeleridir.

Bu anlatılanların anafikri göstergelerin tamsayı olmadıkları ve bazı işleçlerin (++, --, +, -, +=, -=) göstergeler için farklı uygulandıklarıdır. *g ile gösterilen değişkenin n bayt kapladığını varsayın. O zaman,

g++
g--
g+tamsayi_ifade
g-tamsayi_ifade

gösterge ifadeleri sırasıyla g+n, g-n, g+n×tamsayı_ifade, g-n×tamsayı_ifade şeklinde hesaplanır. Ayrıca p1 ve p2 aynı tipten göstergelerse, değeri bir tamsayı olan

p1-p2

ifadesi (p1-p2)/n şeklinde hesaplanır. Örneğin,

p1 = &z[3];
p2 = &z[0];
printf("%ld\n", (long)(p1-p2));

(z dizisinin eleman tipine bağlı olmaksızın) çıktı olarak 3 verecektir. İki göstergeyi birbirinden çıkardığımızda, kullanılan sistemdeki bellek boyuna bağlı olan bir tipte bir ifade oluştuğuna dikkat edin. İşaretli olan bu tip stddef.h adlı başlık dosyasında uygun bir şekilde prtdiff_t adıyla tanımlanmıştır. Görüntüleme işlemi sırasında, biz bunu long’a dönüştürmeyi tercih ettik.

İki uyumlu gösterge (<, >, <=, >=, == ve != kullanarak) karşılaştırılabilir, ancak farklı dizilerdeki nesnelere işaret eden iki göstergeyi karşılaştırmak anlamlı (en azından taşınabilir bir özellik) değildir. Doğal olarak, bunun nedeni iki dizinin ana bellekteki birbirine göre durumları konusunda emin olamayacağınızdır. Bu kısımdaki ikinci for’da kullanılan “g<&z[N]” testi uygundur, çünkü g ve &z[N] aynı dizi içindeki (olası değişik) yerlere işaret ederler. &z[N] adresinin z dizisi içinde olmadığını söyleyerek, buna karşı çıkabilirsiniz, zira dizinin son elemanı z[N-1]’dir. Haklısınız, ancak g’nin bu “geçersiz” adrese işaret ettiği zaman *g’ye birşey atamamak için, <= yerine < kullanacak kadar dikkatli olduğumuzu da gözlemleyin. C Standardı ise, bir nesnenin sınırları dışında adres üretmeyi—o adresteki bölge değiştirilsin değiştirilmesin—açıkça yasaklamaktadır. Tek istisna olarak, bir nesnenin son baytından bir sonraki baytı gösteren adresi üretmeğe izin verir. İşte bu özellik “g<&z[N]”de kullanılmıştır.

Bir gösterge (== veya != kullanılarak) 0 tamsayı değişmeziyle karşılaştırılabilir. Geleneksel olarak 0 değerinin hiç bir şeyi göstermediği varsayılmıştır. Bu tamsayı değişmezi programlarda o kadar çok kullanılmaktadır ki stdio.h başlık dosyasında

#define NULL 0

şeklinde bir tanımlama yapılmıştır. Böylece, eğer 0 yerine NULL kullanılacak olursa “göstergeler tamsayı değildir” kuralı bozulmamış gibi görünecektir. Bir göstergenin herhangi bir yeri göstermesini istemiyorsanız

g = NULL;

atama deyimini kullanın. NULL’un her tip gösterge ile uyumlu olduğuna dikkat edin. Bu özel bir durumdur.