C’de Dizilere Giriş

Değişkenlerin belli zaman aralıklarında bellekte tutulan nesneler olduğundan bahsetmiştik. Bellekte tutulan bu değişkenlerin her birinin tür ve adres bilgisi bilinerek onlara erişilebilmektedir. C’deki temel türlerden ve ne tür verileri tutabildiklerinden de daha önceki yazılarda bahsetmiştik. Peki programlarımızı her zaman bu temel türlere dayanarak yazabilir miyiz? Daha karmaşık veya aynı türden daha fazla veriye ihtiyaç duyduğumuzda nasıl bir yol izlememiz gerekir? Öncelikle bu tür sorulara cevap aramakla işe başlayalım.

Öncelikle veri yapılarından bahsedelim. Veri yapısı (data structure), birbiriyle ilişkili verilerden meydana gelen bir koleksiyondur diyebilriz. Peki ne tür bir ilişkiden bahsediyoruz? Çok basit bir örnek olarak bir öğrencinin bilgilerini ele alalım. Bir öğrencinin isim, soy isim, ID, sınıf numarası, şube numarası ve not gibi pek çok verisi bulunabilir. Bu verileri bir C programında ayrı ayrı tanımlayabiliriz. Bu durumda bazılarının veri türleri de farklılık gösterecektir. Örnek:

const char *name = "Mustafa Yemural";
unsigned long id = 344534543;
unsigned short classNo = 12;
char branchNo = 'A';
double grade = 65.5;

Gördüğünüz gibi öğrenci ile ilgili tüm bigilerin ayrı bir veri türü bulunmaktadır. Ancak bu bilgileri bu şekilde tanımlamanın yapısal olarak hiçbir bütünlüğü yoktur. Çünkü programlama açısından düşündüğünüzde bunların hepsi birbirinden ayrı birer değişkendir. Siz bunların birbiri ile ilişkili olduğunu görseniz de kullandığınız derleyici veya bilgisayar bunlar arasındaki ilişkiyi bilemez.

İşte veri yapıları ile bu tür birbiri ile ilişkili verileri bir arada bulundurup derleyicinizin bu ilişkiyi bilmesini sağlayabilirsiniz. Daha sonra da bu veri yapısına özel işlemleri (yeni öğrenci ekleme, öğrenci silme veya öğrenci verilerini güncelleme gibi) kolaylıkla gerçekleştirebilirsiniz.

C’de veri yapıları oluşturmanın pek çok yolu vardır. Bu gibi birbirinden ayrı türlere sahip, ancak birbirleri ile ilişkili verileri bir veri yapısına dönüştürmenin en basit yolu yapıları kullanmaktır. C’de yapı (structure) dediğimiz dil özellikleri ile farklı türden verileri tek bir yerde toplayabilirsiniz. Yapıları daha ileriki yazılarımda ayrıntılı olarak göstereceğim. Şimdilik yukarıdaki verilerin yapılar ile nasıl bir bütünlük içinde tutulabileceği ile ilgili örneği verip geçiyorum:

struct Student {
    const char *name;
    unsigned long id;
    unsigned short classNo;
    char branchNo;
    double grade;
};

struct Student std1 = {"Mustafa Yemural", 344534543, 12, 'A', 65.5};

Elbette C’de çoklu verileri bellekte tutabilmenin tek yolu yapılar değildir. Eğer aynı türden birden fazla veriyi (ve genellikle sayısı baştan belli olan verileri) bellekte tutmak istiyorsanız, bu durumda dizileri kullanabilirsiniz. İşte bu yazıda dizi kavramına giriş yapacak ve dizilerle ilgili temel kavramları öğreneceğiz.

Dizi Kavramı ve Dizilerin Bildirimi

Tutacağımız veriler her zaman az önceki örnekteki gibi fazla veri türünden meydana gelmeyebilir. Örneğin bir sınıfımız olsun ve içerisinde 10 adet öğrenci bulunsun. Normalde şimdiye kadar gördüğümüz bilgiler ile bu öğrencilerin not bilgilerini şu şekilde programımızda saklayabilirdik (uzatmamak için ilk beşini veriyorum):

double student1 = 45.5;
double student2 = 97.0;
double student3 = 66.5;
double student4 = 10.0;
double student5 = 76.5;

Sizce de böyle bir şey yapmak çok hoş mu görünüyor? Peki bu sınıfta 50 öğrenci olsaydı her biri için ayrı değişken mi tanımlamamız gerekiyordu? İşte belli bir sayıda ve birbirleri ile aynı türde olan verileri tutmak için aslında programlama dünyasında dizi olarak bilinen veri yapıları bulunmaktadır. Dizi (array), belli sayıdaki aynı türden verileri bir bellek bölgesinde bitişik olarak tutmaya yarayan veri yapısına verilen isimdir.

C dili dizileri doğal söz diziminde desteklemektedir (yani onlar için ayrı bir kütüphaneye ihtiyaç duymamaktadır). C dilindeki diziler de aslında aynı işi yapan dil araçlarıdır. Dizileri kullanarak bu tip çoklu verilere tek bir isim üzerinden erişebiliriz. Öncelikle C dilinde bir diziyi bildirmenin genel biçimine bakalım:

