Veri Yapıları ve Programlama

2 - Fonksiyonlar, Diziler ve İşaretçiler

Emre Can Yılmaz

Ondokuz Mayıs Üniversitesi

2026

Fonksiyonlar

  • Fonksiyonlar, belirli bir görevi yerine getiren kod bloklarıdır.
  • Programları daha küçük, yönetilebilir ve yeniden kullanılabilir parçalara ayırmamıza yardımcı olurlar.
  • Bir fonksiyonu tanımlarken yazdıklarımıza parametre, çağırırken verdiğimiz değerlere argüman denir.

Sözdizimi

geri_donus_tipi fonksiyon_adi(parametre_listesi) {
  // Fonksiyon gövdesi (işlemler)
  return deger;
}

Not

  • void fonksiyonlarda return; yazmak zorunlu değildir (yazılabilir).
  • int, float vb. dönüş tipi olan fonksiyonlarda anlamlı bir dönüş değeri vermek gerekir.

Örnek

#include <stdio.h>

int ciftMi(int sayi) {
  if (sayi % 2 == 0) {
    return 1; // 1, "doğru" anlamına gelir
  } else {
    return 0; // 0, "yanlış" anlamına gelir
  }
}

int main(void) {
  int sayi = 6;

  if (ciftMi(sayi)) { // ciftMi fonksiyonu 1 (doğru) döndürürse
    printf("%d çift bir sayıdır.\n", sayi);
  } else {
    printf("%d tek bir sayıdır.\n", sayi);
  }

  return 0;
}

Fonksiyon Prototipleri

  • Fonksiyon prototipleri, derleyiciye fonksiyonun geri dönüş tipi, adı ve parametreleri hakkında bilgi verir.
  • Prototipler, genellikle fonksiyonun tanımından önce yer alır (çağrıdan önce görülmesi gerekir).

Sözdizimi:

geri_donus_tipi fonksiyon_adi(parametre_listesi);

Örnek

#include <stdio.h>

// Fonksiyon prototipi
int usAl(int taban, int us);

int main(void) {
  int x = 2, y = 3;

  int sonuc = usAl(x, y); // Prototip sayesinde çağrı doğru yorumlanır.
  printf("%d üssü %d = %d\n", x, y, sonuc);

  return 0;
}

int usAl(int taban, int us) {
  int sonuc = 1;

  // (Bu derste negatif üsleri ele almıyoruz.)
  if (us < 0) {
    return 0;
  }

  for (int i = 0; i < us; i++) {
    sonuc *= taban;
  }

  return sonuc;
}

Özyinelemeli Fonksiyonlar

  • Özyinelemeli fonksiyonlar, kendilerini çağırabilen fonksiyonlardır.
  • Bu tür fonksiyonlar, bir problemi daha küçük alt problemlere bölerek çözer; işlem bir temel duruma ulaşılana kadar devam eder.

Dikkat

Özyinelemede mutlaka bir temel durum olmalıdır; aksi takdirde sonsuz çağrı (ve çökme) oluşabilir.

int faktoriyel(int n) {
  // Varsayım: n >= 0
  if (n <= 1) {
    return 1; // Temel durum: 0! = 1 ve 1! = 1
  } else {
    return n * faktoriyel(n - 1); // Özyinelemeli adım
  }
}

Diziler

  • Diziler, aynı türdeki verileri saklamak için kullanılan veri yapılarıdır.
  • Diziler, bellekte ardışık olarak sıralanmış elemanlardan oluşur.
  • Dizi indisleri 0’dan başlar: ilk eleman dizi[0].

Sözdizimi:

veri_tipi dizi_adi[dizi_boyutu];

Örnekler:

int sayilar[5];        // 5 tam sayı saklayabilen bir dizi
float notlar[10];      // 10 ondalık sayı saklayabilen bir dizi
double uzunluklar[3];  // 3 double sayı saklayabilen bir dizi

