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
NULLatamak, 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.