<Veri Türü> <Dizi Adı> [<Dizinin Eleman Sayısı>];

"<Veri Türü>" kısmına değişken tanımlarken kullanabileceğimiz veri türlerinden herhangi biri yazılabilir. Bu tür, aslında dizide tutulacak olan veri elemanlarının türünü belirtir. Aynı şekilde "<Dizi Adı>" kısmına da C’deki değişken isimlendirme kurallarına uyan bir isim verilebilir. "<Dizinin Eleman Sayısı>" kısmına ise dizide yer alacak eleman sayısı yazılır. Eleman (element), bir dizide yer alan her bir öğeye verilen isimdir. Eleman sayısı kısmına herhangi bir pozitif tam sayı sabiti yazılabilir. Sabit yerine farklı ifadelerin yazıldığı durumlar daha sonra anlatılacaktır. Şimdi dizi bildirimine birkaç örnek verelim:

int test[20]; // 20 elemanlı ve her bir elemanı int olan dizi
double students[10]; // 10 elemanlı ve her bir elemanı double olan dizi
unsigned char data[40]; // 40 elemanlı ve her bir elemanı unsigned char olan dizi
float grades[5]; // 5 elemanlı ve her bir elemanı float olan dizi

Burada eleman sayısının tam sayı türünden bir sabit olması gerektiğini söylemiştik. Yani kayan noktalı sayı gibi diğer sabitler eleman sayısı kısmına yazılamaz. Ayrıca negatif sayıların da buraya yazılması derleme zamanında hata oluşturacaktır:

int test[34.5]; // Derleme Hatası: Tam sayı olmayan boyut bilgisi
int test2[-3]; // Derleme Hatası: Negatif boyut bilgisi

Ancak sizeof operatörünün kullanıldığı veya sadece tam sayı sabitlerinin kullanıldığı ifadeler buraya yazılabilir (örnekte int türünün boyutu 4 byte olarak ele alınmıştır):

double a[100 * 2]; // 200 elemanlı ve her bir elemanı double olan dizi
float b[sizeof(int) * 20]; // 80 elemanlı ve her bir elemanı float olan dizi

Yine #define önişlemci bildirimi ile tanımlanan isimler, eğer kuralları bozmayan bir değere sahipse eleman sayısı kısmına yazılabilirler (ki bu kullanım genellikle iyi bir pratiktir):

#define SIZE 200
int values[SIZE];

Diziler C’de diğer türlerden farklı bir tür olarak ele alınırlar. Yani int türünden tanımlanan bir değişken ile int türünden tanımlanan bir dizi farklı tür bilgilerine sahiptirler ve bu nedenle derleyici tarafından farklı şekilde yorumlanırlar. Yine burada bir konuya daha değinmekte fayda var: Bildiğiniz gibi virgül (,) atomu ile değişkenleri tek bir satırda bildirmemiz mümkündü. Bunu diziler için de yapabilirsiniz. Şu örneği inceleyelim:

int a[80], b[20], c[40], d[150];
int testArray[100], testVar1, testVar2;

Yukarıdaki örnekte ilk satırda 4 adet dizi bildirimi yer almaktadır. İkinci satırda ise testArray bildirimi 100 elemanlı bir int dizisini belirtirken, testVar1 ve testVar2 bildirimleri sıradan int türünden değişkenleri belirtmektedirler. Bu şekilde herhangi bir türden dizinin bildirimini nasıl yapabileceğimizi anlamış olduk. Şimdi de bu dizilere ilk değer verilmesi ve varsayılan değerleri hakkında biraz konuşalım.

Dizilere İlk Değer Verilmesi

C’de değişkenkere ilk değer vermek gibi dizilere ilk değer vermek de önemlidir. Çünkü yerel olarak (bir fonksiyonun içerisinde) tanımlanan ve ilk değer verilmeyen dizilerin tuttukları değerler belirsiz (undetermined) olur. Yani o anda bellek bölgesinde bulunan herhangi bir çöp değeri (garbage value) alabilirler. Örneğin bir fonksiyonun içerisinde 5 elemanlı a dizisini şu şekilde yazmış olalım:

int test()
{
    int a[5];
    ...
}

İlk değer verilmemiş bu dizinin elemanlarını kullanmaya kalkarsak çok farklı tam sayılar görmeniz mümkündür. Bazı sistemlerde genellikle 0 sayısı denk gelse bile buna güvenmemeniz gerekir. Bu nedenle dizilere ilk değer vermeyi alışkanlık haline getirmeniz iyi bir pratiktir.

C’de dizilere ilk değer vermenin C23 standartlarına göre temelde iki yolu bulunmaktadır:

  1. String değişmezi ile ilklendirme (string literal initializer)
  2. Süslü parantez ile ilklendirme (braced initializer)

