C Programlama Dersi – XII

Bu yazıda öğrenecekleriniz:

Çok Boyutlu Diziler

Önceki derslerimizde dizileri görmüştük. Kısaca özetleyecek olursak, belirlediğimizsayıda değişkeni bir sıra içinde tutmamız, diziler sayesinde gerçekleşiyordu. Budersimizde, çok boyutlu dizileri inceleyip, ardından dinamik bellek konularınagireceğiz.

Şimdiye kadar gördüğümüz diziler, tek boyutluydu. Bütün elemanları tek boyutlubir yapıda saklıyorduk. Ancak dizilerin tek boyutlu olması gerekmez; istediğinizboyutta tanımlayabilirsiniz. Örneğin 3×4 bir matris için 2 boyutlu bir dizikullanırız. Ya da üç boyutlu Öklid uzayındaki x, y, z noktalarını saklamak için3 boyutlu bir diziyi tercih ederiz.

Hemen bir başka örnek verelim. 5 kişilik bir öğrenci grubu için 8 adet testuygulansın. Bunların sonuçlarını saklamak için 2 boyutlu bir dizi kullanalım:
#include<stdio.h>
int main( void )
{
// 5 adet ogrenci icin 8 adet sinavi
// temsil etmesi icin bir ogrenci tablosu
// olusturuyoruz. Bunun icin 5×8 bir matris
// ortaya çıkartilmasi gerekiyor.
int ogrenci_tablosu[ 5 ][ 8 ];
int i, j;
for( i = 0; i < 5; i++ ) {
for( j = 0; j < 8; j++ ) {
printf( “%d no.’lu ogrencinin “, ( i + 1 ) );
printf( “%d no.’lu sınavı> “, ( j + 1 ) );
// Tek boyutlu dizilerdeki gibi deger
// atiyoruz
scanf( “%d”, &ogrenci_tablosu[ i ][ j ] );
}
}

return 0;
}

Bu programı çalıştırıp, öğrencilere çeşitli değerler atadığımızı düşünelim. Bunugörsel bir şekle sokarsak, aşağıdaki gibi bir çizelge oluşur:

[2D Example]

Tabloya bakarsak, 1.öğrenci sınavlardan, 80, 76, 58, 90, 27, 60, 85 ve 95 puanalmış gözüküyor. Ya da 5.öğrencinin, 6.sınavından 67 aldığını anlıyoruz. Benzer şekilde diğer hücrelere gerekli değerler atanıp, ilgili öğrencinin sınav notları hafızada tutuluyor.

Çok Boyutlu Dizilere İlk Değer Atama

Çok boyutlu bir diziyi tanımlarken, eleman değerlerini atamak mümkündür. Aşağıdakiörneği inceleyelim:
int tablo[3][4] = { 8, 16, 9, 52, 3, 15, 27, 6, 14, 25, 2, 10 };

Diziyi tanımlarken, yukardaki gibi bir ilk değer atama yaparsanız, elemanlarındeğeri aşağıdaki gibi olur:

Satır 0 : 8 16 9 52
Satır 1 : 3 15 27 6
Satır 2 : 14 25 2 10

Çok boyutlu dizilerde ilk değer atama, tek boyutlu dizilerdekiyle aynıdır.Girdiğiniz değerler sırasıyla hücrelere atanır. Bunun nedeni de basittir.Bilgisayar, çok boyutlu dizileri sizin gibi düşünmez; dizi elemanlarını hafızada arka arkaya gelen bellek hücreleri olarak değerlendirir.

Çok boyutlu dizilerde ilk değer atama yapacaksanız, değerleri kümelendirmek iyibir yöntemdir; karmaşıklığı önler. Örneğin yukarıda yazmış olduğumuz ilk değeratama kodunu, aşağıdaki gibi de yazabiliriz:
int tablo[3][4] = { {8, 16, 9, 52}, {3, 15, 27, 6}, {14, 25, 2, 10} };

Farkedeceğiniz gibi elemanları dörderli üç gruba ayırdık. Bilgisayar açısındanbir şey değişmemiş olsa da, kodu okuyacak kişi açısından daha yararlı oldu.Peki ya dört adet olması gereken grubun elemanlarını, üç adet yazsaydık ya dabir-iki grubu hiç yazmasaydık n’olurdu? Deneyelim…
int tablo[3][4] = { {8, 16}, {3, 15, 27} };

