Pointer'landıramadıklarımızdan Mısınız?
Değişkenler Nereye ve Nasıl Kaydedilir?
Programlamanın bir aşamasında "Tanımladığımız değişkenler nereye gidiyor?" diye hepimiz düşünmüşüzdür. Cevap basit: RAM belleğe.
RAM bellek, alt alta kutular şeklinde tasvir edilir denk gelmişseniz. Her biri 8 bit yani 1 bayt yer kaplayan kutular halinde. 1 GB kapasitesi olan RAM'de 1.073.741.824 adet kutu vardır. Bu da 4 bayt yer tutan tamsayılar ile dolduracak olursak maksimum 268.435.456 adet tam sayı saklayabileceğimiz anlamına gelir. Her bir kutunun kendine özel, bellekte bulundukları konumu tarif eden adresi vardır. Bu sayede CPU kutulara veri yerleştirebilir ve yerleştirdiği verilere sonradan erişebilir.
Adresler sayıdır ancak bildiğimiz sayılardan değil. 16'lık (hexadecimal) tabanda ifade edilirler.
Örneklendirmeye bir tam sayı değişkeni tanımlayarak başlayalım:
Şunu anlamakta yarar var: bellekteki her değere adresi ile erişim sağlarız. Ancak programlama dillerinde bununla uğraşmamıza gerek bırakmayan değişken kavramı vardır. Biz bu ismi verdiğimizde CPU hangi adrese bakacağını anlar. Örnekteki
dogum_tarihi
dediğimizde CPU "He tamam, bu 1999 değerini istiyor." der.
Günlük yaşantımızda sayıları 10'luk tabanda (decimal) kullanıyoruz. Ancak bilgisayarlar bunları anlayabilmek için 2'lik tabana (binary) çevirmek zorunda. Bellek temsillerinde ise 16 tabanını (hexadecimal) kullanılıyor. Aşağıda 1999 değerinin bu tabanlardaki halini görebilirsiniz:
Değişkenin bellekteki şekline bakalım. 16'lık tabandaki karşılığını kutulara ikişerli olarak yerleştiririz:
İkişerli yerleştirme sebebimiz, 2'lik tabandaki her 8 bitin 16'lık tabanda 2 karaktere karşılık gelmesidir. Ve bu kısma özel bir isim vererek dogum_tarihi dedik:
Artık biz bu değişkeni çağırdığımızda CPU belleğe gider ve değişkenin başlangıcı adresi olan 90000008 adresine uğrar. Değişken int ise 4 bayt olacaktır. Dolayısıyla 4 kutu okur ve birleştirir. Yani 00 00 07 CF değerini elde eder ve 10 tabanına çevirerek 1999 sayısını verir.
Bellek Adresini Bilmek Ne İşimize Yarar?
Verinin kendisi yerine adresini kullanmak, onu kopyalamadan birden fazla yerden erişim sağlayabilmemizi ve değişiklik yapabilmemizi sağlar. Bellekte gereksiz yer işgalini önlemiş oluruz.
Bir otonom araba hayal edin. Kameradan gelen görüntüler belleğe kaydediliyor ve birden fazla programda kullanılıyor. Bir program bu görüntüler ile yolu takip ediyor, bir diğeri yayaları takip ediyor, başka bir tanesi ise trafik ışıklarını takip ediyor. Şimdi her program için görüntüleri kopyaladığımızı düşünün. Görüntülerin kaplayacağı alan birden 3 katına çıktı. Şimdi bir de bu programlara yalnızca resimlerin adreslerini verdiğinizi hayal edin. Her biri ordan alıp işini görebilir. Ne bellek doldurulmuş olur ne de programlar veriden mahrum kalmış olur.
Değişkenlerin Adreslerini Nasıl Elde Ederiz?
Burada devreye, bellek adresi tutan değişkenler girer. Değerleri nasıl değişkenlerde depolayabiliyorsak bellek adreslerini de aynı şekilde depolayabiliriz. Bunu gerçekleştiren yapılara pointer denir. Aslında kendisi de bir değişkendir.
Önceki kısımda görsel olarak anlattığımız örneği kodlama ortama dökelim. İlk olarak değişken tanımımızı yapalım:
Dediğimiz gibi dogum_tarihi
1999 tamsayı değerini depoluyor. Şimdi bu değerin bellekteki adresini elde edelim:
Pointer'ları belli etmek için genellikle ismin başına veya sonuna "p", "ptr" gibi eklemeler yapılır.
Şimdi bu sözdizimini inceleyelim.
dogum_tarihi_ptr
değişkenimizin türü, göreceğimiz üzere int*. Bu da bize tamsayı değişkenin adresini tutan pointer olduğunu gösterir. Başına koyduğumuz '&'' işareti ise değişkenimizin adresini elde etmemize yarayan address-of operatörüdür. Yani &dogum_tarihi
, dogum_tarihi
değişkeninin bellekteki adresidir.
Şu iki çıktıya bakarsak '&' işaretinin ne işe yaradığı daha net anlaşılabilir:
=== Output:1999 0x7ffd83c692bc 0x7ffd83c692bc
Hemen toparlayalım. dogum_tarihi
ifadesi, bildiğimiz int değişkenidir. Ve 1999 sayısını depolar. &dogum_tarihi
ifadesi, değişkenimizin adresidir. dogum_tarihi_ptr
ifadesi ise elde ettiğimiz adresi depoladığımız değişkendir. Tipi ise int*'dir. Yani değişken adresi depolayan değişken diyebiliriz :)
Bu kısmı tamamlarken bir noktaya daha değinmekte fayda görüyorum. Pointer tanımlarken int *dogum_tarihi_ptr
, int* dogum_tarihi_ptr
sözdimlerinden herhangi birini kullanabiliriz. Yıldızın sabit konumu yoktur, sadece tip ile değişken ismi arasında olmalıdır. Ancak C/C++ programcıları geleneksel olarak int *dogum_tarihi_ptr
yazımını kullanılır. Bunun sebebi aşağıdaki kullanımda yarattığı kafa karışıklığıdır:
Buradaki iki değişkeninde pointer olacağını düşünüyorsunuz değil mi? Ancak ptr_a
pointer iken ptr_b
int değerdir. Şu şekilde yazılmış gibi kabul edilir:
Bu yüzden aşağıdaki şekilde kullanırsak, herhangi bir sorun oluşmaz:
Değişken ismine bitişik olduğu için değişkene ait olduğundan emin oluruz.
Adresler ile Değişken Değerlerine Erişmek
Değişkenin adresini elde edebileceğimiz gibi adres yardımıyla değişken değerine de erişebiliriz. Şu görseli birlikte inceleyelim:
Örneğin elimizdeki doğum tarihi bilgisini kullanıcının yanlış girmiş. Bunu değiştirmek istiyor. Elimizde hem değişken hem de adresini tutan pointer var:
Değişkenlerin değerini nasıl değiştirebileceğimizi zaten biliyorduk:
"Peki neden hala o yöntemle değiştirmeye devam etmiyoruz?" diyorsanız çoğunlukla öyle yapacak olsakta bazı zamanlar gelecek ki pointer kullanmak zorunda kalacağız. İleride göreceğiz.
Artık pointer vasıtasıyla da değiştirebiliriz:
=== Output:0x7ffc0206d83c // başındaki "0x" hexadecimal olduğunu belirtir
Burada kafalar çorba olabiliyor. Pointer tanımlanırken kullanılan * işareti, onun pointer olduğunu belirtmek için konur ve başka amacı yoktur. Ancak tanımlandıktan sonra kullanacağımız her * işareti, pointer'ın işaret ettiği adresteki değeri temsil eder. Buna dereference operator denir.
Adresler ile Dizi Elemanlarına Erişmek
Bu kısma geçmeden önce küçük bir ipucu vermek istiyorum. String değişkenler tanımlarken
char[] isim = "Ahmet"
şeklindeki bir kullanıma denk gelmişsinizdir. C'de aslında string diye bir tip yoktur. Son elemanının '\0' karakteri olan karakter dizileri ile ifade edilirler. Yani bu anlatacaklarım onlar için de geçerli olacak.
Dizi isimleri farklı kullanımlarda farklı anlam ifade edebilirler. Pointer kavramı, dizileri anlamak için önemlidir. Bu sebeple bir dizi oluşturup pointer ile ilişkisini görelim:
Dizinin ismi, ilk elemanın adresini tutar. Dolayısıyla pointer gibi davranır. Aşağıdaki çıktılara bakacak olursak bunu net bir şekilde görebiliriz:
=== Output:0x7ffcd4fb610c 0x7ffcd4fb610c 0x7ffcd4fb610c
Üçünün de aynı olduğunu ve diziye erişeceğimiz adresi verdiklerini görebiliriz. Özetleyecek olursak; diziler, temel veri tipleri gibi değer tutmak yerine dizinin ilk elemanının adresini tutar. Bunu sayi_dizisi
ifadesini yazdırarak görebiliriz. &sayi_dizisi
dediğimizde dizinin adresini elde ederiz. Ve dizinin ilk elemanı ile aynıdır. Son olarak &sayi_dizisi[0]
ifadesiyle dizimizin ilk elemanının adresine bakıyoruz ki ki bu da aynıdır.
printf()
ile kullandığımızda dizinin adresini ifade eder ancak bazı durumlarda da dizinin değerini temsil eder. Örneğin sizeof()
fonksiyonuna gönderirsek, pointer boyutunu değil dizi değerinin boyutunu verecektir:
=== Output:5
Yani adresi yerine gerçek diziyi kullandığımız anlamına gelir.
Pointer Aritmetiği
Kullandığımız diziyi tekrardan hatırlayalım ve adresini tutan ekstra bir pointer oluşturalım:
sayi_dizisi
zaten adres tutuyordu değil mi? İkinci bir yerde daha tutmak için sayi_dizisi_ptr
pointer'ını oluşturduk.
Dizimizin bellekteki durumunu görselleştirelim:
Dizilerin elemanlarına erişmek için indeksleri kullanıyorduk (bir sonraki kısımda karşılaştıracağız). Ancak arkada neler dönüyor merak ediyoruz. İşte bunu pointer yardımıyla anlayabiliriz.
Her bir elemana dizi adresine uyguladığımız aritmetik işlemler ile erişiyoruz. Şu örneğe bakalım:
=== Output:ilk adres: 0x7ffcd4fb610c ilk eleman: 10 ikinci adres: 0x7ffcd4fb6110 ikinci eleman: 20 son adres: 0x7ffcd4fb6114 son eleman: 30
İsmin ilk öğeye işaretçi olduğunu söylemiştik. Dolayısıyla *array_ptr
dediğimizde ilk öğeyi verdiğini biliyoruz. Bu kısım tamam. Peki array_ptr += 1;
ne anlama geliyor?
Pointer'a 1 eklediğimizde bir sonraki kutunun adresini tutmasını istiyoruz.
Biraz kafalar karışmış olabilir. Bu tarz şeylere görsellik katmadan anlamak zordur bilirim. İkinci elemana ulaşmak için adrese 1 eklediğimizde olacak şey böyledir:
Göreceğimiz gibi 1 eklediğimizde esasen 1*sizeof(int)
demek isteriz. int olmasının sebebi dizinin elemanlarının tam sayı olmasıdır. Tam sayı boyutunun 4 bayt olduğundan bahsetmiştik. Yani 4 kutu. 1 eklemek istediğimizde 4 kutu sonraya git demiş oluruz. Yani sayi_dizisi_ptr
pointer değişkeni artık 0x7ffcd4fb6110 adresini depolayacak. Bunu yapma sebebimiz ise diğer elemanın adresine erişmek. Tekrardan 1 eklediğimizde de aynı işlemler uygulanır ve adres 0x7ffcd4fb6114 olur.
Direk olarak 3. elemana erişmek isteseydik 2*sizeof(int)
, yani 2 eleman sonraya git diyebilirdik:
Bu sefer sonraki 8. kutuya giderek 30 değerinin adresine sahip olurdu.
Açıklaması biraz uzun sürmüş olabilir. Ama kafanızda netleşeceğini düşünüyorum. Anlayıp anlamadığınızı bir sorgulayın. Eğer anladıysanız devam, anlamadıysanız editörü açın ve bir takım C/C++ deneyleri yapın.
Söylememe gerek var mı bilmiyorum ancak pointer'a tekrar atama yaptığımız için adres değişir diyoruz. Yani
sayi_dizisi_ptr += 1;
ifadesi adresi kalıcı olarak değiştirir. Sonraki adresi yazdırmak amacıylasayi_dizisi_ptr + 1;
ifadesini kullansaydık adresin değeri değişmezdi. Oradaki = kritik öneme sahiptir.
Son işlemin ardından artık sayi_dizisi_ptr
sonuncu elemanın adresini tutacaktır:
=== Output:adres: 0x7ffcd4fb6114 eleman: 30
Peki tekrardan 2. elemana ulaşmak istersek? Çocuk oyuncağı. Toplama yerine çıkarma yaparak önceki kutulara gideceğiz:
=== Output:adres: 0x7ffcd4fb6110 eleman: 20
Artık pointer ile toplama ve çıkarma yapmanın ne anlama geldiğini benimsediğimizi düşünüyorum. Bunu anlamak önemli.
Bu kısımdan bir çıkarım yapacak olursak, dizi tanımlamak için ilk elemanın adresini ve eleman sayısını bilmemizin yeterli olacağını anlayabiliriz. Bunu anladığınızda dizi tanımlamak çok daha kolay hale gelir.
İndekslerin Pointer Aritmetiğindeki Karşılıkları
Az önceki kısımda anlattığımız dizi elemanlarını elde etmek işlemini pointerlara girmeden önce indeksler ile kolayca yapabiliyorduk:
=== Output:ilk eleman: 10 ikinci eleman: 20 son eleman: 30
Ancak bazen pointerlara ihtiyacımız olur. O yüzden indekslere karşılık gelen pointer ifadelerine göz gezdireceğiz. Yukarıdaki kodu indeksler yerine pointer aritmetiği kullanılarak yeniden yazalım
=== Output:ilk eleman: 10 ikinci eleman: 20 son eleman: 30
Ancak bu kısımda dikkat etmemiz gereken bir şey var. Bir dizinin 2. elemanının adresini depolayan bir pointer tanımlayalım. Ve indeks 1'deki değere bakalım:
=== Output:30
30 sonucunu alırız. Çünkü dizimiz artık şu hale gelecektir:
Dolayısıyla ilk eleman artık 30 olacaktır (başlangıçtan 4 kutu sonrası olarak hayal ediyorduk hatırlayın).
Fonksiyonlar ile Pointer İlişkisi
Bir fonksiyon içerisinde değişkenlerin değerini değiştirmek istediğimizi düşünelim. Bunun için bir örnek oluşturalım:
=== Output:Fonksiyon öncesinde sayı: 12 Fonksiyon içinde sayı: 144 Fonksiyondan return edilen sayı: 144 Fonksiyon sonrasında sayı: 12
Burada fonksiyona gönderilen sayi
değişkeninin n
isminde kopyası oluşturulur ve bellekte yer işgal eder. Yapılan işlemlerin orjinal değişken üzerinde etkisi olmaz. Yeni değer yalnızca return
ile çağrıldığı yere döndürülebilir.
Peki ya argümanı alırken pointer kullanırsak ne olurdu?
=== Output:Fonksiyon öncesinde sayı: 12 Fonksiyon içinde sayı: 144 Fonksiyon sonrasında sayı: 144
Farkı net bir şekilde görebilirsiniz. Fonksiyona değişkenin adresini gönderdiğimizde yapılan işlemler orjinal değişkeni değiştirir. Adresteki içeriğin karesi alır ve adresin içeriği değiştirilir. Fonksiyon sonrasında o adrese bakacak olursak değiştiğini görürüz.
Bu özelliğin yararı çok fazladır. Örneğin çok sayıda veri üzerinde çalışıyorsunuz, mesela 100.000 adet resmimiz var. Ve bu resimleri nesne tespitini yapacak fonksiyona göndermek istiyoruz. Ancak pointer olmasaydı ve hepsinin kopyasını göndermiş olsaydık, bellekte 200.000 resim depolanmış olurdu. Muazzam bir yer işgali olacaktır ve performans düşecektir. Bu sebeple, sadece adresin içeriğinde değişiklik yapmak faydalıdır. Her yerden orjinal içeriğe erişebiliriz.
Peki dizi göndermek istesek ne olurdu? Dizi isimlerinin pointer olduğundan bahsetmiştik. Diziyi fonksiyona kabul ederken parametre olarak tanımlayabileceğimiz ve temelde aynı olan 2 seçeneğimiz var.
Birincisi dizi olarak almak:
İkincisi ise pointer olarak almak:
Bunların ikisi de adresler ile işlem yapacağından aynıdır ve orjinal diziyi değiştirirler. Örneğimize bakalım:
=== Output:6 13 8 12 10
Ancak pointer olarak atamak içeriğini değiştirmek istemediğimiz durumlarda (mesela örnekteki gibi en büyük elemanı bulmak istersek) risk oluşturur. Ya yanlışlıkla değiştirirsek? İstenmeyen veri kayıpları olabilir. Bundan kaçınmak için içeriğin değiştirilemeyeceğini belirten const
anahtar kelimesini kullanabiliriz:
=== Output:En büyük sayi: 8
Matruşka Pointerlar
Bunu sona bırakmak istedim. Pointer'ın en çok kafa karıştıran kısmıdır. Basitçe anlatmaya çalışacağım.
pointer değişkenin adres depoladığını artık biliyoruz. Elbette pointer değişkenlerde bellekte depolanır. Bu sebeple pointer adresi tutan pointer oluşturmak mümkündür. Bir değişken ve pointer oluşturalım:
Şimdi bu pointer'ın adresini depolayan bir pointer daha oluşturalım:
Bu da ne, hani tek yıldız ile oluşturuluyordu? Sakin. Hala tek yıldız ile oluşturuluyor. Yukarıda int *yas_ptr_ptr
ismini int* yas_ptr_ptr
olarak ifade ederek analize başlayalım. int tipinde yas
değişkenin adresini bir pointer'a atarsak bunun tipinin int* olduğunu gördük. Şimdi ise int* tipini tutan pointer oluşturmuş oluruz. Yeni değişkenimizin tipi ise int* olur. Biraz daha netleşti değil mi?
Yeni pointer ile ilgili birkaç çıktıya bakalım:
=== Output:0x7fffd4e80798 0x7fffd4e80794 50
İlk satırı biliyoruz yas_ptr_ptr
'in adresini yazdırdı. İkinci satırda ise bu pointer'ın değerini, yani yas_ptr
değişkeninin adresini yazdırdık. Son satırda ise yas_ptr_ptr
pointer değişkeninin adresini depoladığı yas_ptr
pointer değişkeninin adresinin içerisinde bulunan değeri yazdırmış olduk. Görsel olarak da eklemiş olayım:
Yeni yazılarda görüşmek üzere, esen kalın.