C Programlama: Göstergeler

C programlama dilinin en güçlü özelliklerinden birisi olan göstergeler (pointers), genellikle C programlamaya yeni başlayanlar için zorlanılan konuların başında gelir. Bu yazıda; gösterici tanımlama ve kullanma, bellek erişimi, hafıza yönetimi ve fonksiyon parametreleri olarak göstergelerin kullanımlarına değineceğim.

Değişkenler ve Göstericilerin Hafızadaki Organizasyonu

Göstergeleri anlatmadan önce bir miktar değişkenlerin C’de nasıl tanımlandığına ve hafızada (RAM) nasıl organize olduklarına bir bakalım. int x=7; şeklindeki bir tanımlamayla integer (tamsayı) veritipinde x isminde bir değişken tanımlayıp içeriğine 7 değerini atamış oluyoruz. Bu ifadede gözükmeyen ancak arka planda tüm olayın üzerinde döndüğü bir parametre daha var ki ona da bu değişkenin adresi diyoruz. Bilgisayar mimarisi veya sayısal tasarım derslerinden hatırlayacak olursak makine dilinde bellekteki verileri kullanmak için (load/restore) bellek hücresinin adresine ihtiyaç duyuyoruz. Arkaplanda işler adres üzerinden dönüyor.

Göstergeler de aslında bir tür değişken ancak sadece başka değişkenlerin adreslerini saklıyorlar. Nasıl integer bir değişken tanımladığımızda bu değişkene belirli limitler dahilinde bir tamsayı değeri atayabiliyorsak, gösterge (pointer) tipi bir değişken tanımladığımızda aynı tipteki başka bir değişkenin adresini depolayabiliyoruz. Şimdi bir örnek verelim:

 1: int x=7;
 2: int *y;
 3: y=&x;
 4: printf("%d\n",*y); //7

Bu kod parçasında, önce x isimli tamsayı bir değişken tanımlıyoruz ve bu değişkene 7 değerini atıyoruz. İkinci satırda y isimli bir tamsayı göstergesi tanımlıyoruz. Üçüncü satırda x değişkenin adresini y göstergesine atıyoruz. Dördüncü satırda ise y göstergesinin gösterdiği değeri ekrana yazdırıyoruz. Belleğin temsili görüntüsü şu şekilde:

Bellek Adresleme

Tabii ki bu temsili gösterimde adres değerleri hayali. Ancak dikkat edilirse, 7 değerine sahip x değişkeni belleğin 1000 numaralı adresinde, 1002 numaralı adresteki y göstergesi ise aslında x’in adresi olan 1000 değerini depoluyor. Yukarıdaki kod parçasında yeni karşılaştığımız iki yeni operatör var: * (yönlendirme, indirection) ve & (adres). Ancak dikkat edelim, 2. satırdaki * karakterini, az önce bahsettiğim yönlendirme operatörü ile karıştırmayalım. 2. satırdaki değişken isiminden hemen önce gelen * karakteri, ilgili değişkenin gösterge tipinde olduğunu tarif ediyor. Yönlendirme operatörü olan *’dan birazdan bahsedelim.

Gösterge Operatörleri

& (adres) operatörü bir değişkenin bellekteki adresini öğrenmemizi sağlıyor. 3. satırda x’in adresini öğrenip y’ye atıyoruz. Yukarıdaki görsele göre y=1000.

* (yönlendirme) operatör ise bir gösterge değişkenin göstermekte olduğu değişkenin değerini okuyor. Nasıl mı? *y olarak ifade ettiğimiz şey aslında y’de muhafaza edilen hafıza adresindeki değer. Örneğimizde y’nin değeri 1000 idi, *y diyerek aslında 1000 numaralı hafızadaki değere erişmiş oluyoruz. Dolayısıyla 4. satırda printf komutu ile *y’nin içeriğini ekrana yazdırdığımızda 7 değerini görüyoruz. Şimdi size bir soru:

 5: printf("%d\n",y);
 6: printf("%d\n",&y);

komutları nasıl bir ekran çıktısı verir? 5. satır y göstergesinin değerini yazdırıyor, y’nin değeri 1000 olduğuna göre ekranda önce 1000 ifadesi yazacak. 6. satırda ise y’nin adresini okuyup ekrana yazdırıyoruz, üstteki görsele göre y göstergesi 1002 nolu adreste olduğuna göre ekranda 1002 ifadesi yazacak.

Gelelim scanf komutu ile göstergelerin kullanımına.

 7: scanf("%d",&x);
 8: scanf("%d",y);

C dilini ilk öğrendiğimiz zamanları hatırlarsak normal bir değişkene klavyeden değer atamak için scanf komutunda & operatörünü kullanılmıştı. O zamanlar, eğitimcinize “hocam neden & ifadesini kullandık?” diye sorduysanız muhtelemen “daha sonra bu konuyu anlatacağız” diye bir cevap almışsınızdır. İşte o “daha sonra” şimdi!

