Tekrardan merhabalar. Bu gün C dilinde pointer olarak adlandırılan konu üzerine bir yazı yazmaya çalışacağım. Yine diğer yazılarda ufaktan da olsa bahsettiğim işaretçiler(pointers) hakkında fark yaratabileceğini düşündüğüm bilgileri paylaşmaya çalışacağım.
Epeyce bir süredir, ne yazık ki yeni bir yazı yayımlayamadım. Ülkemizde yaşanan çok çok üzücü olaylar, hayatın dönem dönem hepimizi vuran yoğunluğu gibi sebeplerle sessiz bir dönem geçirdim. Ancak ne olursa olsun, başladığımız işleri bitirmekte fayda var. Bu vesileyle bu yazımın temasını “vicdan” olarak belirliyorum.
C dilinin belki de en zor konusudur işaretçiler (pointers). Nedendir hikmeti tam bilinmez, bir türlü açık seçik açıklanamamıştır. Benim yorumum şu; işaretçiler çoğu zaman basit kurgulanmış bir yapı olmadığından, anlaması da çok basit değil. Bu yorumuma katılan, katılmayan olabilir ancak sebeplerimi kendimce sıralayıp ardından olayı açık şekilde anlatmaya çalışacağım. Zira olayın aslı oldukça basit.
İşaretçiler belleğe kestirme yollardan erişmeyi sağlar diyebiliriz. İşaretçiler, türlü potansiyel sorunları beraberinde getirdiğinden bazı dillerde kullanımı yasaklanmıştır. Örneğin C#’da pointer kullanımını açmak için kodu güvenilmez (unsafe) yazdığınızı kabul etmeniz beklenmektedir. Yüksek seviyeli dillerde belleğe doğrudan erişim hakikaten güvenilmez kodlar doğurabilir ancak C gibi orta seviyeli bir dilde işaretçi kullanarak bellek erişimi yapmak, doğru tasarım kalıpları ve prensipler izlendiğinde oldukça verimli olabilir. Bizim de niyetimiz, işaretçileri doğru kullanarak C dilinin güçlü olan bu yanına hükmedebilmek bu sayede de güzel kodlar yazabilmek. Sırf işaretçiler risk unsurları yaratabilir diye, onlara kem gözlerle bakmak, onları kötü kullanmak vicdansızlık olur diye düşünüyorum.
Bellek Hakkında
Bildiğiniz üzere bellek dediğimiz esasen fiziksel bir bilgi deposu oluyor. Bu zımbırtının veriyi saklama biçimine göre iki temel çeşidi var. Birincisi Volatile Memory denilen “Geçici Hafıza”. Birisi de Non-Volatile Memory(NVM) denilen “Kalıcı Hafıza”. Efendim ne göre geçici ya da kalıcı? Elektrik gittiğinde gösterdiği davranışa göre. Elektriği kessek (ya da cihaza hard reset atsak) dahi, geri verdiğimizde veriler saklanıyorsa NVM, saklanmıyorsa VM. Misal RAM bir VM’dir yani geçici bellektir. Hard disk veya flash memory ise NVM yani kalıcı bellektir.
Pointer (İşaretçi) Nedir?
Biz işaretçiler ile hem kalıcı bellekteki hem de geçici bellekteki verilere erişebiliyoruz. Çok önemli olduğu için tekrar edeceğim. Bir işaretçi iki operatör ile ifade edilebilir.
1 2 3 |
* Opetaörü: Bellekteki bir adres içindeki veriye erişim. Kullanım: DegiskeninDegeri=*Adres. & Operatörü: Bir değişkenin adresine erişim. Kullanım: Adres = &Degisken; |
Arkadaşlar C dilinde her şey bir değişken gibi değerlendirilebilir demiştik. Pointer da benzer muameleye tabi olduğundan, onun da bir tipi vardır. Örneğin
1 |
uint8_t *dataPtr; |
yukarıdaki dataPtr’nin tipi uint8_t* olduğundan, bu işaretçi gösterdiği bellek alanındaki bilgiyi uint8_t olarak yorumlayacaktır. Benzer bir yorum farklı veri tipleri için de yapılabilir. Misal float* tipindeki bir değişken, göstereceği adres içindeki veriyi float olarak yorumlayacaktır. Bu bilgiyi daha detaylıca açıklayacağım.
İşaretçileri anlatmanın en iyi yolu örnekler üzerinden gitmektir. Öyleyse aşağıdaki örnek üzerinden başlayalım.
1 2 3 4 5 6 7 8 |
#include <stdint.h> int32_t main() { int32_t num; int32_t *ptr; return 0; } |
Bir değişken tanımlandığında, ona uygun bir bellek gözü (ya da bellek gözleri) tahsis edilir. Hangi bellek olacağı işletim sisteminin ve derleme ortamının takdirindedir. Yukarıdaki değişkenler farzı misal 0x2015 adresinden başlayarak yerleşsin.
1 2 3 4 5 6 7 8 |
|-----| 0x2014| | num; 32 bit genişliğinde yer kaplayan bir değişkendir. |-----| 0x2018| | ptr; 32 bit genişliğinde yer kaplayan bir işaretçidir. |-----| 0x201C| | belleğin geri kalanı... |-----| : |
Şimdi kodun bir yerlerinde C dilinde şunları dersek:
1 2 |
num = 42; ptr = # |
Bunların insan dilinde okunuşu şöyle olur: “num değişkeninin değeri 42’dir. ptr işaretçisi ise num değişkeninin adresine eşittir.”
Tanımlandığında num değişkeni 0x2014 numaralı bellek gözünde yer almaktaydı hatırlayın. Buna göre ptr=0x2014 olması gerekir çünkü num değişkeninin adresi budur. Öteyandan ptr değişkeninin adresi 0x2018 olduğundan, ptr’nin değeri bu adreste saklanır. Buna göre 0x2014 numaralı bellek gözünde 42 yazacak, 0x2018 numaralı bellek gözünde ise 0x2014 yazacaktır. Haydi azıcık zahmet edip çizelim.
1 2 3 4 5 6 7 8 |
|------| 0x2014| 42 | num; 32 bit genişliğinde yer kaplayan bir değişkendir. |------| 0x2018|0x2014| ptr; 32 bit genişliğinde yer kaplayan bir işaretçidir. |------| 0x201C| | belleğin geri kalanı... |------| : |
Şimdi bu ibretlik bilgiyi sınayabileceğiniz tam bir kodu paylaşalım.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <stdint.h> #include <stdio.h> int32_t main() { int32_t num=42; int32_t* ptr; ptr = # printf("num'un adresi: %x n", &num); printf("ptr'nin degeri: %x n", ptr); printf("num'un degeri: %d n", num); printf("ptr'gosterdigi deger: %d n", *ptr); printf("ptr'nin adresi: %x n", &ptr); getchar(); return 0; } |
Efendim bu kodun benim bilgisayarımdaki çıktısı şöyle:
ptr’nin degeri: 22fe4c
num’un degeri: 42
ptr’gosterdigi deger: 42
ptr’nin adresi: 22fe40
——————————–
Process exited with return value 0
Press any key to continue . . .
- Gördüğünüz gibi, num’un adresi hakikaten ptr’nin degerine eşit.
- Ptr’nin gösterdigi deger hakikaten num’un degerine eşit.
- Ve hakikaten ptr, bir degisken olarak bellekte saklaniyor ve adresi num’un adresinden farklı.
Mevzuyu açık seçik gösterdik diye düşünüyorum. Şimdi benzer bir olayı diziler üzerinde anlatalım ki başka püf noktaları görelim.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#include <stdint.h> #include <stdio.h> int32_t main() { int8_t name[] = "Ozge"; int8_t *p8; int32_t *p32; p8 = name; p32 = (int32_t *)name; printf("name'in adresi: %x rn", name); printf("p8'in degeri: %x rn", p8); printf("p32'nin degeri: %x rn", p32); printf("p8'in gosterdigi deger: %d [%c] rn", *p8,*p8); printf("p32'nin gosterdigi deger: %d [%x] rn", *p32, *p32); printf("p8'in adresi: %x rn", &p8); printf("p32'nin adresi: %x rn", &p32); ++p8; ++p32; printf("rnisaretci degerleri arttiktan sonra!!!rn"); printf("p8'in degeri: %x rn", p8); printf("p32'nin degeri: %x rn", p32); printf("p8'in gosterdigi deger: %d [%c] rn", *p8,*p8); printf("p32'nin gosterdigi deger: %d [%x] rn", *p32, *p32); printf("p8'in adresi: %x rn", &p8); printf("p32'nin adresi: %x rn", &p32); getchar(); } |
Öncelikle kodu okuyarak anlamaya çalışınız.
Şimdi bu çıktıları teker teker yorumlayalım. Ancak önce dizinin ve işaretçilerin belleğe nasıl yerleştiğini farz-ı misal bir açıkalayalım.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|---| 0x2014 |'O'| "name", tanimlanan dizinin adresidir ve bu ornekte 0x2014'dur. |---| 0x2015 |'z'| int8_t(char): 1 byte |---| 0x2016 |'g'| int8_t(char): 1 byte |---| 0x2017 |'e'| int8_t(char): 1 byte |---| 0x2018 | |