Dizi İlklendirme

Örnek 1:

int sayilar[5] = {10, 25, 30, 45, 50};

Örnek 2:

float notlar[] = {85.5, 90.0, 78.2};
// Boyut otomatik olarak 3 olarak atanır.

Dikkat!

Bir dizinin boyutu ve türü, tanımlandıktan sonra değiştirilemez!

Dizi Elemanlarının Değerini Değiştirme

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

// 3. elemanın (indis 2) değerini -1 yap
sayilar[2] = -1;

// 5. elemanın (indis 4) değerini 0 yap
sayilar[4] = 0;

Dizi elemanlarının indislerini kullanarak yukarıdaki gibi değiştirebiliriz.

Örnek: Dizinin Elemanlarını Yazdırma

#include <stdio.h>

int main(void) {
  int sayilar[5] = {10, 25, 30, 45, 50};

  for (int i = 0; i < 5; i++) {
    printf("sayilar[%d]: %d\n", i, sayilar[i]);
  }

  return 0;
}

sizeof Operatörü: Dizilerin Boyutunu Hesaplama

  • sizeof operatörü, bir değişkenin veya veri tipinin bellekte kapladığı bayt sayısını döndürür.

  • Dizilerle kullanıldığında, dizinin toplam boyutunu bayt cinsinden verir.

  • Dizinin eleman sayısını bulmak için:

    • eleman_sayisi = sizeof(dizi) / sizeof(dizi[0])

Örnek

#include <stdio.h>
#include <stddef.h>

int main(void) {
  int sayilar[] = {10, 20, 30, 40, 50};

  size_t eleman_sayisi = sizeof(sayilar) / sizeof(sayilar[0]);
  printf("Dizinin eleman sayısı: %zu\n", eleman_sayisi);

  return 0;
}

Not

Örneğin sizeof(sayilar) = 20 ve sizeof(sayilar[0]) = 4 ise 20 / 4 = 5 olur. Bu değerler sistem ve derleyiciye göre değişebilir; yöntem ise aynıdır.

Diziler ve Fonksiyonlar

  • Dizileri fonksiyonlara parametre olarak geçirebiliriz.
  • Bir fonksiyona dizi gönderildiğinde, fonksiyon dizinin bir kopyasını değil, bellekteki başlangıç adresini alır.
  • Bu nedenle fonksiyon içinde yapılan değişiklikler, orijinal diziyi de etkiler.

Kritik not

Fonksiyon parametresi olarak alınan int dizi[], pratikte int *dizi gibi davranır. Bu yüzden fonksiyon içinde sizeof(dizi) artık dizinin toplam boyutu değildir; pointer’ın boyutudur.

Örnek: Dizinin Elemanlarını İkiye Katlayan Fonksiyon

#include <stdio.h>

void ikiyeKatla(int dizi[], int boyut) {
  for (int i = 0; i < boyut; i++) {
    dizi[i] *= 2;
  }
}

int main(void) {
  int sayilar[5] = {1, 2, 3, 4, 5};
  ikiyeKatla(sayilar, 5);

  for (int i = 0; i < 5; i++) {
    printf("%d ", sayilar[i]);
  }
  printf("\n"); // Çıktı: 2 4 6 8 10

  return 0;
}

İşaretçiler (Pointers)

  • İşaretçiler (pointers), bellekteki bir değişkenin adresini tutan değişkenlerdir.
  • Normal değişkenler değer saklarken, işaretçiler adres saklar.
  • İşaretçiler sayesinde, bir değişkenin değerini dolaylı olarak değiştirebilir, fonksiyonlara veri gönderebilir ve dinamik bellek yönetimi gibi işlemleri gerçekleştirebiliriz.

İşaretçiler ve Bellek: Görsel Bir Açıklama

  • Bellek, her biri bir bayt (byte) saklayabilen adreslenmiş bölmelerden oluşur.
  • Her bölmenin benzersiz bir adresi ve bir değeri vardır.
