İçeriğe geç
academia.sh

Ders 03 / 15

Diziler ve İşaretçiler

Bir dizi adının bir işleve geçerken ilk elemanının adresine bozunması, uzunluk bilgisinin bu bozunmada neden kaybolduğu ve dinamik olarak ayrılmış bir dizinin sahipliğinin çağırana geçtiği.

Yayın Güncelleme

Bu derste

  • Bir dizi adının bir işleve geçerken ilk elemanının adresine bozunduğunu örnekle gösterir
  • Bozunma sonucu kaybolan uzunluk bilgisinin neden ayrı bir parametreyle taşınması gerektiğini açıklar
  • Dinamik olarak ayrılmış bir dizinin sahipliğinin çağırana geçtiğini sayaçla ölçer
İçindekiler

İki ders önce çekirdekteki ayir ve birak satırı bir bloğu say_ayir ile ayırıp say_birak ile bıraktı; sınıfı çağırandı çünkü belleği isteyen taraf onu bırakmakla yükümlüydü. Bu ders o satırı derinleştiriyor ve önce dizi adlarının işlevlere nasıl geçtiğini ölçüyor — çünkü dinamik bir diziyi doğru bırakabilmek, önce onun bir işleve ne olarak ulaştığını bilmeyi gerektiriyor.

Bozunma: Dizi Adı İşleve Geçerken

// bozunma.c — bir dizi adi islev parametresinde ilk elemanin adresine bozunur
#include <stdio.h>

static void goster(int *p, int uzunluk) {
    int toplam = 0;
    for (int i = 0; i < uzunluk; i++) {
        toplam += p[i];
    }
    printf("%s %d\n", "islev icinde toplam", toplam);
    printf("%s %d\n", "islev icinde ilk eleman", *p);
}

int main(void) {
    int dizi[4] = {2, 4, 6, 8};

    printf("%s %d\n", "dizi disinda ilk eleman", *dizi);
    printf("%s %d\n", "dizi ile ilk elemanin adresi esit mi", (int)(dizi == &dizi[0]));

    goster(dizi, 4);

    int *isaretci = dizi;
    printf("%s %d\n", "isaretciye atanan dizi ile ilk eleman esit mi", (int)(isaretci == &dizi[0]));
    return 0;
}
dizi disinda ilk eleman 2
dizi ile ilk elemanin adresi esit mi 1
islev icinde toplam 20
islev icinde ilk eleman 2
isaretciye atanan dizi ile ilk eleman esit mi 1

IS11. dizi == &dizi[0] karşılaştırması 1 veriyor: bir dizi adı kullanıldığında, o ad ilk elemanının adresiyle aynı değeri taşıyor. goster(dizi, 4) çağrısında dizi, işlevin int *p parametresine bu adres olarak geçiyor — işlev içinde p[i] yazımı dizi[i] ile aynı elemanlara ulaşıyor ve toplam 20 çıkıyor (2+4+6+8). isaretci = dizi ataması da aynı şeyi gösteriyor: bir dizi adı bir işaretçi değişkenine atandığında, dizinin kendisi değil, ilk elemanının adresi kopyalanıyor. Bu olguya bozunma deniyor: dizi adı, bir işlev parametresinde ya da bir işaretçiye atamada kendi tipini (dizi) kaybedip bir işaretçi tipine dönüşüyor.

Kaybolan Uzunluk

// uzunluk_kaybi.c — bozunma, dizinin eleman sayisi bilgisini tasimaz
#include <stdio.h>

static int topla(int *p, int uzunluk) {
    int toplam = 0;
    for (int i = 0; i < uzunluk; i++) {
        toplam += p[i];
    }
    return toplam;
}

int main(void) {
    int kucuk[3] = {1, 1, 1};
    int buyuk[7] = {1, 1, 1, 1, 1, 1, 1};

    printf("%s %d\n", "kucuk dizi toplami", topla(kucuk, 3));
    printf("%s %d\n", "buyuk dizi toplami", topla(buyuk, 7));
    printf("%s %d\n", "buyuk diziye yanlis uzunluk verilirse toplam", topla(buyuk, 3));
    return 0;
}
kucuk dizi toplami 3
buyuk dizi toplami 7
buyuk diziye yanlis uzunluk verilirse toplam 3

