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
ptrdiff_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
testi uygundur, çünkü g<&z[N]
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
de
kullanılmıştır.g<&z[N]
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.