İlk ilklendirme yöntemini karakter dizileri konusunda anlatacağım. Şimdilik ikinci ilklendirme yöntemine odaklanalım. Bir diziyi süslü parantezler ile ilklendirmenin de çok farklı formları bulunmaktadır. Bunlar temelde birbirine çok benzese de farklı kaynaklarda isimlerini farklı olarak görebilirsiniz. Ben bunları 3 kategoriye ayıracağım:

  1. İlklendirici listesi (initializer-list)
  2. Belirlenmiş ilklendirici (designated initializer) (C99)
  3. Boş ilklendirici (empty initializer) (C23)

Dediğim gibi, bu 3 durum da aslında hemen hemen aynı söz dizimine shaiptirler. Ancak kullanımları ve anlamları açısından bazı farklar bulunur. Şimdi en basiti olan ilklendirici listesi ile dizileri ilklendirme konusundan başlayalım. Bir diziyi tanımlarken eşittir (=) atomundan sonra süslü parantezlerin içerisine bir değer listesi girerek ilklendirmek isterseniz, bunun adı ilklendirici listesi (initializer-list) olmaktadır. Hemen bir örnek verelim:

int numbers[6] = {1, 2, 3, 4, 5, 6};

Yukarıda tanımlanan 6 elemana sahip int dizisine bir ilklendirme listesi ile ilk değer verilmiştir. Bu şekilde ilk eleman 1, ikinci eleman 2 şeklinde giden bir ilk değer ataması gereçekleşir. Değerler süslü parantezler arasında virgüller ile ayrılarak verilmektedir. Ancak dizilerin tüm elemanlarına ilk değer verme zorunluluğu yoktur. İlk değer verilmeyen elemanlara varsayılan değer (default value) verilmektedir. Bu yapılan ilklendirmeye de varsayılan ilklendirme (default initialization) adı verilmektedir. Varsayılan ilklendirmede değerler şu kurallara göre verilmektedir:

  • Gösterici türüne NULL değeri atanır.
  • Tam sayı türlerine işaretsiz 0 değeri atanır.
  • Kayan noktalı sayı türlerine pozitif 0 değeri atanır, ancak verideki üs (exponent) kımsı implementasyona bağlıdır (implementation defined).
  • Yapı, birlik (sadece ilk eleman için) ve dizi gibi çoklu elemana sahip türlerin her bir elemanına özyinelemeli olarak yukarıdaki 3 kural uygulanır. Herhangi bir dolgu (padding) değeri varsa, bu değerin tüm bitleri 0 yapılır.

Son kurala ve ilk kurala şimdilik çok takılmanıza gerek yoktur. İkinci ve üçüncü kuralara göre bazı örnekler verelim:

int test1[5] = {1}; // {1, 0, 0, 0, 0}
int test2[5] = {1, 2}; // {1, 2, 0, 0, 0}
unsigned int test3[5] = {1, 2, 3}; // {1, 2, 3, 0, 0}

float f1[3] = {5.6f}; // {5.6, 0.0, 0.0}
float f2[4] = {1.2f, 23.4f}; // {1.2, 23.4, 0.0, 0.0}
double f3[5] = {1.4, 2.5, 3.6}; // {1.4, 2.5, 3.6, 0.0, 0.0}

Yukarıdaki örneklerden de anlayabileceğiniz gibi, ilk değer verilmeyen tüm elemanlara varsayılan ilklendirme uygulanmaktadır. Siz de kendi ortamınızda çeşitli denemeler yaparak sonuçları inceleyebilirsiniz. Peki bir diziye bildirilen eleman sayısından fazla ilk değer verilmeye kalkılırsa ne olur? Bunu yaptığınızda muhtemelen derleyici size bir uyarı verecek, ancak var olan elemanlara ilk değerler atanacaktır. Örnek:

double d[3] = {1.15, 3.45, 5.145, 67.5}; // Uyarı: Dizi ilklendiricisinde fazladan eleman

Ancak yine de bu konuda dikkatli olmanızda fayda vardır. Özellikle de dizilerin şimdi anlatacağım durumu ile ilgili… Diziler bildirilirken eleman sayılarının da belirtilmesi gerektiğini zaten söylemiştim. Ancak ilklendirme yapılan bir dizi için bu durumla ilgili bir istisna oluşur. Evet, ilklendirme yapılan dizilerde, dizinin tuttuğu eleman sayısını belirtmeye gerek yoktur. Örnek:

int numbers[] = {1, 2, 3, 4, 5}; // 5 elemanlı int dizisi

Yukarıdaki biçimde dizilerin eleman sayısını baştan yazmadan, onlara verdiğiniz ilk değer sayısına göre derleyicinin eleman sayısını anlamasını sağlayabilirsiniz. İşte burada ilklendirme listesine yazdığınız eleman sayısının bir önemi olur. Çünkü yanlışlıkla eksik veya fazladan yazdığınız bir eleman ile, istemediğiniz boyutlarda diziler tanımlayabilirsiniz. Bu nedenle bu kullanımı çoğu durumda tavsiye etmiyorum. Birkaç örnek daha verelim:

int numbers[] = {1}; // 1 elemanlı int dizisi
float test[] = {4.5f, 6.7f}; // 2 elemanlı float dizisi
long double temps[] = {30.5, 34.5, 29.7, 28.9, 67.4}; // 5 elemanlı long double dizisi