+------------+------------+------------+------------+
|    Adres   |   0x1000   |   0x1004   |   0x1008   | ...
+------------+------------+------------+------------+
|    Değer   |     10     |   0x1000   |     'A'    | ...
+------------+------------+------------+------------+
                   ^             ^
                   |             |
                int sayi       int *ptr
  • sayi değişkeni 0x1000 adresinde saklanıyor ve değeri 10.
  • ptr işaretçisi 0x1004 adresinde saklanıyor ve değeri sayi değişkeninin adresi olan 0x1000.
  • *ptr ifadesi, ptr’nin tuttuğu adresteki değeri verir (burada, 10).
  • Bu bağlamda * operatörüne dereference (içeriğini alma) operatörü denir.

İşaretçi Tanımlama ve İlklendirme

Sözdizimi:

veri_tipi *isaretci_adi;
  • veri_tipi: İşaretçinin işaret edeceği değişkenin veri tipi.
  • *: İşaretçi olduğunu belirtir.
  • isaretci_adi: İşaretçiye verilen isim.

İlklendirme:

isaretci_adi = &degisken_adi; // &: Adres operatörü

Örnek

#include <stdio.h>

int main(void) {
  int sayi = 10;
  int *ptr; // int türünde bir işaretçi tanımlama

  ptr = &sayi; // ptr'ye sayi'nin adresi atanır

  printf("sayi'nin değeri: %d\n", sayi);
  printf("sayi'nin adresi: %p\n", (void*)&sayi);
  printf("ptr'nin değeri: %p\n", (void*)ptr); // ptr, sayi'nin adresini tutar
  printf("ptr'nin gösterdiği adresteki değer: %d\n", *ptr); // *ptr, sayi'nin değerini verir

  return 0;
}

İşaretçilerle Değerleri Değiştirme

  • İşaretçiler aracılığıyla, işaret ettikleri değişkenin değerini değiştirebiliriz.

Örnek

#include <stdio.h>

int main(void) {
  int sayi = 10;
  int *ptr = &sayi;

  *ptr = 20; // ptr'nin gösterdiği adresteki değer (sayi) 20 olur

  printf("sayi'nin yeni değeri: %d\n", sayi); // Çıktı: 20
  return 0;
}

İşaretçiler ve Fonksiyonlar

  • İşaretçiler, fonksiyonlara parametre olarak geçirilebilir.
  • Bu sayede, fonksiyonlar, ana programdaki değişkenlerin değerlerini değiştirebilir.

Örnek: İki Sayıyı Değiştirme - Takas İşlemi

#include <stdio.h>

void degistir(int *a, int *b) {
  int temp = *a;
  *a = *b;
  *b = temp;
}

int main(void) {
  int sayi1 = 10;
  int sayi2 = 20;

  printf("Önce: sayi1 = %d, sayi2 = %d\n", sayi1, sayi2);
  degistir(&sayi1, &sayi2);
  printf("Sonra: sayi1 = %d, sayi2 = %d\n", sayi1, sayi2);

  return 0;
}

İşaretçiler ve Diziler

  • Bir dizinin adı, çoğu kullanımda dizinin ilk elemanının bellek adresi gibi davranır.
  • Bu nedenle dizi üzerinde pointer aritmetiği ile dolaşmak mümkündür.

Not

Dizi adı her zaman “pointer” değildir; bazı özel durumlarda dizi olarak ele alınır (ör. sizeof(dizi)).

Örnek

#include <stdio.h>

int main(void) {
  int sayilar[5] = {10, 20, 30, 40, 50};
  int *ptr = sayilar; // ptr, sayilar[0]'ın adresini gösterir

  printf("İlk eleman: %d\n", *ptr);           // Çıktı: 10
  printf("İkinci eleman: %d\n", *(ptr + 1));  // Çıktı: 20
  printf("Üçüncü eleman: %d\n", *(ptr + 2));  // Çıktı: 30

  return 0;
}

