C Programlama Dersi – 11

Çok Boyutlu Diziler

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

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

Hemen bir başka örnek verelim. 5 kişilik bir öğrenci grubu için 8 adet test uygulansı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 5x8 bir matris 
	// yaratilmasi 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. Bunu gö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 puan almış 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ın değ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 iyi bir yöntemdir; karmaşıklığı önler. Örneğin yukarıda yazmış olduğumuz ilk değer atama 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ından bir ş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 da bir-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ğer girerseniz, kalan değerler 0 olarak kabul edilir. Aynı şey çok boyutlu diziler için de geçerlidir; olması gerektiği sayıda eleman ya da grup girilmezse, bu değerlerin hepsi 0 olarak kabul edilir. Yani üstte yazdığımız kodun yaratacağı 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ün elemanları 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 diziyi göndermekten farklı sayılmaz. Tek farkı dizinin iki boyutlu olduğunu belirtmemiz ve ikinci boyutun elemanını mutlaka yazmamızdır. Basit bir örnek yapalım; kendisine gönderilen iki boyutlu bir diziyi matris şeklinde yazan bir fonksiyon oluş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ıl aktarı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 belirtmemiz bundan kaynaklanıyor. Şayet 7 x 6 x 4 boyutlarında dizilerin kullanılacağı bir fonksiyon yazsaydık tanımlamamızıint dizi[ ][ 6 ][ 4 ] olarak değiştirmemiz gerekirdi. Kısacası fonksiyonu tanımlarken dizi boyutlarına dair ilk değeri yazmamakta 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 fonksiyona gönderebileceğinizi hatırlatmak isterim. Yani fonksiyon her boyutta matrisi alabilir, 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 dizi iç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ğini aş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ği noktada ikinci satırın elemanları devreye girer. Kapsamlı bir örnekle hafıza yerleş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şlarnn", 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 %pn", 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 dizilerin esasında, tek boyutlu dizilerden oluşmuş bir bütün olduğudur. Tablo isimli 2 boyutlu dizimiz 5 adet satırdan oluşur ve bu satırların her biri kendi başına bir dizidir. Eğer tablo[2] derseniz bu üçüncü satırı temsil eden bir diziyi ifade eder. satir_goster(  ) fonksiyonunu ele alalım. Esasında fonksiyon içersinde satır diye bir kavramın olmadığını söyleyebiliriz. Bütün olan biten fonksiyona 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. İki boyutlu bir diziyi bir hamlede, tek boyutlu bir diziye dönüştürmek bunlardan biridir.

#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( "%dn", p[i] );

	return 0;
}

Daha önce sıralama konusunu işlemiştik. Ancak bunu iki boyutlu dizilerde nasıl yapacağımızı henüz görmedik. Aslında görmemize de gerek yok! İki boyutlu bir diziyi yukardaki gibi tek boyuta indirin ve sonrasında sıralayın. Çok boyutlu dizileri, 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 yarayan Pointer’lardan pek farklı olmadığını zaten biliyorsunuz. Şimdi de pointer dizilerini gö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( "%pn", &tablo[i][j] );
	}
	return 0;
}

Ülke isimlerini verdiğimiz 5 adet dizi tanımladık. Bu dizileri daha sonra tabloya sı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 matrise ataması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 herhangi bir 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ğimize bakalı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( "%pn", &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 Bellek Yönetimi…

Şimdiye kadar yazdığımız programlarda kaç eleman olacağı önceden belliydi. Yani sınıf listesiyle ilgili bir program yazacaksak, sınıfın kaç kişi olduğunu biliyormuşuz gibi davranıyorduk. Programın en başında kaç elemanlık alana ihtiyacımız varsa, o kadar yer ayırıyorduk. Ama bu gerçek dünyada karşımıza çıkacak problemler için yeterli bir yaklaşım değildir. Örneğin bir sınıfta 100 öğrenci varken, diğer bir sınıfta 50 öğrenci olabilir ve siz her ortamda çalışsın diye 200 kişilik bir üst sınır koyamazsınız. Bu, hem hafızanın verimsiz kullanılmasına yol 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üm dinamik bellek yönetimindedir.

Dinamik bellek yönetiminde, dizilerin boyutları önceden belirlenmez. Program akışında dizi boyutunu ayarlarız ve gereken bellek miktarı, program çalışırken tahsis edilir. Dinamik bellek tahsisi içincalloc(  ) ve malloc(  ) olmak üzere iki önemli fonksiyonumuz vardır. Bellekte yer 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ız zaman, 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 çarparak hafızada gereken bellek alanını ayırır. Dinamik oluşturduğunuz dizi içersindeki her elemana, otomatik olarak ilk değer 0 atanır.

malloc(  ) fonksiyonu, calloc(  ) gibi dinamik bellek 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 ilk programımızı yazalım:

#include<stdio.h>
#include<stdlib.h>
int main( void )
{
	// Dinamik bir dizi yaratmak 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 yaratiyoruz. 
	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( "%dn", 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 şekilde işgal etmesini istemiyorsanız, free(  ) fonksiyonunu kullanmanız gerekmektedir. 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 programlama dili için bir çöp toplayıcı yoktur ve iyi programcıyla, kötü programcı burada kendisini 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 ve kapsamlı bir program söz konusuysa, efektif bellek yönetiminin ne kadar önemli olduğunu daha iyi anlarsınız. Gereksiz tüketilen bellekten kaçınmak gerekmektedir. Bunun için fazla bir şey yapmanız gerekmez; calloc(  ) fonksiyonuyla tahsis 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(  ) fonksiyonuyla kaldı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 yaratmak 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 yaratiyoruz. 
	dizi = malloc( eleman_sayisi * sizeof( int ) );

	for( i = 0; i < eleman_sayisi; i++ )
		printf( "%dn", 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 yeterli alan olmayabilir ya da benzeri bir sıkıntı olmuştur. Bu tarz problemlerin sık olacağı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 yaratmayı gördük. Ancak bu diziler tek boyutlu dizilerdi. 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 iki kez üzerinden geçerseniz, temel yapının aklınıza yatacağını düşünüyorum. Kodun koyu yazılmış yerlerini öğrendiğiniz takdirde, sorun kalmayacaktır.

Rate this post