scanf komutu yapısı itibariyle bir değişkene klavyeden veri atayabilmek için değişkenin adresini bilmesi gerekiyor. Biz de normal değişkenlere değer okumak için scanf komutuna 7.satırda da olduğu gibi değişkenin adresini gönderiyoruz (ampul yanmıştır umarım). Gelelim 8.satıra; gösterge bir değişkenin gösterdiği değişkene klavyeden değer atamak için doğrudan göstergenin adını yazıyoruz. & adres operatörüne ihtiyacımız yok çünkü y’nin içinde zaten x’in adresi mevcut. Dolayısıyla 8.satırda aslında y’nin kendisine değil, y’nin göstermekte olduğu x değişkenine klavyeden bir değer atamış oluyoruz. Bir anlamda 7.satır ile 8.satır aynı işleve sahip.

Fonksiyonlarda Gösterge Kullanımı

C programlama dilinde fonksiyonlara parametreler iki farklı yolla gönderilir: değer (call by value) ve referans (call by reference). Aşağıdaki programı inceleyelim:

#include <stdio.h>
//Iki degiskenin iceriklerini degistiren program (call by value)
void swap(int a, int b){
 int temp;
 temp=a;
 a=b;
 b=temp;
}
void main(){
 int x,y;
 x=3;
 y=5;
 printf("x:%d y:%d\n");
 swap(x,y);
 printf("x:%d y:%d\n");
}

Bu uygulama ile yapmaya çalıştığımız iki tamsayı değişkenin değerlerini yer değiştirme işlemidir. Ancak programın ekran çıktısı aşağıdaki gibi olacaktır:

 x:3 y:5
 x:3 y:5

Görüleceği üzere swap fonksiyonun çağrılmasından önceki ve sonraki x ve y değerlerinin içerikleri aynı kalmıştır. Çünkü main fonksiyonunun kapsama alanı (scope) içinde tanımlanan x ve y değerleri swap fonksiyonuna gönderildiğinde bu değişkenlere karşılık olarak swap fonksiyonunun kapsama alanı içinde a ve b değişkenleri tanımlanmış olup a ve b’nin içeriği değiştirilmesine karşın işlemci swap fonksiyonundan çıkıp main’e tekrar geri döndüğünde a ve b bellekte erişilemez duruma düşecek ve main’deki x ve y hala eski değerlerini koruyor olacaktır. Bu kullanıma “call by value” ismi verilmiştir. Yani fonksiyona değişkenin kendisi değil aslında değişkenin sadece içeriği gönderilmiştir. Yukarıdaki kodu bir miktar değiştirerek şu hale getirelim:

#include <stdio.h>
//Iki degiskenin iceriklerini degistiren program (call by reference)
void swap(int *a, int *b){
 int temp;
 temp=*a;
 *a=*b;
 *b=temp;
}
void main(){
 int x,y;
 x=3;
 y=5;
 printf("x:%d y:%d\n");
 swap(&x,&y);
 printf("x:%d y:%d\n");
}

Kırmızı renkle değişken satırları işaretlediğim bu örnekte ise swap fonksiyonu int tipinde gösterge değişkenler alacak şekilde tanımlanıyor. Dolayısıyla main içinde swap fonksiyonunu çağırırken x ve y’nin adresini (& adres operatörü ile) gönderiyoruz. Yani artık a ve b’nin içinde x ve y’nin değerleri olan 3 ve 5 rakamları değil, artık hafızada hangi adreste bulunuyorlarsa o adres değerleri gidiyor. swap fonksiyonu ise (* yönlendirme operatörü ile) x ve y’nin hafızadaki içeriklerine ulaşıp değişim işlemini gerçekleştiriyor. main’e döndüğümüzde swap’in kapsamında bulunan a ve b erişilemez oluyor. Zaten artık a ve b ile işimiz yok. Ancak önceki örnekten farklı olarak swap fonksiyonu main’in kapsamında bulunan x ve y’nin doğrudan içeriğine göstergeler aracılığı ile müdahale ettiğinden dolayı programın çıktısı artık şu şekilde olacaktır:

 x:3 y:5
 x:5 y:3

İşte fonksiyonlara bu şekilde değer göndererek kullanıma ise “call by reference” adı veriliyor.

Şimdilik bu kadar. Bir sonraki yazıda göstericiler (pointers) ile diziler arasındaki ilişkilere, dinamik ve çok-boyutlu gösterge dizilerine değineceğim. Görüşmek dileğiyle.

Rifat Kurban

Kaynak

N.E. Çağıltay, C.F. Selbes, G. Tokdemir, Ç. Turhan, C Dersi: Programlamaya Giriş

P. Deitel, H. Deitel, C: How to Program, 7th Edition

Bir Cevap Yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir