İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat