C Programlama Dili'ne Giriş | ||||||||||||||||||||||||||||||||||||||
* Giriş
*
* C (wikipedia)
|
Ders 11: Gösterici (Pointer) Kavramı
Giriş Hemen hemen bütün programlama dillerinin temelinde gösterici (pointer) veri tipi bulunmaktadır. Bir çok dil gösterici kullanımını kullanıcıya sunmamıştır veya çok sınırlı olarak sunmuştur. Fakat C Progrmalama Dili'nde göstericiler yoğun olarak kullanılır. Hatta gösterici kavramı C dilinin bel kemiğidir. Kavranması biraz güç olan göstericiler için -latife yapılıp- C kullanıcılarını "gösterici kullanabilenler ve kullanmayanlar" olmak üzere iki gruba ayıranlar da olmuştur. Özetle, bir C programcısı gösterici kavramını anlamadan C diline hakim olamaz. Türkçe yazılan C kitaplarda pointer kelimesi yerine aşağıdaki ifadelerden biri karşılaşılabilir:pointer = işaretçi = gösterici = gösterge Anlatımda, gösterici terimini kullanacağız. 11.1 Değişken ve Bellek Adresi Bilgisayarın ana belleği (RAM) sıralı kaydetme gözlerinden oluşmuştur. Her göze bir adres atanmıştır. Bu adreslerin değerleri 0 ila belleğin sahip olduğu üst değere bağlı olarak değişebilir. Örneğin 1GB MB bir bellek, 1024*1024*1024 = 1073741824 adet gözden oluşur. Değişken tiplerinin bellekte işgal ettiği alanın bayt cinsinden uzunluğu sizeof() operatörüyle öğrenildiğini hatırlayın. (bkz: Program 2.1). Bir programlama dillinde, belli bir tipte değişken tanımlanıp ve bir değer atandığında, o değişkene dört temel özellik eşlik eder:
Örneğin tam adlı bir tamsayı değişkenini aşağıdaki gibi tanımladığımızı varsayalım: int tam = 33; Bu değişken için, int tipinde bellekte (genellikle herbiri 1 bayt olan 4 bayt büyüklüğünde) bir hücre ayrılır ve o hücreye 33 sayısı ikilik (binary) sayı sitemindeki karşılığı olan 4 baytlık (32 bitlik): 00000000 00000000 00000000 00100001 sayısı elektronik olarak yazılır. tam değişkenine ait dört temel özellik Şekil 11.1'deki gibi gösterilebilir:
Bellek adresleri genellikle onaltılık (hexadecimal) sayı sisteminde ifade edilir. 0x3fffd14 sayısı onluk (decimal) sayı sisteminde 67108116 sayına karşık gelir. Bunun anlamı, tam değişkeni, program çalıştığı sürece, bellekte 67108116. - 67108120. numaralı gözler arasındaki 4 baytlık hücreyi işgal edecek olmasıdır. Şekil 11.1'deki gösterim, basit ama anlaşılır bir tasvirdir. Gerçekte, int tipindeki tam değişkeninin bellekteki yerleşimi ve içeriği (değeri) Şekil 11.2'de gösterildiği gibi olacaktır.
Değişkenin saklı olduğu adres, & karakteri ile tanımlı adres operatörü ile öğrenilebilir. Bu operatör bir değişkenin önüne konursa, o değişkenin içeriği ile değil adresi ile ilgileniliyor anlamına gelir. Aşağıdaki program parçasının: int tam = 33; printf("icerik: %d\n",tam); printf("adres : %p\n",&tam); çıktısı: icerik: 33 adres : 3fffd14 şeklindedir. Burada birinci satır tam değişkeninin içeriği, ikinci ise adresidir. Adres yazdırılırken %p tip belirleyicisinin kullanıldığına dikkat ediniz. 11.2 Gösterici Nedir? Gösterici, bellek alanındaki bir gözün adresinin saklandığı değişkendir. Göstericilere veriler (yani değişkenlerin içeriği) değil de, o verilerin bellekte saklı olduğu hücrenin başlangıç adresleri atanır. Kısaca gösterici adres tutan bir değişkendir. Bir gösterici, diğer değişkenler gibi, sayısal bir değişkendir. Bu sebeple kullanılmadan önce program içinde bildirilmelidir. Gösterici tipindeki değişkenler şöyle tanımlanır: tip_adı *gösterici_adı; Burada tip_adı herhangi bir C tip adı olabilir. Değişkenin önünedeki * karakteri yönlendirme (indirection) operatörü olarak adlandırılır ve bu değişkenin veri değil bir adres bilgisi tutacağını işaret eder. Örneğin: char *kr; /* tek bir karakter için */ int *x; /* bir tamsayı için */ float *deger, sonuc; /* deger gösterici tipinde, sonuc sıradan bir gerçel değişkenler */ Yukarıda bildirilen göstericilerden; kr bir karakterin, x bir tamsayının ve deger bir gerçel sayının bellekte saklı olduğu yerlerin adreslerini tutar. Bir göstericiye, bir değişkenin adresini atamak için adres operatörünü kullanabiliriz. Örneğin tamsayı tipindeki tam adlı bir değişken ve ptam bir gösterici olsun. Derleyicide, aşağıdaki gibi bir atama yapıldığında: int *ptam, tam = 33; . . . ptam = &tam; ptam göstericisinin tam değişkeninin saklandığı adresi tutacaktır. Bu durum Şekil 11.3'deki gibi tasvir edilir.
Şekil 11.3'deki gösterimde, ptam göstericisinin içeriği tam değişkeninin içeriği (33) değil adresidir (0x3fffd14). Ayrıca, ptam değişkeni, bellekte başka bir hücrede saklandığına ve bu hücrenin int değil int * tipinde bir bölge olduğuna dikkat ediniz. Buraya kadar anlatılanlar, Program 11.1'de özetlenmiştir. Program 11.1: Bir değişkenin içeriğini ve adresini ekrana yazdırma
7. satırda değişkenler bildirilmiştir. 9. satırdaki atama ile tam değişkeninin adresi, ptam göstericisine atanmıştır. Bu satırdan itibaren ptam, tam değişkeninin gösterir. 11. satıda tam'ın içeriği (33 sayısı), 12. ve 13. satırda tam'ın adresi, %p tip karakteri ile, ekrana yazdırılmıştır. Ekran çıktısı incelendiğinde, &tam ve ptam içereriğinin aynı anlamda olduğu görülür. ÇIKTI
tam adlı değişkenin içeriğine ptam gösterici üzerinde de erişilebilir. Bunun için program içinde ptam değişkeninin önüne yönelendirme operatörü (*) koymak yeterlidir. Yani *ptam, tam değişkeninin adresini değil içeriğini tutar. Buna göre: *ptam = 44; komutuyla, ptam'ın adresini tuttuğu hücreye 44 değeri atanır. Bu durum, Program 11.2'de gösterilmiştir. Program 11.2: Bir değişkenin içeriğini ve adresini ekrana yazdırma
ÇIKTI
11.3 Gösterici Aritmetiği Göstericiler kullanılırken, bazen göstericinin gösterdiği adres taban alınıp, o adresten önceki veya sonraki adreslere erişilmesi istenebilir. Bu durum, göstericiler üzerinde, aritmetik işlemcilerin kullanılmasını gerektirir. Göstericiler üzerinde yalnızca toplama (+), çıkarma (-), bir arttırma (++) ve bir eksiltme (--) operatörleri işlemleri yapılabilir. Aşağıdaki gibi üç tane gösterici bildirilmiş olsun:char *kar; int *tam; double *ger; Bu göstericiler sırasıyla, bir karakter, bir tamsayı ve bir gerçel sayının bellekte saklanacağı adreslerini tutar. Herhangi bir anda, tuttukları adresler de sırasıyla 10000 (0x2710), 20000 (0x4e20) ve 30000 (0x7530) olsun. Buna göre aşağıdaki atama işelemlerinin sonucu: kar++; tam++; ger++; sırasyla 10001 (0x2711), 20004 (0x4e24) ve 30008 (0x7538) olur. Bir göstericiye ekleme yapıldığında, o anda tuttuğu adres ile eklenen sayı doğrudan toplanmaz. Böyle olsaydı, bu atamaların sonuçları sırasıyla 10001, 20001 ve 30001 olurdu. Gerçekte, göstericiye bir eklemek, göstericinin gösterdiği yerdeki veriden hemen sonraki verinin adresini hesaplamaktır. Genel olarak, bir göstericiye n sayısını eklemek (veya çıkarmak), bekllekte gösterdiği veriden sonra (veya önce) gelen n. elemanın adresini hesaplamaktır. Buna göre aşağıdaki atamalar şöyle yorumlanır. kar++; /* kar = kar + sizeof(char) */ tam = tam + 5; /* tam = tam + 5*sizeof(int) */ ger = ger - 3; /* ger = ger - 3*sizeof(double) */ Program 11.3, bu bölümde anlatlanları özetlemektedir. İnceleyiniz. Program 11.3: Gösterici aritmetiği
ÇIKTI
11.4 Gösterici ve Diziler Arasındaki İlişki C dilinde göstericiler ve diziler arasında yakın bir ilişki vardır. Bir dizinin adı, dizinin ilk elemanının adresini saklayan bir göstericidir. Bu yüzden, bir dizinin herhangi bir elemanına gösterici ile de erişilebilir. Örneğin: int kutle[5], *p, *q; şeklinde bir bildirim yapılsın. Buna göre aşağıda yapılan atamalar geçerlidir: p = &kutle[0]; /* birinci elemanın adresi p göstericisne atandı */ p = kutle; /* birinci elemanın adresi p göstericisne atandı */ q = &kutle[4]; /* son elemanın adresi q göstericisne atandı */ İlk iki satırdaki atamalar aynı anlamdadır. Dizi adı bir gösterici olduğu için, doğrudan aynı tipteki bir göstericiye atanabilir. Ayrıca, i bir tamsayı olmak üzere, kutle[i];ile *(p+i); aynı anlamdadır. Bunun sebebi, p göstericisi kutle dizisinin başlangıç adresini tutmuş olmasıdır. p+i işlemi ile i+1. elemanın adresi, ve *(p+i) ile de bu adresteki değer hesaplanır.
Program 11.4'de tanımlanan fonksiyon kendine parameter olarak gelen n elemanlı bir dizinin aritmetik ortlamasını hesaplar. Program 11.4: Bir dizi ile gösterici arasındaki ilişki
ÇIKTI
20. - 31. satırda tanımlanan fonksiyon aşağıdaki gibi de yazılabilirdi: double ortalama(double dizi[], int n) { double *p, t=0.0; for(p=dizi; p < &dizi[n]; p++) t += *p; return (t/n); } Bu fonksiyonda, döngü sayacı için (i değişkeni) kullanılmayıp, döngü içinde dizinin başlangıç adresi p göstericisine atanmış ve koşul kısmında adres karşılaştırılması yapılmıştır. Bu durumda döngü, p'nin tuttuğu adresten başlar, ve p'nin adresi dizinin son elemanının adresinden (&dizi[n-1]) küçük veya eşit olduğu sürece çevrim yinelenir. 11.5 Fonksiyon Parametresi Olan Göstericiler C (ve C++) programlama dilinde fonksiyon parametreleri değer geçerek (pass by value) yada adres geçerek (pass by reference) olarak geçilebilir. Bölüm 8'deki uygulamalarda fonksiyonlara parametreler değer geçerek taşınmıştı. Bu şekilde geçirilen parametreler, fonksiyon içersinde değiştirilse bile, fonksiyon çağılıldıktan sonra bu değişim çağrılan yerdeki değerini değiştirmez. Fakat, bir parametre adres geçerek aktarılısa, fonksiyon içindeki değişikler geçilen parametreyi etkiler. Adres geçerek aktarım, gösterici kullanmayı zorunlu kılar. Örneğin, Program 11.5'de fonksiyonlara değer ve adres geçerek aktarımın nasıl yapılacağı gösterilmiştir. Program 11.5: Bir değişkenin içeriğini ve adresini ekrana yazdırma
5. ve 6. satırlada kendine geçilen parametrenin değerini alan f1 fonksiyonu ve parametrenin adresini alan f2 adlı iki fonksiyon örneği belirtilmişdir. 11. satırdaki x değişkeni 16. ve 21. satırlarda, f1(x) ve f2(&x) fonksiyonlarına, sırasıyla değer ve adres geçerek aktarılmıştır. f1 içinde x (n = 66; işlemi ile) değişime uğramış, fakat çağrılma işleminin sonucunda, x'in değeri değişmemiştir. Ancak f2 içinde x'in ( *n = 77 işlemi ile) değişimi, çağrıldıktan sonrada korunmuştur. Yani, adres geçerek yaplıan aktarımda, f2'ye aktarılan değer değil adres olduğu için, yollanan x parametresi f2 içinde değişikliğe uğrayacak ve bu değişim çağrıldığı 21. satırdan itibaren devam edecektir. ÇIKTI
Program 11.6'da iki tamsayı değişkeninin nasıl takas (swap) edileceği gösterilmiştir. Bu işlemi C porgramlama dilinde, eğer değişkenler global olarak bildirilmemişse, gösterici kullanmadan bu işlemi yapmak imkansızdır. Program 11.6: İki tamsayının birbiri ile takas edilmesi
ÇIKTI
11.6 Geri Dönüş Değeri Gösterici Olan Fonksiyonlar Fonkiyonların geri dönüş değeri bir gösterici olabilir. Bu durumda fonksiyon bir değer değil adres döndürecek demektir. Program 11.7'da önce bir dizinin indisleri, dizi değerleri ve dizi elemanlarının adresleri ekrana basılır. Daha sonra, maxAdr(); fonksiyonu ile dizinin en büyük elemanının adresi döndürülür. Bu örnek progam, göstericilerin gücünü çok zarif bir biçimde bize sunmaktadır. Lütfen inceleyiniz. Program 11.7: Bir dizinin en büyük elemanının adresini öğrenmek
Dizi elemanları 21. satırda belirlenir. Bu dizinin indisleri, değerleri ve adresleri 26. satırda ekrana basılmıştır. En büyük elemanın adresi 29. satırdaki p = maxAdr(a,6); ile p göstericisine atanmıştır. 5. satırda bildirilen maxAdr(); fonksiyonu, en büyük elemanın adresini hesaplayıp çağrılan yere gönderir. Burada dikkat edilmesi gereken husus, fonksiyonun dönüş değerinin yerel eba göstericisi olmasıdır. eba göstericisi 12. satırda hesaplanan ve fonksiyon parametersi olan dizinin en büyük elemanın adresini tutmaktadır. Son olarak, fonksiyon çağırıldıktan sonra, p göstericisin gösterdiği değer, tuttuğu adres ve dizinin birinci elemanına göre konumu (indisi) ekrana basılmıştır. Indis hesabı int(p-&x[0]) işlemi ile yapılabilir. Bu aslında, p göstericisin tuttuğu adres ile dizinin ilk elemanının adresi arasındaki farktır. Sonuç yine bir adres olduğu için tamsayı değer elde etmek için int() takısı kullanılmıştır. Netice itibarıyla bir fonksiyon ile üç şey aynı anda öğrenilmiş olur. ÇIKTI
11.7 Fonksiyon Göstericileri
Fonksiyon göstericileri, gösterici (pointer) kavramının gücünü gösterin diğer bir uygulama alanıdır.
Dizilerde olduğu gibi, fonksiyon adları da sabit göstericidir. Fonksiyon adının bellete yer işgal ettiği şöyle öğrenilebilir: int f(int); /* fonksiyon bildirimi */ int (*pf)(int); /* fonksiyon göstericisi bildirimi */ pf = &f; /* f'nin adresini pf'ye ata! */ Program 11.8: Bir fonksiyonun 'adresini' iki yoldan öğrenme
ÇIKTI
Aşağıdaki ikinci örnekte, bir fonksiyon diğer fonksiyona parametre olarak geçirilmiş ve sayısal türevi hesaplanmıştır. Türev hesaplanırken merkezi fark yaklaşımı (central difference approximation) yöntemi kullanılmıştır.
Program 11.9: Türev alan fonksiyon
ÇIKTI
11.8 NULL Gösterici Bir göstericinin bellekte herhangi bir adresi göstermesi, veya öncden göstermiş olduğu adres iptal edilmesi istemirse NULL sabiti kullanılır. Bu sabit derleyicide ASCII karakter tablosunun ilk karakteridir ve '\0' ile sembolize edilir. int *ptr, a = 12; . . ptr = &a; /* ptr bellekte a değişkenin saklandığı yeri gösteriyor */ . . ptr = NULL; /* ptr bellekte hiç bir hücreyi göstermiyor */ *ptr = 8 /* hata! NULL göstericinin gösterdiği yere bir değer atanamaz */ 11.9 void Tipindeki Göstericiler void göstericiler herhangi bir veri tipine ait olmayan göstericilerdir. Bu özelliğinden dolayı, void gösterici genel gösterici (generic pointer) olarak da adlandırılır. void göstericiler, void anahtar sözcüğü ile bildirilir. Örneğin: void *adr;gibi. void göstericiler yalnızca adres saklamak için kullanılır. Bu yüzden diğer göstericiler arasında atama işlemlerinde kullanılabilir. Örneğin aşağıdaki atamada derleyici bir uyarı veya hata mesajı vermez: void *v; char *c; . . . v = c; /* sorun yok !*/ Program 11.10'de void tipindeki bir göstericinin, program içinde, farklı tipteki verileri nasıl göstereceği ve kullanılacağı örneklenmiştir. İnceleyiniz. Program 11.10: void gösterici ile farklı tipteki verileri gösterme
ÇIKTI
Benzer olarak, fonksiyon parameterelerinin kopyalanması sırasında da bu türden atama işlemleri kullanılabilir. Uygulamada, tipten bağımsız adres işlemlerinin yapıldığı fonksiyonlarda, parametre değişkeni olarak void göstericiler kullanılır. Örneğin void free (void *p) { . . . } Parametresi void *p olan free fonksiyonu, herhangi türden gösterici ile çağrılabilir. |