Tek boyutlu dizilerde ilk değer ataması yaparken, eleman sayısından az değergirerseniz, kalan değerler 0 olarak kabul edilir. Aynı şey çok boyutlu dizileriçin de geçerlidir; olması gerektiği sayıda eleman ya da grup girilmezse, budeğerlerin hepsi 0 olarak kabul edilir. Yani üstte yazdığımız kodun ortaya çıkartacağı sonuç, şöyle olacaktır:

Satır 0 : 8 16 0 0
Satır 1 : 3 15 27 0
Satır 2 : 0 0 0 0

Belirtmediğimiz bütün elemanlar 0 değerini almıştır. Satır 2’ninse bütünelemanları direkt 0 olmuştur; çünkü grup tanımı hiç yapılmamıştır.

Fonksiyonlara 2 Boyutlu Dizileri Aktarmak

İki boyutlu bir diziyi fonksiyona parametre göndermek, tek boyutlu diziyigöndermekten farklı sayılmaz. Tek farkı dizinin iki boyutlu olduğunu belirtmemizve ikinci boyutun elemanını mutlaka yazmamızdır. Basit bir örnek yapalım;kendisine gönderilen iki boyutlu bir diziyi matris şeklinde yazan bir fonksiyonoluşturalım:
#include<stdio.h>
/* Parametre tanimlamasi yaparken, iki boyutlu dizinin
satir boyutunu girmemize gerek yoktur. Ancak sutun
boyutunu girmek gerekir.
*/
void matris_yazdir( int [ ][ 4 ], int );
int main( void )
{
// Ornek olmasi acisindan matrise keyfi
// degerler atiyoruz. Matrisimiz 3 satir
// ve 4 sutundan ( 3 x 4 ) olusuyor.
int matris[ 3 ][ 4 ] = {
{10, 15, 20, 25},
{30, 35, 40, 45},
{50, 55, 60, 65} };

// Matris elemanlarini yazdiran fonksiyonu
// cagriyoruz.
matris_yazdir( matris, 3 );

return 0;
}
void matris_yazdir( int dizi[ ][ 4 ], int satir_sayisi )
{
int i, j;
for( i = 0; i < satir_sayisi; i++ ) {
for( j = 0; j < 4; j++ ) {
printf( “%d “, dizi[ i ][ j ] );
}
printf( “\n” );
}
}

Kod içersinde bulunan yorumlar, iki boyutlu dizilerin fonksiyonlara nasılaktarıldığını göstermeye yetecektir. Yine de bir kez daha tekrar edelim…Fonksiyonu tanımlarken, çok boyutlu dizinin ilk boyutunu yazmak zorunda değilsiniz.Bizim örneğimizde int dizi[ ][ 4 ] şeklinde belirtmemizbundan kaynaklanıyor. Şayet 7 x 6 x 4 boyutlarında dizilerinkullanılacağı bir fonksiyon yazsaydık tanımlamamızı int dizi[ ][ 6 ][ 4 ] olarak değiştirmemizgerekirdi. Kısacası fonksiyonu tanımlarken dizi boyutlarına dair ilk değeriyazmamakta serbestsiniz; ancak diğer boyutların yazılması zorunlu! Bunun yararını merak ederseniz, sütun sayısı 4 olan her türlü matrisi bu fonksiyonagönderebileceğinizi hatırlatmak isterim. Yani fonksiyon her boyutta matrisialabilir, tabii sütun sayısı 4 olduğu sürece…

2 Boyutlu Dizilerin Hafıza Yerleşimi

Dizilerin çok boyutlu olması sizi yanıltmasın, bilgisayar hafızası tek boyutludur. İster tek boyutlu bir dizi, ister iki boyut ya da isterseniz 10 boyutlu bir diziiçersinde bulunan elemanlar, birbiri peşi sıra gelen bellek hücrelerinde tutulur. İki boyutlu bir dizide bulunan elemanların, hafızada nasıl yerleştirildiğiniaşağıdaki grafikte görebilirsiniz.

[2D Memory Layout]