Ancak ilk değer vermeden dizi tanımı yapmaya kalkmak derleme zamanında hataya neden olacaktır:

int numbers[]; // Derleme Hatası: Dizi boyutu belirtilmemiş

Çünkü C’de sıradan dizilerin eleman sayısı baştan derleyiciye bildirilmek zorundadır (değişken boyutta diziler olayına şimdilik girmiyorum). Şimdi süslü parantez ile ilklendirme biçimlerinden ikincisi olan ve C99 standartları ile dile eklenen belirlenmiş ilklendiricilere bakalım. Belirlenmiş ilklendirici (designated initializer) ile bir dizinin elemanlarına istediğiniz sırada ilk değer vermeniz mümkündür. İlk bakışta söz dizimi biraz karmaşık gibi görünse de aslında oldukça basittir. Hemen bir örnek üzerinden gidelim:

int numbers[5] = {[4]=5, [0]=1, 2, 3, 4}; // {1, 2, 3, 4, 5}

Burada değerlerin başında kullanılan [<sabit ifadesi>]= söz dizimine belirtici (designator) adı verilmektedir. Belirticiler bir dizinin hangi elemanına ilk değer verileceğini belirtirler. İçlerine yazılan sabit ifadeleri elemanın sırasını belirtir. Birazdan da ayrıntılı göreceğimiz üzere, C dilinde elemanların sırası (yani indisi) 0’dan başlar. Yani 0. indis ilk elemanı, 1. indis ikinci elemanı belirtir ve bu şekilde gider.

Yukarıdaki örnekte [4]=5 ile dizinin 5. elemanına 5 değeri atanmış, [0]=1 ile de dizinin ilk elemanına 1 değeri atanmıştır. Geri kalan değerler ise dizinin boş kalan elemanlarını sırasıyla ilklendirmektedirler. Balirlenmiş ilklendiriciler, ilklendirme listesinin içerisinde sıradan değerler ile karışık bir biçimde kullanılabilirler:

int numbers[5] = {2, [1]=4, [2]=6, 8, 10}; // {2, 4, 6, 8, 10}

Bu tip kullanımlardaki temel mantık şudur: Öncelikle belirlenmiş ilklendirici olmayan öğeye kadar baştan itibaren değerler yerleştirilir, belirlenmiş ilklendirici öğesine gelindiğinde (o sıradaki değere ilk değer verilmiş olsa bile) değerler ilklendiricide yer alan sıra numarasına göre yeniden atanır. Belirlenmiş ilklendiricinin bittiği noktadan sonra normal değerler ile devam ediliyorsa, bu değerler son belirleyicinin sıra numarasının bir sonrasından itibaren atanır. Şimdi bir örnek üzerinden tek tek göstereyim:

int numbers1[5] = {2}; // {2, 0, 0, 0, 0}
int numbers2[5] = {2, 4}; // {2, 4, 0, 0, 0}
int numbers3[5] = {2, 4, [3]=4}; // {2, 4, 0, 4, 0}
int numbers4[5] = {2, 4, [3]=4, [1]=6}; // {2, 6, 0, 4, 0}
int numbers5[5] = {2, 4, [3]=4, [1]=6, 8}; // {2, 6, 8, 4, 0}
int numbers6[5] = {2, 4, [3]=4, [1]=6, 8, 10}; // {2, 6, 8, 10, 0}

Burada gerektiğinden fazla eleman sayısı yazıldığını ve derleyicinin buna uyarı vereceğini düşünebilirsiniz. Ancak durum tam olarak böyle değildir. [1]=6 değeri ikinci elemana atama yaptıktan sonra, atama yapma sırası üçüncü elemana gelir ve bu nedenle ondan sonra gelen 8 değeri üçüncü elemana atanır. Daha sonraki 10 değeri ise, önceden [3]=4 ile atama yapılan dördüncü sıraya atanır. Yani bu değerden sonra bir değer bile yazsanız değerleyici uyarı vermeyecektir.

Belirlenmiş ilklendiricileri bu kadar karmaşık bir şekilde kullanmanızı zaten tavsiye etmiyorum. Bu örnekleri sadece söz dizimini biraz daha iyi anlamanız açısından verdim. Aslında bu ilklendiricilerin en iyi kullanıldığı yerler yapı ve birlikleri ilklendirme işlemleridir. Ancak bu konu yazının kapsamı dışındadır. Genel olarak bütün elemanları belirlenmiş ilklendiricilerden oluşan şu basit kullanım örneği ile bu konuyu sonlandıralım:

int numbers[5] = {[0]=1, [1]=2, [2]=3, [3]=4, [4]=5}; // {1, 2, 3, 4, 5}

Son olarak C23 standartları ile dile eklenecek olan boş ilklendirici kavramına bakalım. Boş ilklendirici (empty initializer), boş süslü paranrtezler ile bir dizinin tüm elemanlarına varsayılan ilklendirme uygulama yöntemidir. Bu yöntem C++’ta zaten varken C’de C23 standartlarına kadar yer almamaktaydı. Boş ilklendirici kullanımı şu şekildedir:

