Veri Yapıları ve Programlama

2 - Fonksiyonlar, Diziler ve İşaretçiler

Emre Can Yılmaz

Ondokuz Mayıs Üniversitesi

2025

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.

Sözdizimi

geri_dönüş_tipi fonksiyon_adı(parametre_listesi) {
  // Fonksiyon gövdesi (işlemler)
  return değer; // İsteğe bağlı
}

Ö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() {
  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.
  • Fonksiyon prototipleri, fonksiyonun tanımından önce yer alır.

Sözdizimi:

geri_dönüş_tipi fonksiyon_adı(parametre_listesi);

Örnek

#include <stdio.h>

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

int main() {
  int x = 2, y = 3;
  int sonuc = usAl(x, y); // Fonksiyon prototipi sayesinde derleyici bu çağrıyı anlayabilir.
  printf("%d üssü %d = %d\n", x, y, sonuc);
  return 0;
}

// Fonksiyon tanımı
int usAl(int taban, int us) {
  int sonuc = 1;
  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 ve her alt problem için kendini tekrar çağırır. Bu işlem, temel bir duruma ulaşılana kadar devam eder.
int faktoriyel(int n) {
  if (n == 0) {
    return 1; // Temel durum: 0! = 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.

Sözdizimi:

veri_tipi dizi_adı[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 };
// ucuncu elemanın degerini -1 yap
sayilar[2] = -1;// besinci elemanın degerini 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() {
  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, dizinin toplam boyutunu bir elemanın boyutu ile bölebiliriz. Yani:
  • dizi_boyutu = sizeof(dizi) / sizeof(dizi[0])

Örnek

#include <stdio.h>

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

    int dizi_boyutu = sizeof(sayilar) / sizeof(sayilar[0]);
    printf("Dizinin boyutu: %d\n", dizi_boyutu);
}

sayilar bellekte 20 baytlık bir alanda saklansın. Bu dizi içerisindeki ilk eleman yani 10 sayısı 4 bayt’lık yer kaplıyorsa. 20 / 4 = 5 sonucunu verecektir. Yani sayilar dizisi 5 elemanlıdır diyebileceğiz.

Diziler ve Fonksiyonlar

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

Ö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() {
  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

  • Bilgisayarın belleği, ardışık olarak numaralandırılmış, her biri bir bayt (byte) veri saklayabilen küçük 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). * operatörüne, bu bağlamda, dereference 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 = &değişken_adı; // &: Adres operatörü

Örnek

#include <stdio.h>

int main() {
  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", &sayi);
  printf("ptr'nin değeri: %p\n", 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() {
  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() {
  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ı, dizinin ilk elemanının bellek adresini temsil eden bir işaretçidir.

Örnek

#include <stdio.h>

int main() {
  int sayilar[5] = {10, 20, 30, 40, 50};
  int *ptr = sayilar; // ptr, sayilar dizisinin ilk elemanını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;
}

NULL Pointer

  • NULL, bir işaretçinin hiçbir adresi işaret etmediğini belirtmek için kullanılan özel bir değerdir.
  • stdio.h başlık dosyasında tanımlıdır.
  • Değeri 0’dır.

Örnek

#include <stdio.h>

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

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

  return 0;
}

İş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: Fonksiyonlara büyük veri yapıları geçirmeden, sadece adreslerini geçirerek performansı artırır ve bellek kullanımını azaltır.
  • Veri Yapıları: İşaretçiler, bağlı listeler, ağaçlar gibi karmaşık veri yapıları oluşturmak için kullanılır. (İlerleyen bölümlerde detaylı olarak işlenecek.)

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

  • İşaretçileri İlklendirme: Kullanmadan önce işaretçilere bir adres veya NULL değeri atamak önemlidir.
  • Bellek Sızıntıları: Dinamik olarak ayrılan bellek alanları, kullanıldıktan sonra serbest bırakılmalıdır (free() fonksiyonu 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 sayları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.