Görüldüğü gibi elemanların hepsi sırayla yerleştirilmiştir. Bir satırın bittiğinoktada ikinci satırın elemanları devreye girer. Kapsamlı bir örnekle hafızayerleşimini ele alalım:
#include<stdio.h>
void satir_goster( int satir[ ] );
int main( void )
{
int tablo[5][4] = {
{4, 3, 2, 1},
{1, 2, 3, 4},
{5, 6, 7, 8},
{2, 5, 7, 9},
{0, 5, 9, 0} };
int i, j;

// Dizinin baslangic adresini yazdiriyoruz
printf( “2 boyutlu tablo %p adresinden başlar\n\n”, tablo );

// Tablo icersinde bulunan dizi elemanlarinin adreslerini
// ve degerlerini yazdiriyoruz.
printf( “Tablo elemanları ve hafıza adresleri:\n”);
for( i = 0; i < 5; i++ ) {
for( j = 0; j < 4; j++ ) {
printf( “%d (%p) “, tablo[i][j], &tablo[i][j] );
}
printf( “\n” );
}

// Cok boyutlu diziler birden fazla dizinin toplami olarak
// dusunulebilir ve her satir tek boyutlu bir dizi olarak
// ele alinabilir. Once her satirin baslangic adresini
// gosteriyoruz. Sonra satirlari tek boyutlu dizi seklinde
// satir_goster( ) fonksiyonuna gonderiyoruz.
printf( “\nTablo satırlarının başlangıç adresleri: \n”);
for( i = 0; i < 5; i++ )
printf( “tablo[%d]’nin başlangıç adresi %p\n”, i, tablo[i] );

printf( “\nsatir_goster( ) fonksiyonuyla, ”
“tablo elemanları ve hafıza adresleri:\n”);
for( i = 0; i < 5; i++ )
satir_goster( tablo[i] );
}
// Kendisine gonderilen tek boyutlu bir dizinin
// elemanlarini yazdirir.
void satir_goster( int satir[ ] )
{
int i;
for( i = 0; i < 4; i++ ) {
printf( “%d (%p) “, satir[i], &satir[i] );
}
printf( “\n” );
}

Örnekle ilgili en çok dikkat edilmesi gereken nokta, çok boyutlu dizilerinesasında, tek boyutlu dizilerden oluşmuş bir bütün olduğudur. Tablo isimli 2boyutlu dizimiz 5 adet satırdan oluşur ve bu satırların her biri kendi başınabir dizidir. Eğer tablo[2] derseniz bu üçüncü satırı temsil eden bir diziyi ifadeeder. satir_goster( ) fonksiyonunu ele alalım. Esasında fonksiyoniçersinde satır diye bir kavramın olmadığını söyleyebiliriz. Bütün olan bitenfonksiyona tek boyutlu bir dizi gönderilmesidir ve fonksiyon bu dizinin elemanlarını yazar.

Dizi elemanlarının hafızadaki ardışık yerleşimi bize başka imkanlar da sunar. İkiboyutlu bir diziyi bir hamlede, tek boyutlu bir diziye dönüştürmek bunlardanbiridir.
#include<stdio.h>
int main( void )
{
int i;
int tablo[5][4] = {
{4, 3, 2, 1},
{1, 2, 3, 4},
{5, 6, 7, 8},
{2, 5, 7, 9},
{0, 5, 9, 0} };

// Cok boyutlu dizinin baslangic
// adresini bir pointer’a atiyoruz.
int *p = tablo[0];

// p isimli pointer’i tek boyutlu
// bir dizi gibi kullanabiliriz.
// Ayni zamanda p uzerinde yapacagimiz
// degisikler, tablo’yu da etkiler.
for( i = 0; i < 5*4; i++ )
printf( “%d\n”, p[i] );

return 0;
}

Daha önce sıralama konusunu işlemiştik. Ancak bunu iki boyutlu dizilerde nasılyapacağımızı henüz görmedik. Aslında görmemize de gerek yok! İki boyutlu birdiziyi yukardaki gibi tek boyuta indirin ve sonrasında sıralayın. Çok boyutludizileri, tek boyuta indirmemizin ufak bir faydası…

Pointer Dizileri

