Temel C Programlama -31- İşaretçiler

Önceki bölümde işaretçilerin bellek adreslerine erişmemizi ve bunların içindeki değerleri değiştirebilmemizi sağlayan bir özellik olduğundan bahsetmiştik. Yalnız işaretçiler uygulamada bu anlattığımız kadar basit bir iş için kullanılmamaktadır. Fonksiyonlarda referans ile aktarma yapabilir, fonksiyonlar arası fonksiyonları aktarabilir ve dinamik veri yapıları oluşturabiliriz. Bu oluşturduğumuz veri yapıları önceden anlattığımız değişkenler, diziler veya sonra göreceğimiz yapılar gibi sabit değildir. Dinamik veri yapıları program işletilirken büyüyüp küçülebilir. Ayrıca veri yapılarından bahsetmişken veri yapılarının bilgisayar bilimleri arasında önemli ve ayrı bir yeri olduğunu söyleyelim. Bağlı liste, sıra, yığın ve ağaç gibi veri yapıları sadece C dilinde değil bütün programlama dillerinde karşımıza çıkacak genel konulardandır.

İşaretçi Değişkeni Tanımlama

İşaretçileri kullanabilmek için öncelikler içerisinde “Adres verisi” barındıran bir işaretçi değişkeni tanımlamamız gereklidir. Bu işaretçi değişkeni normal değişkenlere benzeyip kendine ait bir adresi olsa da diğer değişkenler gibi bir sayı verisi barındırmamaktadır. Barındırabildiği tek değer bir adres verisi olmaktadır. Bir işaretçi değişkeninin adres barındırabilmesi için yine bir değişkene ait adresi barındırması gereklidir. Yani bir tarafta işaretçi değişkeni diğer tarafta ise işaretçi değişkeninin “işaret ettiği” gerçek değişken olmalıdır. İşaretçi değişkeni adından da anlaşılacağı gibi kendisine ulaşıldığında bizi işaret ettiği değişkene götürmelidir.

Aşağıdaki örnek söz diziminde bir işaretçi tanımlanmıştır. İşaretçiler de değişkenlerde olduğu gibi kullanmamız için öncelikle tanımlanmak zorundadır.

int *isaretci_degiskeni;

Burada “*” işareti tanımlama sırasında bu değişkenin işaretçi değişkeni olduğunu söylemektedir. Dikkat etmeniz gereken en önemli nokta “*” işaretinin program akışında farklı bir anlama geldiği, tanımlama sırasında ise farklı bir anlama geldiğidir. Program akışında bu işaret işaretçinin işaret ettiği değişkendeki değere erişmemizi sağlasa da tanımlama sırasında değişkenin işaretçi olduğunu derleyiciye söylemektedir. int* isim veya int *isim şeklinde tanımlamalar geçerli olsa da siz int *isim şeklinde yıldızın isme yanaşık olduğu şekli kullanmalısınız. Burada int tipinde bir tam sayı değişkenin adresini içinde barındıracak bir işaretçi tanımlanmaktadır. Aynı değişken tipleri olduğu gibi işaretçi tipleri de vardır ve aynı tipteki işaretçi aynı tipteki değişkeni işaret edebilir. Siz int tipindeki bir işaretçi ile float tipinde bir değişkeni ilişkilendiremezsiniz.

İşaretçiye Değer Atama

İşaretçiler işaret ettikleri bir değişkenin adres verisini bulundurmadan bir işe yaramazlar. Siz de bir işaretçi tanımladığınız zaman öncelikle bu işaretçiye bir değer atamanız gerekecektir. İşaretçiye atadığınız değer işaret edilecek değerin adresi olmalıdır. Başta söylediğimiz gibi işaretçiler adresten başka bir değer alamaz. Biz bir değişkenin adını yazdığımızda o değişkenin değerini yazmış gibi oluruz. Yani o değişkeni doğrudan referans etmiş oluruz. İşaretçiye bir değer atamamız gerektiğinde o değişkenin adresini atamamız gerektiğinden addressof (&) operatörünü kullanırız. Bu operatörü scanf() fonksiyonundan bilmeniz gerekir. scanf() fonksiyonunda da bu operatör ile değişkenlerin adresleri fonksiyona aktarılmaktadır. Böylelikle scanf() fonksiyonu doğrudan değişkenlere erişebilmiş olur ve değişkenler kopyalanarak boş yere hafıza işgal edilmez.

int degiskenimiz = 5;
int *degisken_isaretcisi;
degisken_isaretcisi = &degiskenimiz;

Görüldüğü gibi öncelikle normal bir değişken tanımladıktan sonra bir de işaretçi tanımlıyoruz. Orada tanımladığımız işaretçi boş halde. Biz bunu önceki tanımladığımız değişkeni işaret etmek için kullanacağız. Bunun için önceki tanımladığımız degiskenimiz adındaki değişkenin adresine ihtiyacımız var. Bu adres değerini “&” operatörü ile alıyoruz ve degisken_isaretcisi adındaki işaretçi değişkenine aktarıyoruz. Bundan böyle degisken_isaretcisi işaretçi değişkeninin içerisi boş olmayacak ve degiskenimiz değişkeninin adresini içerecektir. Biz bu işaretçiye eriştiğimiz zaman bizi degiskenimiz değişkeninin değerine götürecektir. Eğer boş bir işaretçi tanımlarsak muhakkak 0 veya NULL değerini atamamız gereklidir. Programın ilerleyen kısımlarında farklı değerleri atayabilsek de ilk tanımlamada boş bırakmamak gereklidir.

Bunu böyle neden tanımladığımızı sorabilirsiniz. Normalde degiskenimiz diye tanımladığımız tam sayı değişkeninin değerine erişebiliyor ve üzerinde tam manasıyla bir işlem yapabiliyorduk. Burada bir işaretçi degiskenimiz adlı değişkenin adresini aldı ve istediğimiz vakit bu işaretçi vasıtasıyla de degiskenimiz değişkenine erişip değerini okuyacak veya değiştirebileceğiz. Bunu normalde değişkeni yazarak da yapabilsek de C dilinde “kapsam” adı verilen bir değer erişim kısıtlamasının olduğunu unutmayın. Bir değişken global olmadığı müddetçe ancak ilgili kapsamda erişilebilir. Bu bir fonksiyon bloku veya döngü bloku olabilir. Üst kapsamdaki değişken alt kapsamlar tarafından erişilebilse de alt kapsamdaki değişken üst kapsamlar tarafından veya aynı statüde farklı kapsamlar tarafından erişilemez. Kapsamların { ve } işaretleri arasındaki bölge olduğunu biliyoruz. Bu değişkenin adresini bilmekle farklı kapsamlar tarafından da erişme imkanına sahip oluruz. Çünkü doğrudan adrese ulaşıyoruz!. Eğer değişkenimiz auto yani geçici değişken ise kapsam dışında yok edildiğini unutmayın fakat static tipinde bir değişken bu şekilde erişilebilir. Ama işin aslına bakarsanız işaretçilerin değişkenlerle de pek fazla işi yoktur.

İşaretçinin İşaret Ettiği Değişkenin Değerine Erişme (Indirection)

Dolaylı erişim adını verebileceğimiz indirection kavramı programlama dillerinden öte mikroişlemci mimarisinin ana yapılarından biridir. Mikroişlemci komutlarına baktığımızda dolaylı erişim ve dolaylı atlama gibi komutların olduğunu görürüz. Program akışı bizim yazdığımız bir sabit adres değerine göre değil bir yazmaçta bulunan değişken adres değerine göre yönlendirilebilir. Bu dolaylı erişim her seviyede programa büyük bir esneklik katmaktadır. Yukarıda bir değişken tanımladık, sonrasında ise bir işaretçi değişkeni tanımladık, en sonunda & operatörü ile değişkenin adresini işaretçi değişkenine yükledik. Artık son yapacağımız iş ise değişkene “işaretçi üzerinden” erişebilmektir. Bunun için “*” operatörünü kullanırız fakat yukarıda söylediğimiz gibi bu operatör ile işaretçi tanımlama operatörü birbirinden farklıdır. Burada bu operatöre dolaylı erişim operatörü (Indirection Operator) adı verilmektedir. Şimdi degiskenimiz değişkeninin değerine erişmek için bir kod yazalım.

printf("%i", *degisken_isaretcisi);

Burada eğer doğrudan degisken_isaretcisi işaretçi değişkenini yazdırmak isteseydik bize adres değerini verecekti. Elbette bunu printf() fonksiyonu ile %i formatında yazdırmamız mümkün olmadığından işaretçi formatında yani %p formatında yazdırabilecektik. Ama burada işaretçi değişkeni işaret ettiği değişkenin değerini yazdırmakta. Bizim normal değişkenimiz ise int tipinde olduğu için format olarak tam sayı formatını seçtik. Ekran görüntüsü “5” değerini gösterecektir. “*” operatörü burada adres değerinde bulunan değeri getirme görevini yapmaktadır.

İşaretçi Operatörleri Özeti

Bu işaretçi operatörlerinin ne işe yaradığını tam olarak kavramadıkça işaretçileri anlamanız ve kodları incelemeniz beklenemez. Matematik operatörlerini (+, – gibi) kolayca anlayabilsek de bunlar yeni bir konuya ait ve alakasız gibi görünen operatörler olduğu için kafamızı karıştıracaktır. Özellikle değişken tanımlama ve değer erişme sırasında kullandığımız “*” işaretinin kullanılması kafanızı karıştıracaktır. Tanımlama sırasında bu işaret operatör olarak değil belirtme işareti olarak kullanılmaktadır. Yani bu tanımlanan değişkenin işaretçi olduğunu söylemektedir. Ötekinde ise operatör olarak değer erişim operatörü olarak görev yapmaktadır. pointer_int, pointer_float demek yerine int*, float* demişler. Bunun işaretçi operatörleri ile ilgisi yoktur.

İşaretçi operatörleri ise program akışında yer alan & ve * operatörleridir. & operatörü adres operatörü olup bir değerin adresini geri döndürmektedir. Normal bir şekilde bir değere eriştiğimiz zaman bunun değerini alırız. İşaretçiye normal bir şekilde eriştiğimiz zaman ise adres değeri alırız. Yani & operatörü işaretçi olmayanlar ile beraber kullanılmaktadır. İşaretçiler zaten normal olarak adres değerini vermektedir. “*” operatörü ise dolaylı erişim operatörüdür. Bildiğiniz üzere işaretçi değişkenleri normal erişim sırasında adres değerini bize vermekte. O halde işaret ettikleri adresin değerini elde etmemiz için özel bir operatör kullanmamız gerekli. Bunu & operatörünün tersi olarak da düşünebilirsiniz. “*” operatörü ile işaretçilerin adres değerinde yer alan veri değerini elde ederiz. Bunu da normal bir değişken ile kullanmamızın bir anlamı olmayacaktır çünkü normal değişken bize adres değil değer verisini vermektedir.

Şimdi bütün bu anlattıklarımızın örnek programda nasıl çalıştığını görelim.

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
	int degisken = 5;
	int* isaretci;
	isaretci = &degisken;
	
	printf("Bizim Degiskenin Degeri: %i \n", degisken);
	printf("Bizim Degiskenin Adresi: %p \n", &degisken); // Burada işaretçi yok sadece adres var.
	printf("Isaretcinin Degeri: %p \n", isaretci);
	printf("Isaretcinin Isaret Ettigi Deger:%i \n", *isaretci);
	
	return 0;
}

Burada yukarıda anlattıklarımızın uygulamasını görebilirsiniz. Sizin de bir bakışta programı anlayacağınızı umuyorum. Öncelikle yukarıdaki kod parçasına benzer bir şekilde bir değişken ve bir işaretçi değişkeni tanımladık. Sonrasında ise tanımladığımız değişken ile işaretçi değişkenini ilişkilendirdik. Sonrasında ise bütün bu bahsettiğimiz verileri printf() fonksiyonu ile uygun formatlarda yazdırdık. Tam sayı için %i ve işaretçi (adres) tipi için %p format belirtecinin kullanıldığını biliyorsunuz. Burada öncelikle bizim bildiğimiz üzere değişkenin değerini yazdırdık ve sonrasında &degisken ile adres operatörünün döndürdüğü adres değerini yazdırdık. Sonrasında ise isaretci değişkenini doğrudan yazdık çünkü biz o işaretçi değişkenini yazdığımız zaman içindeki adres değeri oraya gidecek. O halde format olarak %p yazmamız gerekti. Sonrasında ise işaretçinin adresinde bulunan veriyi çıkarmak için * operatörünü isaretci işaretçi değişkeniyle kullandık ve bu veriyi %i formatında yazdırdık. Çünkü *isaretci kısmı işletildiği zaman ortada 5 sayısından başka bir şey kalmayacaktır.

Şimdi programın çıktısına bakalım ve nasıl olduğunu görelim.

Düzeltme: Yukarıda işaretçinin adresi diye belirttiğim kısım “İşaretçinin içinde bulundurduğu adres verisi” demektir. İşaretçi değişkeninin kendi adresi aynı değişkenlerin adresi gibidir ve belleğin başka bir kısmında yer almaktadır.

Burada gördüğünüz üzere değişkenin değeri ile işaretçinin işaret ettiği değer birbirinin aynı olmakta. Değişkenin adresi ile de işaretçinin adresinin birbirinin aynı olduğunu görüyoruz. Bu aynı kimlik bilgilerinden T.C kimlik numarasını bulmak ve T.C kimlik numarasından ise kimlik bilgilerine ulaşmak gibi bir durum. Biz burada int tipinde sıradan bir değeri kullandığımız için çok da bir şey beklemememiz lazım. Çünkü işaretçilerin asıl kullanım alanları dizilerdir. Diziler bildiğiniz üzere belli bir sayıda değişkenin birbiri ardınca dizilmesinden oluşur. Yani dizi elemanları sayi[0], sayi[1], sayi[2] gibi sıralanırken bellekte 0x50, 0x51, 0x52 diye sıralanır. Bu durumda işaretçi önceki ve sonraki bellek adreslerinde dizinin elemanlarının olduğunu bilecektir. Tek bir bellek adresiyle yapabileceklerimiz çok fazla değildir.

Burada adresin oldukça uzun bir değer olduğunu görmekteyiz. 64-bitlik sistemde adres değerleri de bu kadar uzun olmaktadır. Eğer siz gömülü sistemler üzerinde çalışacaksanız bu adres değerlerinin oldukça kısaldığını görebilirsiniz. Görüldüğü gibi işaretçilerin temel yapısı oldukça basittir. Bunu zorlaştıran bunların nerelerde ve ne için kullanıldığıdır.

Last updated