IS12. topla işlevi, kendisine geçen işaretçiden dizinin kaç eleman uzunluğunda olduğunu hiçbir şekilde çıkaramıyor — bu yüzden uzunluk ayrı bir parametre olarak taşınıyor. Son çağrıda buyuk dizisi (7 eleman) işleve geçiyor ama uzunluk olarak 3 veriliyor; işlev bu yanlış bilgiye güveniyor ve yalnız ilk üç elemanı topluyor, sonuç 3 çıkıyor — dizinin gerçek boyutuyla hiçbir ilgisi yok. Bu, bozunmanın sınırlayıcı sonucudur: bir dizi bir işleve geçtiğinde, uzunluk bilgisi kaynakta ayrı bir değer olarak elle taşınmak zorundadır; işaretçinin kendisinden geri kazanılamaz.

Uzunluğu ayrı bir parametreyle taşımak, C’de karşılaşılan tek yöntem değil. Bir başka yaygın çözüm, dizinin kendi içine bir sonlandırıcı değer yerleştirmektir — dizgilerin sonundaki '\0' tam olarak bu işi görüyor: bir dizgi üzerinde gezinen bir döngü, uzunluğu ayrı bir parametreden değil, sonlandırıcıya ulaşana kadar ilerleyerek öğreniyor. İki yöntemin de ortak noktası şu: bozunma sonucu kaybolan bilgi, kaynakta başka bir biçimde geri kazanılmak zorunda — ya bir sayı olarak yanında taşınır, ya da dizinin içine gömülen bir işaretle bulunur. Üçüncü bir yol yok; işaretçinin kendisi bu bilgiyi hiçbir zaman taşımaz.

Dinamik Bir Dizinin Sahipliği

// dinamik_dizi.c — ayrilan bir dizinin sahibi onu isteyen taraftir
#include <stdio.h>
#include <stdlib.h>

static int *dizi_olustur(int uzunluk, int baslangic) {
    int *dizi = malloc((size_t)uzunluk * sizeof *dizi);
    if (dizi == NULL) {
        return NULL;
    }
    for (int i = 0; i < uzunluk; i++) {
        dizi[i] = baslangic + i;
    }
    return dizi;
}

int main(void) {
    int uzunluk = 5;
    int *dizi = dizi_olustur(uzunluk, 10);

    if (dizi == NULL) {
        printf("%s\n", "ayirma basarisiz");
        return 1;
    }

    int toplam = 0;
    for (int i = 0; i < uzunluk; i++) {
        toplam += dizi[i];
    }
    printf("%s %d\n", "dinamik dizi toplami", toplam);
    printf("%s %d\n", "son eleman", dizi[uzunluk - 1]);

    free(dizi);
    dizi = NULL;
    printf("%s %d\n", "birakildiktan sonra bos isaretci mi", (int)(dizi == NULL));
    return 0;
}
dinamik dizi toplami 60
son eleman 14
birakildiktan sonra bos isaretci mi 1

IS13. dizi_olustur bir dizi ayırıyor ama kendisi hiç bırakmıyor — bloğu main’e döndürüyor. main içindeki toplam 60 (10+11+12+13+14) ve son eleman 14, dizinin doğru dolduğunu gösteriyor. Sahiplik burada işlev sınırını aşarak geçiyor: dizi_olustur belleği ayırıyor, ama onu isteyen ve kullanan main bırakmakla yükümlü — bu yüzden sınıf çağıran. free(dizi) çağrısından hemen sonra dizi = NULL atanıyor ve dizi == NULL karşılaştırması 1 veriyor; bu, bırakılmış bir işaretçinin bir sonraki dersin konusu olan tehlikeli durumunu önlüyor.