Çok boyutlu dizilerin tek boyutlu dizilerin bir bileşimi olduğundan bahsetmiştik. Şimdi anlatacağımız konuda çok farklı değil. Dizilerin, adresi göstermeye yarayanPointer’lardan pek farklı olmadığını zaten biliyorsunuz. Şimdi de pointer dizilerinigöreceğiz. Yani adres gösteren işaretçi saklayan dizileri…
#include<stdio.h>
int main( void )
{
int i, j;

// Dizi isimleri keyfi secilmistir.
// alfa, beta, gama gibi baska isimler de
// verebilirdik.
int Kanada[8];
int ABD[8];
int Meksika[8];
int Rusya[8];
int Japonya[8];

// Bir pointer dizisi tanimliyoruz.
int *tablo[5];
// Yukarda tanimlanan dizilerin adreslerini
// tablo’ya aktiriyoruz.
tablo[0] = Kanada;
tablo[1] = ABD;
tablo[2] = Meksika;
tablo[3] = Rusya;
tablo[4] = Japonya;

// Tablo elemanlarinin adreslerini gosteriyor
// gibi gozukse de, gosterilen adresler Kanada,
// ABD, Meksika, Rusya ve Japonya dizilerinin
// eleman adresleridir.
for( i = 0; i < 5; i++ ) {
for( j = 0 ; j < 8; j++ )
printf( “%p\n”, &tablo[i][j] );
}
return 0;
}

Ülke isimlerini verdiğimiz 5 adet dizi tanımladık. Bu dizileri daha sonra tabloyasırayla atadık. Artık her diziyle tek tek uğraşmak yerine tek bir diziden bütünülkelere ulaşmak mümkün hâle gelmiştir. İki boyutlu tablo isimli matriseatamasını yaptığımız şey değer veya bir eleman değildir; dizilerin başlangıçadresleridir. Bu yüzden tablo dizisi içersinde yapacağımız herhangibir değişiklik orijinal diziyi de (örneğin Meksika) değiştirir.

Atama işlemini aşağıdaki gibi tek seferde de yapabilirdik:
int *tablo[ ] = { Kanada, ABD, Meksika, Rusya, Japonya };

Şimdi de bir pointer dizisini fonksiyonlara nasıl argüman olarak göndereceğimizebakalım.
#include<stdio.h>
void adresleri_goster( int *[ ] );
int main( void )
{
int Kanada[8];
int ABD[8];
int Meksika[8];
int Rusya[8];
int Japonya[8];

int *tablo[ ] = { Kanada, ABD, Meksika, Rusya, Japonya };

// Adresleri gostermesi icin adresleri_goster( )
// fonksiyonunu cagriyoruz.
adresleri_goster( tablo );

return 0;
}
void adresleri_goster( int *dizi[ ] )
{
int i, j;
for( i = 0; i < 5; i++ ) {
for( j = 0 ; j < 8; j++ )
printf( “%p\n”, &dizi[ i ][ j ] );
}
}

Dinamik Bellek Yönetimi

Dizileri etkin bir biçimde kullanmayı öğrendiğinizi ya da öğreneceğinizi umuyorum.Ancak dizilerle ilgili işlememiz gereken son bir konu var: Dinamik BellekYönetimi…

Şimdiye kadar yazdığımız programlarda kaç eleman olacağı önceden belliydi. Yanisınıf listesiyle ilgili bir program yazacaksak, sınıfın kaç kişi olduğunubiliyormuşuz gibi davranıyorduk. Programın en başında kaç elemanlık alanaihtiyacımız varsa, o kadar yer ayırıyorduk. Ama bu gerçek dünyada karşımıza çıkacakproblemler için yeterli bir yaklaşım değildir. Örneğin bir sınıfta 100 öğrencivarken, diğer bir sınıfta 50 öğrenci olabilir ve siz her ortamda çalışsın diye200 kişilik bir üst sınır koyamazsınız. Bu, hem hafızanın verimsiz kullanılmasınayol açar; hem de karma eğitimlerin yapıldığı bazı fakültelerde sayı yetmeyebilir.Statik bir şekilde dizi tanımlayarak bu sorunların üstesinden gelemezsiniz. Çözümdinamik bellek yönetimindedir.

Dinamik bellek yönetiminde, dizilerin boyutları önceden belirlenmez. Programakışında dizi boyutunu ayarlarız ve gereken bellek miktarı, program çalışırkentahsis edilir. Dinamik bellek tahsisi için calloc( ) ve malloc( ) olmak üzere iki önemli fonksiyonumuz vardır. Bellekteyer ayrılmasını bu fonksiyonlarla sağlarız. Her iki fonksiyon da stdlib kütüphanesinde bulunur. Bu yüzden fonksiyonlardan herhangi birini kullanacağınızzaman, programın başına #include<stdlib.h> yazılması gerekir.

calloc( ) fonksiyonu aşağıdaki gibi kullanılır:
isaretci_adi = calloc( eleman_sayisi, her_elemanin_boyutu );

calloc( ) fonksiyonu eleman sayısını, eleman boyutuyla çarparakhafızada gereken bellek alanını ayırır. Dinamik oluşturduğunuz dizi içersindeki herelemana, otomatik olarak ilk değer 0 atanır.

malloc( ) fonksiyonu, calloc( ) gibi dinamikbellek ayrımı için kullanılır. calloc( ) fonksiyonundan farklı olarak ilk değer ataması yapmaz. Kullanımıysa aşağıdaki gibidir:
isaretci_adi = malloc( eleman_sayisi * her_elemanin_boyutu );

Bu kadar konuşmadan sonra işi pratiğe dökelim ve dinamik bellekle ilgili ilkprogramımızı yazalım:
#include<stdio.h>
#include<stdlib.h>
int main( void )
{
// Dinamik bir dizi ortaya çıkartmak icin
// pointer kullaniriz.
int *dizi;

// Dizimizin kac elemanli olacagini
// eleman_sayisi isimli degiskende
// tutuyoruz.
int eleman_sayisi;
int i;

// Kullanicidan eleman sayisini girmesini
// istiyoruz.
printf( “Eleman sayısını giriniz> “);
scanf( “%d”, &eleman_sayisi );

// calloc( ) fonksiyonuyla dinamik olarak
// dizimizi istedigimiz boyutta ortaya çıkartiyoruz.
dizi = calloc( eleman_sayisi, sizeof( int ) );

// Ornek olmasi acisindan dizinin elemanlarini
// ekrana yazdiriliyor. Dizilerde yapabildiginiz
// her seyi hicbir fark olmaksizin yapabilirsiniz.
for( i = 0; i < eleman_sayisi; i++ )
printf( “%d\n”, dizi[i] );

// Dinamik olan diziyi kullandiktan ve isinizi
// tamamladiktan sonra free fonksiyonunu kullanip
// hafizadan temizlemelisiniz.
free( dizi );

return 0;
}

