Ders 02 / 15
İşaretçi Aritmetiği
Bir işaretçiyi artırmanın, azaltmanın ve iki işaretçiyi çıkarmanın neden bayt cinsinden değil eleman cinsinden işlediği; sınır dışına taşan bir aritmetiğin neden hiç koşulmadığı.
Yayın Güncelleme
Bu derste
- Bir işaretçiyi artırma ve azaltmanın tip boyutuna göre nasıl ilerlediğini ölçer
- İki işaretçinin farkını eleman biriminde hesaplar ve sırasını karşılaştırma ile ölçer
- Dizi sınırlarının dışına taşan bir aritmetiğin neden koşturulmadığını açıklar
İçindekiler
Önceki ders yerel dizi satırında son - ilk farkını 3 olarak verdi: dört elemanlı
bir dizide ilk ve son eleman arasında üç adım var. Ama bu fark bayt cinsinden değil —
son ve ilk arasındaki gerçek bellek uzaklığı, dizinin eleman tipi kaç bayt tutuyorsa
onun katıdır. Bu ders o katsayının nereden geldiğini ve bir işaretçiyi artırıp
azaltmanın ne anlama geldiğini ölçüyor. Sınıf hâlâ dil: bir dizinin elemanları
arasında gezinmek, herhangi bir ayırma ya da bırakma çağrısı gerektirmiyor.
Artırma ve Azaltma: Tip Boyutuna Göre İlerleme
// artirma.c — bir isaretciyi artirmak, tip boyutu kadar ilerler
#include <stdio.h>
int main(void) {
int dizi[5] = {10, 20, 30, 40, 50};
int *p = dizi;
printf("%s %d\n", "ilk eleman", *p);
p++;
printf("%s %d\n", "bir artirmadan sonraki eleman", *p);
p += 2;
printf("%s %d\n", "uc artirmadan sonraki eleman", *p);
p--;
printf("%s %d\n", "bir azaltmadan sonraki eleman", *p);
double cift[3] = {1.5, 2.5, 3.5};
double *d = cift;
d++;
printf("%s %.1f\n", "double dizide bir artirmadan sonraki eleman", *d);
return 0;
}
ilk eleman 10 bir artirmadan sonraki eleman 20 uc artirmadan sonraki eleman 40 bir azaltmadan sonraki eleman 30 double dizide bir artirmadan sonraki eleman 2.5
IS5. p++ işaretçiyi bir sonraki int‘e taşıyor — yazılan adım her zaman bir
eleman, bir bayt değil. p += 2 iki elemanı birden atlıyor: indeks 1’den indeks 3’e.
p-- bir eleman geri dönüyor. double cift dizisinde de aynı p++ yazımı kullanıldı
ve yine tam olarak bir eleman ilerledi — int ve double farklı boyutlarda olsa
bile, artırma işleci her ikisinde de “bir sonraki eleman” anlamına geliyor. Boyutun
kendisi (sizeof) hiçbir yerde basılmadı; ölçüm yalnız işaret edilen değer üzerinden
yapıldı.
İki İşaretçinin Farkı ve Sırası
// fark_ve_sira.c — iki isaretcinin farki eleman biriminde, sirasi karsilastirma ile olculur
#include <stdio.h>
int main(void) {
int dizi[6] = {1, 2, 3, 4, 5, 6};
int *ilk = &dizi[0];
int *orta = &dizi[3];
int *son = &dizi[5];
printf("%s %d\n", "orta eksi ilk", (int)(orta - ilk));
printf("%s %d\n", "son eksi orta", (int)(son - orta));
printf("%s %d\n", "ilk once orta mi geliyor", (int)(ilk < orta));
printf("%s %d\n", "son once orta mi geliyor", (int)(son < orta));
int *gezici = ilk;
int adim = 0;
while (gezici < son) {
gezici++;
adim++;
}
printf("%s %d\n", "ilkten sona kadar atilan adim", adim);
return 0;
}
orta eksi ilk 3 son eksi orta 2 ilk once orta mi geliyor 1 son once orta mi geliyor 0 ilkten sona kadar atilan adim 5
IS6. orta - ilk 3 veriyor: dizi[3] ile dizi[0] arasında üç eleman aralığı
var. Bu çıkarma işleminin sonucu her zaman eleman sayısıdır, iki adresin bayt
farkı değil — orta - ilk ifadesi 3 yazdırıyor, adreslerin kendisi hiç görünmüyor.
ilk < orta karşılaştırması 1 veriyor çünkü ilk, dizide orta’dan önce duruyor;
bu sıralama, < işlecinin aynı dizi içindeki iki işaretçi için tanımlı olmasından geliyor.
gezici < son koşuluyla kurulan döngü, ilk’ten son’a kadar tam 5 adımda
ilerliyor — bu da son - ilk farkıyla birebir eşleşiyor.
char Dizisinde Aritmetik: Eleman Adımı Bayt Adımına Eşitken
Önceki iki örnekte int ve double dizileri kullanıldı; her ikisinde de bir artırma
birden fazla bayt ilerledi, ama bu ilerleme hiç ölçülmedi — yalnız işaret edilen değer
okundu. char tipi burada özel bir durum oluşturuyor: sizeof(char) her zaman 1’dir,
bu yüzden bir char *’ı artırmak, eleman cinsinden bir adım ile bayt cinsinden bir adımı
aynı anda karşılıyor. Bu, bir dizginin karakterleri arasında gezinirken sıkça karşılaşılan
bir durum.
// karakter_gezinme.c — char isaretcisinde bir artirma, bir sonraki karaktere tasir
#include <stdio.h>
int main(void) {
char dizgi[] = "abcd";
char *harf = dizgi;
printf("%s %c\n", "ilk karakter", *harf);
harf++;
printf("%s %c\n", "bir artirmadan sonraki karakter", *harf);
char *son_harf = &dizgi[3];
printf("%s %d\n", "son harf eksi ilk harf", (int)(son_harf - dizgi));
printf("%s %c\n", "son karakterin degeri", *son_harf);
return 0;
}
ilk karakter a bir artirmadan sonraki karakter b son harf eksi ilk harf 3 son karakterin degeri d
IS7. harf++ çağrısı dizgi[0]’dan dizgi[1]’e geçiyor; okunan değer 'a'’dan
'b'’ye değişiyor. son_harf - dizgi farkı 3 veriyor — dizgi[3], dizinin
başlangıcından üç eleman uzakta. Bu fark, int dizisindeki orta - ilk örneğiyle aynı
kuralı izliyor: sonuç eleman cinsinden. Farkın char için aynı zamanda bayt cinsinden bir
uzaklığa da eşit olması, sizeof(char)’ın 1 olmasının doğrudan sonucu — bu, kuralın
bir istisnası değil, kuralın kendisinin char özelinde aldığı biçim.
İki Boyutlu Dizide Satır Adımı
Buraya kadarki üç örnek tek boyutlu diziler kullandı; artırma her zaman bir eleman
ilerledi. İki boyutlu bir dizide bu kural değişmiyor, ama “eleman” tanımı değişiyor:
int matris[3][4] üzerinde bir satır işaretçisi (int (*)[4]) tanımlandığında, bir
artırma tek bir int değil, tüm bir satırı (4 int) atlıyor.
// satir_adimi.c — iki boyutlu bir dizide satir isaretcisi tum satiri bir adim sayar
#include <stdio.h>
int main(void) {
int matris[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int (*satir)[4] = matris;
printf("%s %d\n", "ilk satirin ilk elemani", satir[0][0]);
satir++;
printf("%s %d\n", "bir artirmadan sonraki satirin ilk elemani", satir[0][0]);
printf("%s %d\n", "ayni satirin ucuncu elemani", satir[0][2]);
int *duz = &matris[0][0];
int *ikinci_satir_basi = &matris[1][0];
printf("%s %d\n", "ikinci satir basi eksi ilk eleman", (int)(ikinci_satir_basi - duz));
return 0;
}
ilk satirin ilk elemani 1 bir artirmadan sonraki satirin ilk elemani 5 ayni satirin ucuncu elemani 7 ikinci satir basi eksi ilk eleman 4
IS8. satir değişkeni int (*)[4] tipinde — “dört int’lik bir dizinin
adresi” tipini taşıyor, düz bir int * değil. satir++ çağrıldığında işaretçi bir
sonraki satıra geçiyor: satir[0][0] değeri 1’den 5’e atlıyor, yani matris’in
ikinci satırının başına. Bu, artirma.c’deki p++’ın bir int kadar ilerlemesiyle aynı
kuralın farklı bir tip üzerindeki görünümü: adım her zaman işaret edilen tipin
boyutu kadardır, burada o tip tek bir int değil dört int’lik bir satır. Düz bir
int * (duz) ile ikinci satırın başı arasındaki fark 4 çıkıyor — bu da int matris[3][4]’ün bir işleve geçtiğinde neden int ** değil int (*)[4]’e bozunduğunu
doğruluyor: satır boyutu (4) tipin bir parçası olarak taşınıyor, aksi hâlde satir++
doğru elemana ilerleyemezdi.
Dinamik Ayrılmış Bir Dizide Aynı Kural
Buraya kadarki üç örnek yerel (dil sınıfı) dizilerde çalıştı. Aynı aritmetik kuralları,
dinamik olarak ayrılmış bir blok üzerinde de birebir geçerli — tip boyutuna göre
ilerleme, işaretçinin dizinin nereden geldiğini bilmesini gerektirmiyor.
// dinamik_artirma.c — dinamik ayrilan bir blokta isaretci aritmetigi ayni kurali izler
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int uzunluk = 4;
int *dizi = malloc((size_t)uzunluk * sizeof *dizi);
if (dizi == NULL) {
printf("%s\n", "ayirma basarisiz");
return 1;
}
for (int i = 0; i < uzunluk; i++) {
dizi[i] = (i + 1) * 10;
}
int *p = dizi;
printf("%s %d\n", "ilk eleman", *p);
p += 2;
printf("%s %d\n", "iki artirmadan sonraki eleman", *p);
int *son = dizi + uzunluk - 1;
printf("%s %d\n", "son eksi ilk", (int)(son - dizi));
free(dizi);
dizi = NULL;
return 0;
}
ilk eleman 10 iki artirmadan sonraki eleman 30 son eksi ilk 3
IS9. dizi, malloc ile ayrılmış bir blok, ama p += 2 çağrısı artirma.c
örneğindeki yerel diziyle tamamen aynı sonucu üretiyor: iki eleman atlıyor, 10’dan
30’a. son - dizi farkı 3 — fark_ve_sira.c’deki orta - ilk örneğiyle aynı
kural. Aradaki fark aritmetikte değil, sahiplikte: bu blok malloc ile isteniyor, bu
yüzden sınıf burada çağıran — dizi’yi ayıran main, onu kullanan da main, ve
bırakma yükümlülüğü de main’de kalıyor. Aritmetiğin kendisi bu sahiplik bilgisinden
tamamen bağımsız: bir işaretçinin gösterdiği blok dil sınıfında yerel bir dizi olsun ya
da çağıran sınıfında dinamik bir blok olsun, p++’ın ne kadar ilerleyeceği yalnız
tipe bakar, belleğin nereden geldiğine bakmaz.
İndeksleme ile Aritmetiğin Denkliği
Buraya kadar p[i] yazımı ve p + i yazımı ayrı ayrı kullanıldı, ama aralarındaki
ilişki hiç ölçülmedi. C’de köşeli parantez indekslemesi, aritmetik üzerine tanımlı bir
kısaltmadır: p[i] ifadesi tam olarak *(p + i) anlamına geliyor, ne bir bayt
farklı ne de ayrı bir kural.
// indeks_denkligi.c — p[i] yazimi ile *(p + i) yazimi ayni adresi gosterir
#include <stdio.h>
static int indeksle(int *p, int i) {
return p[i];
}
static int dolaylayarak_topla(int *p, int i) {
return *(p + i);
}
int main(void) {
int dizi[5] = {100, 200, 300, 400, 500};
int *p = dizi;
int indeks_sonucu = indeksle(p, 3);
int aritmetik_sonucu = dolaylayarak_topla(p, 3);
printf("%s %d\n", "p uc indeksi", indeks_sonucu);
printf("%s %d\n", "p artirilmis uc kere dolaylanmis", aritmetik_sonucu);
printf("%s %d\n", "iki yazim ayni deger mi veriyor", (int)(indeks_sonucu == aritmetik_sonucu));
int sifirinci_indeks = indeksle(p, 0);
int sifirinci_aritmetik = dolaylayarak_topla(p, 0);
printf("%s %d\n", "sifirinci indeks", sifirinci_indeks);
printf("%s %d\n", "dolaylanmis p ile ayni mi", (int)(sifirinci_indeks == sifirinci_aritmetik));
return 0;
}
p uc indeksi 400 p artirilmis uc kere dolaylanmis 400 iki yazim ayni deger mi veriyor 1 sifirinci indeks 100 dolaylanmis p ile ayni mi 1
IS10. p[3] ve *(p + 3) ifadeleri aynı değeri (400) veriyor ve eşitlik
karşılaştırması 1 çıkıyor — bu, iki yazımın derleyici için tamamen aynı işlemi ifade
ettiğini doğruluyor. p[0] ile *p arasındaki eşitlik de aynı kuralın bir uç durumu:
sıfırıncı indeks, hiç ilerlemeden doğrudan dolaylamakla özdeş. Bu denklik, bu dersteki
p++, p += n ve son - ilk ölçümlerinin hepsinin neden dizi[i] yazımıyla tutarlı
sonuç verdiğini açıklıyor: indeksleme, aritmetiğin üstüne kurulmuş bir gösterim
kolaylığı, ayrı bir mekanizma değil. Sınıf yine dil: hem p[3] hem *(p + 3) aynı
yerel diziyi gösteriyor, aradaki fark yalnız yazım tercihidir. Bu denklik aynı zamanda
şunu da açıklıyor: dizi[i] yazımı bile aslında bir işaretçi aritmetiğidir, çünkü dizi
bir işleve ya da bir işleç bağlamına girdiğinde ilk elemanının adresine bozunur ve
dizi[i], tam olarak *(dizi + i)’ye eşdeğer hâle gelir.
Sınırlayıcı Ölçüm
Bir işaretçi aritmetiği, yalnız aynı dizinin içinde ya da onun bir eleman ötesinde
tanımlıdır — dizinin son elemanının bir ötesini göstermek (döngü sonlandırma için)
geçerlidir, ama o adresi dolaylamak ya da diziyi iki eleman aşarak ilerletmek
tanımsız davranıştır. Yukarıdaki fark_ve_sira.c örneğinde gezici, döngü sırasında
en fazla son adresine kadar ilerliyor ve orada duruyor — dizinin sınırını aşan bir
artırma hiç koşturulmuyor. Bu sınır, derleyicinin denetlediği bir şey değil: p += 100
yazmak da derlenir, ama sonucu tanımsızdır ve bu ders böyle bir çağrıyı hiç içermiyor.
Aritmetiğin geçerliliği, kodu yazanın dizinin sınırlarını bilmesine bağlıdır — tip
sistemi bu sınırı taşımaz.
Aynı sınır, dizi gibi statik ayrılmış bir bloğun dışına çıkan işaretçi değerleri
için de geçerli: p - 1 ile dizinin başlangıcının bir öncesini göstermek, ya da
son + 1’den daha ileri gitmek, aynı tanımsız davranış sınıfına giriyor. Sınırın
“bir eleman ötesi” olarak tanımlanmasının nedeni tam olarak bu: döngü sonlandırma
koşulları (gezici < son gibi) o tek adrese güvenle ulaşabilsin diye standart bu
kadarını garanti ediyor, daha fazlasını değil.
Bu sınır, iki ilgisiz diziye ait işaretçiler arasında da geçerli: int a[4] ile
int b[4] ayrı ayrı tanımlanmış iki yerel diziyse, &b[0] - &a[0] derlenir ama sonucu
tanımsızdır — çıkarma işleci yalnız aynı dizi (ya da aynı dizinin bir eleman ötesi)
içindeki iki işaretçi için anlamlı bir eleman sayısı üretmeyi garanti ediyor; iki farklı
dizinin bellekte birbirine göre nerede durduğu derleyicinin kararına bırakılmış bir
ayrıntı ve program bu ayrıntıya güvenemez. Yukarıdaki fark_ve_sira.c ve
dinamik_artirma.c örneklerinin ikisi de bu kuralı dolaylı olarak gözetiyor: her ikisi
de farkı yalnız tek bir dizinin ya da tek bir bloğun içindeki iki işaretçi arasında
alıyor, a ile b gibi ilgisiz iki bloğu hiç karşılaştırmıyor.
Özet
- İşaretçi artırma ve azaltma (
p++,p--,p += n) her zaman tip boyutu kadar ilerler; adım sayısı eleman cinsindendir, bayt cinsinden değildir. - İki işaretçinin farkı (
son - ilk) aralarındaki eleman sayısını verir; adresin kendisi hiç basılmaz. - İki işaretçi
<,>gibi karşılaştırma işleçleriyle sıralanabilir — aynı dizi içindeyse bu sıralama dizideki konum sırasıyla eşleşir. - Dizi sınırının bir ötesini göstermek geçerlidir, ama o adresi dolaylamak tanımsız davranıştır; iki ilgisiz dizinin işaretçileri arasında fark almak da aynı sınıftandır — bu derste hiçbiri koşturulmadı.
p[i]yazımı*(p + i)’nin bir kısaltmasıdır; iki yazım her zaman aynı adresi gösterir ve aynı değeri döndürür.- Aritmetik kuralları, işaretçinin gösterdiği blok yerel bir dizi (
dil) olsun ya da dinamik olarak ayrılmış bir blok (çağıran) olsun değişmez; adım yalnız tipe bakar, belleğin kaynağına bakmaz. Bu yüzden dersin ölçtüğü bellek dil sınıfındadır: kuralı belleğin sahibi değil, dilin kendisi koyar.
Sonraki Adım
Bir dizi adı, çoğu bağlamda kendiliğinden ilk elemanının adresine bozunuyor —
int *ilk = dizi yazımı bunu zaten kullandı ama üzerinde durmadı. Bir sonraki ders,
Diziler ve İşaretçiler, bu bozunmanın ne zaman gerçekleştiğini, bir dizinin bir
işleve nasıl (ve neden uzunluk bilgisi olmadan) geçtiğini ve çağıran sınıfına ilk
adımı — dinamik olarak ayrılmış bir dizinin sahipliğini — ölçüyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.