int numbers[5] = {}; // {0, 0, 0, 0, 0}

C23 öncesinde aynı işlemi yapmak için ilklendirici listesine bir adet varsayılan değer vermeniz gerekmektedir:

int numbers[5] = {0}; // C23 öncesi
double test[4] = {0.0}; // C23 öncesi 

Bu kullanım elbette C23 ve sonrasında da geçerli olacak olsa da boş ilklendirici kullanımı bu olayı oldukça basitleştirmektedir. Eğer C23 öncesinde boş ilklendirici kullanmaya kalkarsanız derleme zamanında hata alırsınız.

Dizi Elemanlarına Erişim

Buraya kadar dizilerin nasıl bildirildiğini ve çok farklı şekillerde onları nasıl ilklendirebileceğimizi gördük. Şimdi artık onları uygulamalarımızda kullanma zamanı! Öncelikle C’de dizi elemanlarına nasıl erişebileceğimiz hakkında biraz konuşalım. Dizi elemanlarına erişim "[]" operatörü ile sağlanır ve bu operatöre C dilinde indis operatörü (index operator) veya alt simge operatörü (subscript operator) adı verilmektedir. İndis operatörünü dizi ismi ile kullanarak, dizi elemanlarına değer verme veya dizi elemanlarının değerini elde etme işlemini gerçekleştirebilirsiniz:

int numbers[3] = {10, 20, 30};
numbers[1] = 30;

printf("%d %d %d\n", numbers[0], numbers[1], numbers[2]); // ÇIKTI: 10 30 30

Yukarıdaki örnekte numbers dizisinin 2. elemanına 30 değeri atanmakta ve daha sonra dizinin bütün değerleri ekrana basılmaktadır. Burada indis operatörünün içine yazdığınız sayıya indis (index) adı verilmektedir ve bu sayı erişilecek elemanın sıra numarasını belirtmektedir. Bu örnekten bir şeyi bariz bir şekilde farketmeniz gerekir: C dilinde dizilerde sıra numaraları (indisler) sıfırdan başlar! Bu çok basit gibi görünen, ama karmaşık uygulamalarda dikkat edilmezse büyük sorunlara yol açabilecek bir bilgidir. Çünkü siz N’inci elemana ulaşmak istediğinizde bu operatörün içine "N-1" yazmanız gerekir ve bunu unutmanız bazı sınır problemlerine yol açabilir. Şimdi şu örneklere bakalım (buradaki her bir isim için dizi tanımlaması yapıldığını varsayalım):

a[34] = 45; /* Dizinin 35. elemanına 45 değerini ata. */
b[0] = 17; /* Dizinin 1. elemanına 17 değerini ata. */
c[20] = 20; /* Dizinin 21. elemanına 20 değerini ata. */
d[1] = -90; /* Dizinin 2. elemanına -90 değerini ata. */

Gördüğünüz gibi aslında bir indis N ise, o dizinin (N+1)’inci elemanına erişim sağlıyorsunuz demektir. Bu durumda N elemanlı bir dizinin ilk indisi 0, son indisi ise N-1 olur. İşte esas tehlike burada başlıyor. Aşağıdaki örnekteki gibi 20 elemanlı bir dizinin sonuna ulaşmak amacıyla indis operatörüne 20 vermeniz tanımsız davranış (undefined behavior) oluşturacaktır:

int a[20];
a[20] = 90;

printf("%d\n", a[20]); // ÇIKTI: ??

Eğer yukarıdaki kodda bellekte erişilmek istenen bölge program tarafından sahiplenilen bir bölgede ise, değerin ekrana basılma olasılığı vardır. Eğer oraya bir değer atamazsanız genellikle o anda bellek bölgesinde var olan değeri (çöp değer) elde edersiniz. Ancak bu bölge programın kontrolü dışında bir bölgede ise parçalama arızası (segmentation fault) gibi bir hata ile programınızın o anda sonlanması olasıdır. Bu hata genellikle programın sahibi olmadığı bölgelerde işlem yapmaya çalışması sonucu ortaya çıkar. Gördüğünüz gibi dizinin sınırları dışında (out of bounds) bir eylem yapmaya kalktığınızda programınızın ne tür bir tepki vereceği belli olmaz.

İndis operatörü içerisine tam sayı türünden bir ifade alabilmektedir. Bu ifade bir negatif sayı da olabilmektedir. Ancak bu operatörü diziler ile kullanırken bunu yapmanızı kesinlikle tavsiye etmiyorum (yine parçalama hatasına yol açabilen bir durumdur). Negatif sayı olayı göstericiler konusunda ele alınacaktır. Bu operatör çalışma zamanında işlem yaptığından, buraya verilen ifadenin de çalışma zamanında hesaplanan bir ifade olması elbette olasıdır. Şimdi şu örneğe bakalım:

int a = 1;
int test[4];

test[0] = 10;
test[a] = 20; // test[1] = 20
test[++a] = 30; // test[2] = 30
test[3] = 40;