Yazdığınız programların bir süre sonra bilgisayar belleğini korkunç bir şekildeişgal etmesini istemiyorsanız, free( ) fonksiyonunu kullanmanızgerekmektedir. Gelişmiş programlama dillerinde ( örneğin, Java, C#, vb… )kullanılmayan nesnelerin temizlenmesi otomatik olarak çöp toplayıcılarla( Garbage Collector ) yapılmaktadır. Ne yazık ki C programlamadili için bir çöp toplayıcı yoktur ve iyi programcıyla, kötü programcı buradakendisini belli eder.

Programınızı bir kereliğine çalıştırıyorsanız ya da yazdığınız program çok ufaksa,boş yere tüketilen bellek miktarını farketmeyebilirsiniz. Ancak büyük boyutta vekapsamlı bir program söz konusuysa, efektif bellek yönetiminin ne kadar önemliolduğunu daha iyi anlarsınız. Gereksiz tüketilen bellekten kaçınmak gerekmektedir.Bunun için fazla bir şey yapmanız gerekmez; calloc( ) fonksiyonuylatahsis ettiğiniz alanı, işiniz bittikten sonra free( ) fonksiyonuyla boşaltmanız yeterlidir. Konu önemli olduğu için tekrar ediyorum;artık kullanmadığınız bir dinamik dizi söz konusuysa onu free( ) fonksiyonuylakaldırılabilir hâle getirmelisiniz!

Az evvel calloc( ) ile yazdığımız programın aynısını şimdi de malloc( ) fonksiyonunu kullanarak yazalım:
#include<stdio.h>
#include<stdlib.h>
int main( void )
{
// Dinamik bir dizi ortaya çıkartmak icin
// pointer kullaniriz.
int *dizi;
// Dizimizin kac elemanli olacagini
// eleman_sayisi isimli degiskende
// tutuyoruz.
int eleman_sayisi;
int i;

printf( “Eleman sayısını giriniz> “);
scanf( “%d”, &eleman_sayisi );

// malloc( ) fonksiyonuyla dinamik olarak
// dizimizi istedigimiz boyutta ortaya çıkartiyoruz.
dizi = malloc( eleman_sayisi * sizeof( int ) );

for( i = 0; i < eleman_sayisi; i++ )
printf( “%d\n”, dizi[i] );

// Dinamik olan diziyi kullandiktan ve isinizi
// tamamladiktan sonra free fonksiyonunu kullanip
// hafizadan temizlemelisiniz.
free( dizi );

return 0;
}

Hafıza alanı ayırırken bazen bir problem çıkabilir. Örneğin bellekte yeterlialan olmayabilir ya da benzeri bir sıkıntı olmuştur. Bu tarz problemlerin sıkolacağını düşünmeyin. Ancak hafızanın gerçekten ayrılıp ayrılmadığını kontrol edip,işinizi garantiye almak isterseniz, aşağıdaki yöntemi kullanabilirsiniz:
dizi = calloc( eleman_sayisi, sizeof( int ) );
// Eger hafiza dolmussa dizi pointer’i NULL’a
// esit olacak ve asagidaki hata mesaji cikacaktir.
if( dizi == NULL )
printf( “Yetersiz bellek!\n” );

Dinamik hafıza kullanarak dizi ortaya çıkartmayı gördük. Ancak bu diziler tek boyutludizilerdi. Daha önce pointer işaret eden pointer’ları görmüştük. Şimdi onları kullanarak dinamik çok boyutlu dizi oluşturacağız:
#include<stdio.h>
#include<stdlib.h>
int main( void )
{
int **matris;
int satir_sayisi, sutun_sayisi;
int i, j;
printf( “Satır sayısı giriniz> ” );
scanf( “%d”, &satir_sayisi );
printf( “Sütun sayısı giriniz> ” );
scanf( “%d”, &sutun_sayisi );

// Once satir sayisina gore hafizada yer ayiriyoruz.
// Eger gerekli miktar yoksa, uyari veriliyor.
matris = (int **)malloc( satir_sayisi * sizeof(int) );
if( matris == NULL )
printf( “Yetersiz bellek!” );

// Daha sonra her satirda, sutun sayisi kadar hucrenin
// ayrilmasini sagliyoruz.
for( i = 0; i < satir_sayisi; i++ ) {
matris[i] = malloc( sutun_sayisi * sizeof(int) );
if( matris[i] == NULL )
printf( “Yetersiz bellek!” );
}

// Ornek olmasi acisindan matris degerleri
// gosteriliyor. Dizilerde yaptiginiz butun
// islemleri burada da yapabilirsiniz.
for( i = 0; i < satir_sayisi; i++ ) {
for( j = 0; j < sutun_sayisi; j++ )
printf( “%d “, matris[i][j] );
printf( “\n” );
}

// Bu noktada matris ile isimiz bittiginden
// hafizayi bosaltmamiz gerekiyor. Oncelikle
// satirlari bosaltiyoruz.
for( i = 0; i < satir_sayisi; i++ ) {
free( matris[i] );
}
// Satirlar bosaldiktan sonra, matrisin
// bos oldugunu isaretliyoruz.
free( matris );

return 0;
}

Yukardaki örnek karmaşık gelebilir; tek seferde çözemeyebilirsiniz. Ancak bir ikikez üzerinden geçerseniz, temel yapının aklınıza yatacağını düşünüyorum. Kodunkoyu yazılmış yerlerini öğrendiğiniz takdirde, sorun kalmayacaktır.

Örnek Sorular

Soru 1: Kendisine gönderilen iki diziyi birleştirip geriye tek bir dizidöndüren fonksiyonu yazınız.

Cevap için tıklayınız…

Soru 2: Sol aşağıda bulunan 4×4 boyutundaki matrisi saat yönünde 90o döndürecek fonksiyonu yazınız. ( Sol matris döndürüldüğü zaman sağ matrise eşit olmalıdır. )

12 34 22 98
88 54 67 11
90 91 92 93
38 39 40 41

>>> 38 90 88 12
39 91 54 34
40 92 67 22
41 93 11 98

Cevap için tıklayınız…