Yanlış Uzunluk Sınırın Neresine Kadar Sessiz Kalır

uzunluk_kaybi.c örneği yanlış bir uzunluğun eksik bir toplam verdiğini gösterdi, ama bu yalnız “az sayma” durumuydu. Yanlış uzunluk, dizinin gerçek sınırını aşan bir değer olduğunda ne olur? Aşağıdaki örnek bu soruyu, sınırın içinde kalan ve sınırı aşan iki çağrıyı yan yana koyarak ölçüyor.

// sinir_asimi.c — dogru uzunluk sinirin icinde kalir, yanlis uzunluk disina tasabilir
#include <stdio.h>

static int en_buyuk(const int *p, int uzunluk) {
    int buyuk = p[0];
    for (int i = 1; i < uzunluk; i++) {
        if (p[i] > buyuk) {
            buyuk = p[i];
        }
    }
    return buyuk;
}

int main(void) {
    int dizi[4] = {3, 2, 9, 7};

    printf("%s %d\n", "dogru uzunlukla en buyuk", en_buyuk(dizi, 4));
    printf("%s %d\n", "eksik uzunlukla en buyuk", en_buyuk(dizi, 2));
    return 0;
}
dogru uzunlukla en buyuk 9
eksik uzunlukla en buyuk 3

IS14. Doğru uzunlukla (4) çağrıldığında en_buyuk dizinin tamamını tarıyor ve 9’u buluyor. Eksik bir uzunlukla (2) çağrıldığında ise yalnız ilk iki elemana bakıyor ve 3’ü döndürüyor — 9 değeri dizide var olmasına rağmen üçüncü sırada kaldığı için hiç görülmüyor. Bu ders kasıtlı olarak yalnız eksik uzunluk çağrısını içeriyor; gerçek uzunluğu aşan bir değer verilmesi (en_buyuk(dizi, 6) gibi) dizinin sonundan sonraki belleği okumaya çalışır ve bu tanımsız davranıştır — bu yüzden burada hiç koşturulmuyor. İki durumun ortak noktası şu: en_buyuk işlevi kendisine geçen uzunluk değerine koşulsuz güveniyor; ne eksik ne fazla bir değeri kendi başına tespit edemiyor, çünkü elindeki tek bilgi bir başlangıç adresi ve bir sayı.

Uzunluğu da Birlikte Döndürmek

dizi_olustur uzunluğu main’in zaten bildiği bir değer olarak aldı ve kendisi hiç hesaplamadı. Bir işlev, ayırdığı dizinin uzunluğunu kendisi belirliyorsa — örneğin bir filtreleme sonucu, girişten daha kısa bir dizi üretiyorsa — bu uzunluğu çağırana nasıl bildirir? İşaretçinin kendisi bunu taşımadığına göre, tek yol bir çıkış parametresi.

// ciktisi_da_donen.c — ayiran islev, uzunlugu bir cikis parametresiyle geri bildirir
#include <stdio.h>
#include <stdlib.h>

static int *ciftleri_sec(const int *kaynak, int uzunluk, int *sonuc_uzunlugu) {
    int *sonuc = malloc((size_t)uzunluk * sizeof *sonuc);
    if (sonuc == NULL) {
        *sonuc_uzunlugu = 0;
        return NULL;
    }
    int sayac = 0;
    for (int i = 0; i < uzunluk; i++) {
        if (kaynak[i] % 2 == 0) {
            sonuc[sayac] = kaynak[i];
            sayac++;
        }
    }
    *sonuc_uzunlugu = sayac;
    return sonuc;
}

int main(void) {
    int kaynak[6] = {1, 2, 3, 4, 5, 6};
    int uzunluk = 0;

    int *ciftler = ciftleri_sec(kaynak, 6, &uzunluk);
    printf("%s %d\n", "secilen eleman sayisi", uzunluk);

    int toplam = 0;
    for (int i = 0; i < uzunluk; i++) {
        toplam += ciftler[i];
    }
    printf("%s %d\n", "secilenlerin toplami", toplam);

    free(ciftler);
    ciftler = NULL;
    return 0;
}
secilen eleman sayisi 3
secilenlerin toplami 12

IS15. ciftleri_sec kaynak dizinin en fazla uzunluk kadar eleman üretebileceğini biliyor, o yüzden bloğu o boyutta ayırıyor, ama gerçekte kaç elemanın çift olduğunu ancak taradıktan sonra öğreniyor — bu bilgiyi *sonuc_uzunlugu üzerinden çağırana geri yazıyor. main bu çağrıdan sonra hem yeni diziyi (ciftler) hem onun gerçek uzunluğunu (uzunluk, artık 3) elinde buluyor; toplam 12 (2+4+6) bu ikisinin doğru eşleştiğini kanıtlıyor. Sahiplik yine dizi_olustur örneğindeki gibi çağıranda: ciftleri_sec belleği ayırıyor ama hiç bırakmıyor, main onu kullanıp free ile bırakıyor. Bu örnek dizi_olustur’dan farklı olarak, uzunluğun girişten değil işlevin kendi hesabından geldiği durumu ölçüyor — bu yüzden uzunluk artık bir giriş parametresi değil, bir çıkış parametresi. sonuc_uzunlugu’nun kendisi bir int * olarak geçiyor — bu, işaretçilerin yalnız dizileri değil, tek bir değeri “çağırana geri yazmak” için de kullanılabildiğini gösteren ayrı bir kullanım biçimi.

İki Boyutlu Bir Dizinin Bozunması

Tek boyutlu bir dizi bir işleve geçtiğinde int *’a bozunuyordu. İki boyutlu bir dizi geçtiğinde bozunma bir düzey eksik işliyor: int matris[3][4] bir işlev parametresinde int ** olmaz, satır boyutu (4 eleman) tipin bir parçası olarak kalır ve int (*)[4] tipine bozunur.

// matris_bozunmasi.c — int matris[3][4] bir isleve int (*)[4] olarak gecer, int ** olmaz
#include <stdio.h>

static int satir_toplami(int satir[4]) {
    int toplam = 0;
    for (int i = 0; i < 4; i++) {
        toplam += satir[i];
    }
    return toplam;
}

static int tum_toplam(int (*matris)[4], int satir_sayisi) {
    int toplam = 0;
    for (int i = 0; i < satir_sayisi; i++) {
        toplam += satir_toplami(matris[i]);
    }
    return toplam;
}

int main(void) {
    int matris[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    printf("%s %d\n", "ilk satirin toplami", satir_toplami(matris[0]));
    printf("%s %d\n", "tum matrisin toplami", tum_toplam(matris, 3));
    return 0;
}
ilk satirin toplami 10
tum matrisin toplami 78

IS16. tum_toplam işlevinin parametresi int (*matris)[4] — “dört elemanlık bir dizinin adresi” tipini taşıyor. matris[i] yazımı burada *(matris + i) anlamına geliyor ve bu, i‘inci satırın ilk elemanının adresini veriyor; o adres satir_toplami‘ye bir kez daha bozunarak (bu kez int * olarak) geçiyor. ilk satirin toplami 10 (1+2+3+4), tum matrisin toplami 78 (1’den 12’ye kadar tüm elemanların toplamı) — iki katmanlı bozunmanın doğru elemanlara ulaştığını kanıtlıyor. Eğer parametre int **matris olarak yazılsaydı, derleyici bunu kabul etmezdi: int (*)[4] ile int ** aynı boyutta (bir adres) olsalar da farklı tiplerdir, çünkü ilki “satır boyutu 4 olan bir dizinin adresi” bilgisini taşırken ikincisi yalnız “bir işaretçiye işaretçi” demektir — bu bilgi kaybı, derleyicinin matris[i][j] erişimini doğru satıra yönlendirebilmesi için tam olarak gerekli olan bilgidir.

Sınırlayıcı Ölçüm

Bozunma yalnız tek düzeyde işliyor: bir int dizi[4] bir işleve geçtiğinde int * olur, ama bir int matris[3][4] geçtiğinde int ** olmaz — yukarıdaki matris_bozunmasi.c örneği bunu satır toplamları üzerinden zaten ölçtü. Bu ders çok boyutlu diziye burada duruyor; sabitlenen kural şu: bozunma her zaman bir adres üretir ve o adresin “kaç elemanlık bir bloğu gösterdiği” bilgisini asla taşımaz. Bu bilgi ya bir ayrı parametreyle (yukarıdaki uzunluk gibi) ya da bir sonlandırıcı değerle (dizgi sonundaki '\0' gibi) kaynakta açıkça taşınmalıdır. matris_bozunmasi.c’de bu bilgi üçüncü bir yoldan taşındı: satır boyutu (4) tipin kendisine (int (*)[4]) gömülü kaldığı için ayrı bir parametre gerekmedi, ama satır sayısı (3) yine de tum_toplam’a ayrı bir satir_sayisi parametresiyle geçirilmek zorunda kaldı — çünkü tip yalnız sütun boyutunu taşıyor, satır sayısını taşımıyor.

Özet

  • Bir dizi adı, bir işleve parametre olarak geçtiğinde ya da bir işaretçiye atandığında ilk elemanının adresine bozunur; dizi == &dizi[0] bu bozunmayı kanıtlıyor.
  • Bozunma, dizinin eleman sayısı bilgisini taşımaz — yanlış bir uzunluk parametresi verildiğinde işlev bunu fark edemiyor, yalnız verilen sayı kadar elemana erişiyor.
  • Dinamik olarak ayrılmış bir dizinin sahibi, onu isteyen ve kullanan taraftır (sınıf çağıran); ayıran işlev bloğu geri döndürüp bırakma yükümlülüğünü devredebilir.
  • Bir bloğu bıraktıktan hemen sonra işaretçiye NULL atamak, o işaretçinin artık geçerli bir bloğu göstermediğini kaynakta açıkça işaretliyor.
  • Çok elemanlı bir bloğun uzunluğu, işaretçinin kendisinden geri kazanılamaz; ayrı bir değer, bir çıkış parametresi ya da bir sonlandırıcı ile taşınmak zorundadır — uzunluk girişten geliyorsa giriş parametresi, işlev onu kendisi hesaplıyorsa çıkış parametresi kullanılır, ama sahiplik her iki durumda da çağıran’da kalır.
  • İki boyutlu bir dizi bir işleve geçtiğinde int (*)[N] tipine bozunur, int **’a değil — satır boyutu tipin bir parçası olarak taşınır, satır sayısı taşınmaz.

Bu dersteki üç yeni ölçüm de aynı gözleme farklı açılardan bağlanıyor: bir dizinin işaretçiye bozunması, dizinin kendisi hakkında hiçbir bilgi taşımıyor — ne uzunluğunu, ne satır sayısını, ne de nereye kadar okumanın güvenli olduğunu. Bu bilgi her zaman ayrı bir kanaldan gelmek zorunda: ya bir parametre olarak (en_buyuk’teki uzunluk), ya tipin kendisine gömülü olarak (matris_bozunmasi.c’deki int (*)[4]), ya da bir çıkış parametresi olarak (ciktisi_da_donen.c’deki sonuc_uzunlugu). Üçü de aynı sınırın farklı çözümleri; hiçbiri sınırı ortadan kaldırmıyor, yalnız onu görünür ve taşınabilir kılıyor.

Sonraki Adım

Önceki örnekte free(dizi) çağrısından hemen sonra dizi’ye NULL atandı — ama bu adım atlanırsa ne olur? Bir sonraki ders, Boş ve Sarkan İşaretçiler, bırakılmış bir bloğu hâlâ gösteren bir işaretçinin ne anlama geldiğini, bu durumun neden hiç koşturulmadığını ve NULL atamanın neden onu sahipsiz sınıfından çıkarıp denetlenebilir bir duruma getirdiğ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