Ek bilgi

ptr + 1 ifadesi adresi “1 bayt” değil, “1 eleman” ilerletir. Örneğin int 4 bayt ise ptr + 1 adresi 4 bayt ilerler.

NULL Pointer

  • NULL, bir işaretçinin geçerli bir adresi göstermediğini belirtmek için kullanılan özel bir değerdir.
  • NULL farklı başlık dosyalarından gelebilir; pratikte yaygın bir kaynak <stddef.h>’dir.
  • NULL bir adres değildir; “boş/işaret etmiyor” anlamında kullanılır.

Örnek

#include <stdio.h>
#include <stddef.h>

int main(void) {
  int x, *p1, *p2, *p3;
  x = 123;
  p2 = NULL;
  p3 = &x;

  printf("Değer atanmamış p1 pointer'ının değeri: %p\n", (void*)p1);
  printf("p2 = NULL atanmış pointer'ın değeri: %p\n", (void*)p2);
  printf("p3 = &x atanmış pointer'ının değeri: %p\n", (void*)p3);
  printf("&x adresinin değeri: %p\n", (void*)&x);

  return 0;
}

Dikkat

p1 değişkenine değer atanmadığı için ekrana basılan sonuç “rastgele/çöp” görünebilir. Gerçek kodda pointer’ları kullanmadan önce mutlaka bir adres veya NULL ile başlatın.

İşaretçilerin Avantajları

  • Dinamik Bellek Yönetimi: Programın çalışma zamanında bellek alanlarını dinamik olarak ayırmamızı ve yönetmemizi sağlar. (İlerleyen bölümlerde detaylı olarak işlenecek.)
  • Fonksiyon Parametreleri: Büyük veri yapıları kopyalamadan, adres üzerinden aktarım yaparak performansı artırır ve bellek kullanımını azaltır.
  • Veri Yapıları: Bağlı listeler, ağaçlar gibi yapılar işaretçilerle kurulabilir. (İlerleyen bölümlerde detaylı olarak işlenecek.)

İşaretçilerle Çalışırken Dikkat Edilmesi Gerekenler

  • İlklendirme: Kullanmadan önce işaretçilere geçerli bir adres veya NULL atamak önemlidir.
  • Geçersiz adres (dangling pointer): Serbest bırakılmış/ömür süresi bitmiş bir bellek alanını işaret etmek ciddi hatalara yol açar.
  • Bellek sızıntıları: Dinamik ayrılan bellek, iş bittikten sonra serbest bırakılmalıdır (free() ile). (İlerleyen bölümlerde detaylı olarak işlenecek.)

Alıştırmalar

  1. Kullanıcının girdiği sayıların ortalamasını hesaplayan kodu dizileri kullanarak yaz. İlk önce kullanıcının kaç adet sayı gireceğini sor. Sayıları kullanıcıdan alırken döngü kullan.
  2. int sayilar[5] = { 11,22,33,44,55 }; şeklindeki sayilar dizisindeki tek sayıların toplamını hesaplayan, sayılardan en büyüğünü, en küçüğünü ve sayıların ortalamasını hesaplayan kodu yazın.
  3. Bir fonksiyon yazın. Bu fonksiyon, bir tamsayı işaretçisi ve bir tamsayı değeri alsın. Fonksiyon, işaretçinin gösterdiği adresteki değeri, verilen tamsayı değeri kadar artırsın. main fonksiyonunda bu fonksiyonu farklı değerlerle çağırarak test edin.
  4. Bir tamsayı değişkeni ve bir işaretçi tanımlayın. İşaretçiyi, tamsayı değişkeninin adresine eşitleyin. İşaretçi aracılığıyla tamsayı değişkeninin değerini değiştirin ve ekrana yazdırın.