İndis operatörü bir sol-taraf ifadesi (lvalue expression) ürettiğinden dizi elemanları bu operatör ile hem değiştirilebilmekte hem de okunabilmektedir. Şimdi daha ilginç örneklerle konuyu biraz daha pekiştirelim. Ancak ondan önce birkaç pratik bilgi vermem daha iyi olacaktır. Öncelikle bir dizinin boyutunu #define önişlemci bildirimi ile bir isme bağlamanız ve daha sonra onu diziyi tanımlarken kullanmanız iyi bir pratiktir. Çünkü dizinin boyut bilgisine programın farklı yerlerinde sıklıkla ihtiyaç duyarsınız ve bir isim ile onu belirtmek size okunabilirlik konusunda fayda sağlar.

İkinci konu ise dizi elemanlarının tamamına erişmek istediğinizde (onların tamamının değerini değiştirmek istediğinizde veya tamamının değerini elde etmek istediğinizde) C dilinde sıklıkla for döngüsü tercih edilmektedir. Bu şekilde çok daha pratik bir biçimde dizi üzerinde işlemler gerçekleştirebilirsiniz. Elbette diğer döngü ifadeleri (while vb.) de kullanılabilir. Ancak pratikte genelde for döngüsü tercih edilmektedir. Şimdi dizi elemanlarını kolay bir şekilde nasıl manipüle edip ekrana yazdırabileceğimize bakalım:

#include <stdio.h>

#define SIZE 10

int main() 
{
    int nums[SIZE] = {0};
    int i;

    /* Dizi elemanlarına değer atama */
    for(i = 0; i < SIZE; ++i) {
        nums[i] = i;
    }

    /* Dizi elemanlarını ekrana basma */
    for(i = 0; i < SIZE; ++i) {
        printf("%d ", nums[i]);
    }

    return 0;
}
ÇIKTI
0 1 2 3 4 5 6 7 8 9

Dizi elemanlarını ekrana basma kısmını while döngüsü kullanarak tekrar yazalım:

int i = 0;
while(i < SIZE) {
    printf("%d ", nums[i++]);
}

Bu şekilde indis operatörünün temel kullanımını, dizi elemanlarını döngü yapıları ile nasıl değiştirebileceğimizi ve ekrana basabileceğimizi görmüş olduk. Şimdilik bu operatör ve dizi elemanlarına erişim ile ilgili bu kadar bilgi yeterlidir. Biraz da dizilerin bellekte nasıl yerleştiğini ve bazı durumlarda normal değişken kullanımlarına göre dizilerin ne gibi avantajlarının bulunduğunu inceleyelim.

Dizilerin Bellekte Yerleşimi

Dizi elemanlarına erişimde parçalama arızası gibi çeşitli hataların çıkabileceğini söylemiştik. Dizi elemanlarına erişim, derleme zamanından (compile-time) ziyade bir çalışma zamanı (run-time) olayı olduğu için, bu tip hatalar da çalışma zamanında ortaya çıkacaktır. Bunu tam olarak anlamak için dizilerin bellekte nasıl yerleştiğini de incelememiz gerekir.

Öncelikle dizilerden bahsettiğim ilk sefer, dizi elemanlarının bellekte bitişik (contiguous) bir şekilde tutulduğunu belirtmiştim. Peki bu tam olarak ne anlama geliyor? Öğrencilerin notunu temel veri türlerinde ayrı ayrı değişkenlerde tutma veya dizilerde tutma örneğimize geri dönelim. Ayrı ayrı değişkenler kullanarak bu notları şu şekilde tutabiliriz:

double grade1 = 10.4;
double grade2 = 50.0;
double grade3 = 96.8;
double grade4 = 78.0;
double grade5 = 45.6;

C’de dizileri kullanarak bu not bilgilerini tek bir değişkende saklamamız mümkündür:

double grade[5] = {10.4 50.0, 96.8, 78.0, 45.6};

Veya şu şekilde de yazabiliriz:

double grade[] = {10.4 50.0, 96.8, 78.0, 45.6};

Burada dizilerin yazım kolaylığının dışında, verilerin bellekte bulunma biçimleri ile ilgili de ciddi farklar bulunmaktadır. Değişkenler programın durumuna göre belleğin (Process sınırları içerisinde) herhangi bir bölgesinde yer alabilirler. Yani grade1 isimli değişkenin tutulduğu yerin hemen ardında grade2 değişkeninin yer alacağının bir garantisi yoktur. Ancak grade isimli dizide yer alan elemanlar konusunda böyle bir garanti vardır. double türünü 8 byte olarak aldığımız bir sistemde, notları ayrı ayrı değişkenlerde veya tek bir dizi değişkeninde tutmanın bellek açısından farkını şu şekilde özetleyebiliriz:

Diziler ile Normal Değişkenlerin Bellek Karşılaştırması
Figür 1: Diziler ile Normal Değişkenlerin Bellek Karşılaştırması

Yukarıdaki figürde sol tarafta notlar ile ilgili tanımladığımız 5 değişkenin olası bellek yerleşimi görülmektedir. Şekilde gördüğünüz gibi bu değişkenler arasında bir bitişiklik ilişkisi olmak zorunda değildir. Ayrıca yerleşim bu şekilde değil de daha farklı biçimlerde de olabilir. Bu tamamen belleğin uygunluk durumu, işletim sisteminiz ve daha pek çok faktöre göre değişir. Sağ tarafta ise grade dizisinin bellekte örnek yerleşimi görülmektedir.

Buradan anlayacağınız gibi dizi elemanları ilk elemandan son elemana kadar sıralı bir biçimde bitişik olarak belleğe yerleştirilmiştir. Dizi için tanımladığınız tür bilgisi buradaki adres aralıklarını da değiştirecektir. Diziler tür bilgisi sayesinde elemanlarının adreslerine kolayca erişebilirler.

Örneğin burada double türü 8 byte olarak alındığından, dizinin bir sonraki elemanının başlangıç adresi, bir önceki elemandan 8 birim sonrasında yer alacaktır. Böylelikle toplamda 40 byte uzunluğundaki veri, bellekte bitişik bir biçimde tutulacaktır. Bu bize indis operatörü ile dizi elemanlarına erişim fırsatı da vermektedir. İndis operatörü sadece dizilerde değil, ileride göreceğimiz göstericilerde de sıklıkla kullanılmaktadır. Onun ileri düzey kullanımlarından elbette göstericiler konusunda bahsedilecektir.

İndis operatörünü şimdilik adres bilgisini çözümleyip oradaki değeri sol-taraf değeri (lvalue) olarak bize döndüren bir operatör gibi düşünebilirsiniz. Yine örneğe göre bu durumda grade[0] bize "0x7fbf" adresindeki veriyi, grade[1] ise "0x7fc7" adresindeki veriyi getirecektir. İşte bu yüzden grade[5] gibi bir kullanım, "0x7fbf + (5 8) = 0x7fe7" işleminden yola çıkarak, bize "0x7fe7" adresindeki veriyi getirmeye çalışacaktır. Bu nedenle bu durum tanımsız davranışa (undefined behavior*) neden olmaktadır. Bu işlemlerin ve bellekle ilgili detayların hepsine göstericiler konusunda değineceğim. Şimdilik bu kadar bilginin yeterli olduğunu düşünüyorum.

Dizilerin Birbirine Atanması ve Kopyalanması

Son olarak dizilerin birbirine atanma durumlarından ve bir diziyi bir başka diziye nasıl kopyalayabileceğimizden bahsedelim. Değişkenleri kolaylıkla atama operatörü ile birbirine atayabiliyor, hatta bir değişkeni başka bir değişkenin değeri ile ilklendirebiliyoruz. Peki aynı yöntemle dizi değişkenlerini de birbirine atayabilir miyiz?

Türleri ve eleman sayıları aynı olsa bile iki dizi değişkenini doğrudan atama operatörü ile birbirine atamak veya bir diziyi doğrudan başka bir dizi ile ilklendirmek C’de mümkün değildir ve derleme zamanında hata oluşturur:

double a[10] = {0.0};
double b[10];

b = a; // Derleme Hatası: Dizi türünden iki değişken birbirine atanamaz

double c[10] = a; // Derleme Hatası: Dizi değişkeni ile ilklendirme yapılamaz

Ne yazık ki C dilinde dizilerle diğer dillerdeki muadilleri gibi (Go, Python vb.) çok esnek bir şekilde çalışamıyoruz. Elbette C dili bunu çalışma zamanı avantajı ile kapatıyor. Diğer dillerde yaptıklarımızı da çeşitli alternatif tekniklerle yapabiliriz. Bunları yapabilmek için öncelikle C dilinde aynı türden (veya birbirlerine dönüşebilen türlerden) dizi elemanlarının birbirine atanmasının geçerli olduğunu aklımızda tutmamız gerekir:

double a[20], b[20];
a[3] = b[5]; /* Geçerli */

Bu tıpkı değişkenleri birbirine atamaya benzer. Peki bir dizinin bütün elemanlarını aynı boyutlu ve aynı türden başka bir diziye nasıl kopyalayabiliriz? Elbette bunun için kullanabileceğiniz en basit yöntem, for döngüsü ile elemanları tek tek dolaşarak atama yapmaktır:

#include <stdio.h>

int main() {
    double first[5] = {12.2, 45.6, 67.5, 21.8, 78.9};
    double last[5];
    int i;

    for(i = 0; i < 5; ++i) {
        last[i] = first[i];
    }

    // Elemanları ekrana basma
    for(i = 0; i < 5; ++i) {
        printf("%f ", last[i]);
    }

    return 0;
}
ÇIKTI
12.200000 45.600000 67.500000 21.800000 78.900000

Bir diziyi bir başka diziye kopyalamanın bir başka yöntemi ise, dizilerin ardışık veri tutma avantajlarından yararlanıp "<string.h>" başlık dosyasında yer alan memcpy fonksiyonunu kullanmaktır. Bu fonksiyonun geniş bir kullanım alanı bulunsa da şimdilik basitçe bu konudaki kullanımına örnek verelim. Öncelikle bu fonksiyonun C99 öncesi prototipi şöyledir:

void* memcpy( void *dest, const void *src, size_t count );

C99 standartlarında şu şekilde değiştirilmiştir:

void* memcpy( void *restrict dest, const void *restrict src, size_t count );

memcpy fonksiyonunun bu hali çok fazla tanımsız davranış durumu içerdiğinden çok güvenli bir fonksiyon değildir. Bu nedenle C11 standartları ile bu fonksiyonun daha güvenli ve hata yakalamada daha iyi olan memcpy_s isimli versiyonu kütüphaneye eklenmiştir. Bu versiyonun prototipi ise söyledir:

errno_t memcpy_s( void *restrict dest, rsize_t destsz,
                  const void *restrict src, rsize_t count );

Ben şimdilik burada sadece kullanımını göstermek amacıyla memcpy fonksiyonu ile diziler arası kopyalama işlemini nasıl yapabileceğimizin örneğini vereceğim:

#include <stdio.h>
#include <string.h>

int main() {
    double first[5] = {12.2, 45.6, 67.5, 21.8, 78.9};
    double last[5];

    memcpy(last, first, 5 * sizeof(double));

    // Elemanları ekrana basma
    for(int i = 0; i < 5; ++i) {
        printf("%f ", last[i]);
    }

    return 0;
}

memcpy fonksiyonunun ilk parametresi hedef dizinin başlangıç adresini, ikinci parametresi ise kaynak dizinin başlangıç adresini alır. Dizi isimleri kodda kullanıldığında aslında birer gösterici belirtirler. Biz de bu nedenle doğrudan isimlerini, yani başlangıç adreslerini veriyoruz. Bu konunun ayrıntısına ileride değineceğim. Ancak burada dikkat çeken başka bir nokta da son parametreye verilen kopyalanacak byte uzunluğudur.

Kopyalanacak byte uzunluğuna doğrudan eleman sayısını yazmak yapılan en büyük yanlışlardan birisidir. Çünkü daha önce de bahsettiğim gibi N elemanlı T türünden bir dizinin boyutu N byte değil, N * sizeof(T) byte olur. Yani tuttuğu her bir elemanın boyut bilgisini de göz önüne almak gerekir. İşte bu şekilde memcpy ile bir diziden başka bir diziye kopyalama yapabilirsiniz. Ancak güvenlik açısından eğer imkanınız varsa memcpy_s kullanmanızı tavsiye ederim. Bu ve benzeri fonksiyonlara ileride çok daha ayrıntılı bir şekilde değineceğim.

Bu yazıyı sonlandırmadan önce yaptıklarımızı pekiştirmek adına son bir örnek vermek istiyorum. Bu örnekte kullanıcıdan 6 öğrencinin not bilgilerini (tam sayı olarak) alıp not ortalamasını ekrana bastıracağız:

#include <stdio.h>

#define STUDENT_COUNT 6

int main()
{
    int grades[STUDENT_COUNT];
    int sum = 0;

    for(int i = 0; i < STUDENT_COUNT; i++)
    {
        scanf("%d", &grades[i]);
        sum += grades[i];
    }

    double result = ((double)sum) / STUDENT_COUNT;
    printf("Average grade: %f\n", result);

    return 0;
}
ÇIKTI
10
20
30
40
50
60
Average grade: 35.000000

Burada kullanılan STUDENT_COUNT isimli sabitin kodu anlamada ve hata yapmayı engellemede ne kadar faydalı olduğuna dikkat edin. Eğer böyle bir sabit kullanmasaydık, öğrenci sayısı yazmamız gereken yerlere yanlışlıkla başka bir sayı girip mantık hatalarına neden olabilirdik. Yine bu şekilde öğrenci sayısını tek bir seferde değiştirmemiz de daha kolaydır.

scanf ile dizi elemanlarına atama yaparken yine adres alma operatörünü (&) kullanmaya dikkat edin. İleride dizi isimleri ile ilgili bazı şeyleri anlattığımızda, bunun başka yollarının da olduğunu göreceksiniz. Yine ortalamayı hesaplarken veri kaybını önlemek için bölme işleminin bir tarafında double dönüşümü yaptık. Burada bir for döngüsü ile kullanıcıdan not bilgisi alma, bir başka for döngüsü ile de toplama işlemini yapma gibi bir kod yazabilirdik. Ancak bu şekilde daha sade bir kod yazmış olduk.

Bu yazıda C diline kısa bir giriş yapıp, dizilere neden ihtiyaç duyduğumuzu örneklerle anlatmata çalıştım. Ayrıca dizilerle ilgili sık yapılan hataları ve dizilerin söz dizimsel olarak nasıl engeller barındırdığını da anlatmaya çalıştım. Bir sonraki yazıda diziler ile ilgili daha fazla uygulama yapmayı ve dizilerin kullanımını daha iyi anlatmayı hedefliyorum.

4.7 9 votes
Article Rating
Subscribe
Bildir
guest

1 Yorum
Eskiler
En Yeniler Beğenilenler
Berfin

Umarım bu serinin devamı çabuk gelir sınava kadar sizden öğrenmeyi çok isterim. Sizin anlatımınız çok iyi. Java ve python dili içinde bu seriler gelse tadından yenmez hocam jdkjdkjjd.