C’de Pointer Sözdizimi (Syntax) ve Aritmetiği

C’de pointer konusunda yüzlerce, binlerce kaynak var. Bu yüzden size sil baştan pointer’i anlatmayacağım (söz!). Sadece, pointer konusunu öğrenmeye başlayan herkesin sıkıntı çektiği sözdizimi(pointer syntaxı) ve aritmetiği ile ilgili ayrıntılara örneklerle değineceğim. Okuduğum bir çok kaynakta örnek malesef çok az. Bu yazımda özellikle C’de pointer sözdiziminde kullanılan yıldız * operatörünün neden kafa karıştırdığını ve *ptr++, *++ptr, ++*ptr, vs.. gibi değişkenlerin ne anlama geldiğini anlatacağım. Örnek C kodları da mevcut olacaktır, yani indirip, derleyip adım adım görebileceksiniz.

Şimdi pointer nedir?

Pointer bir veri tipidir ve bu veri tipi başka bir değişkeni işaret eder. Peki nasıl işaret eder? Diğer değişkenin sakladığı(tuttuğu) adres sayesinde doğrudan oraya işaret eder. Peki pointer nasıl oluşturulur (declaration)? Pointer tıpkı diğer veri tipleri sözdizimi gibi oluşturulur. Tek fark değişkenin önüne bir yıldız konmasıdır. Örneğin aşağıda bir pointer ve örneklerimizi kullanacağımız bir array verelim

int *foo_ptr;
int my_array[] = {13, 4, 9, -5, 45};

Burada “pointer to int” tipinde bir pointer değişkeni oluşturduk ve bu değşikenin ismi de foo_ptr. Şu an için hiç bir yere işaret etmiyor. Geçersiz bir durum söz konusu ve bunu gidermek için bir yere işaret etmesini sağlayacağız. Yukarıda my_array değişkeni oluşturmuştuk. Şimdi ona işaret etmesini sağlayalım:

foo_ptr = &my_array[0];

Array oluşturulurken bellekte bir bütün olarak yer aldığından ilk sayısı (yani my_array[0]) bize arrayin başlangıcını verecektir. & operatoru ise, bize bir veri tipinin bulunduğu alanın adresini verir. Yukarıda böylelikle foo_ptr değişkenini my_array değişkenin ilk elemanına işaret ediyor olacak. my_array elemanı da bir bütün olduğundan aslında hepsini işaret etmiş olacak. Son olarak belki de önemli bir ayrıntı, & operatoru ile geri dönen verinin tipi pointer cinsinden. Bu yüzden doğrudan foo_ptr değişkenine atayabildik.

İşaret edilen veriye ulaşmak

Pointer’in işaret ettiği veriye ulaşmak için tek yapmamız pointer değişkenin önüne yıldız koymak. Mesela my_array’deki ilk sayıyı çıktısını alalım:

printf("Bu my_array[0] değeri: %d", *foo_ptr); // 13 sayısını gösterecek

Peki o zaman bu değeri değiştirebilir miyiz? Evet aynen değiştirebiliriz. Şimdi sıkı durun. Aşağıda iki tane örnek var, bakalım onlar ne yapıyor:

*foo_ptr = 2;
printf("Bu my_array[0] değeri: %d", *foo_ptr); // 2 sayısını gösterecek

C’deki asıl sorun, * operatorunun hem pointer oluşturma için kullanılması hem de veriyi ulaşmak için kullanılmasıdır. Aslında her ikisini anlamı farklı. Örneğin aşağıdaki örnek ne yapıyor sizce?

foo_ptr = 2;

Eğer adres atama örneğini iyi anladıysanız bu ifadenin sorunlu bir ifade olduğunu göreceksiniz. Yukarıdaki örnek 2 sayısını (burada int tipinde) pointere (pointer de burada pointer of int tipinde) atamaya çalışacak. Ama böyle bir şey mümkün değil. Çünkü her ikisinin tipi farklı. Bu yüzden derlemeye çalışırsanız şöyle bir uyarı çıkacak(“-Wall -g” parametreleri ile derliyorum):

warning: assignment makes pointer from integer without a cast [enabled by
default] 

Dereference özelliğini düzgün kullanabilmek

Yukarıda yıldız * operatoru ile veriye doğrudan ulaşmanın bir diğer adı pointer dereference demektir. Yıldız operatörü malesef bir çok kafa karışıklığına da yol açıyor. Çünkü hem pointer veri tipini oluşturmaya yarıyor, hem de pointer’in işaret ettiği yere ulaşmamızı sağlıyor. Örneğin bir fonksiyonumuz olsun ve bu fonksiyon iki tane argüman alsın. Bu argümanlar pointer to int şeklinde olsun:

void foo_function(int *bar_ptr, int *qux_ptr);

Şimdi dereference özelliği ile birlikte yukarıdaki örnek de kafa karıştırıcı olabiliyor. Çünkü man sayfaları ya da herhangi bir kiptalıkta tanımlar bu şekilde ifade edilmiştir. Burada iki tane pointer tipinden veri alıyorum anlamına geliyor. Bir örnekten gidelim. İki tane pointer oluşturalim ve bunları bir yere işaret etmesini sağlayalım

int *a_ptr;
int *b_ptr;
int x = 2;
int y = 3;

Sonra bu pointerleri işaret etmesine sağlayalım:

a_ptr = &x;
b_ptr = &y;

Buraya kadar herşey normal. Şimdi bu pointer’leri fonksiyonumuza düzgün bir şekilde verelim:

foo_function(a_ptr, b_ptr); // OK: Valid

Yukarıdaki örnek doğru sözdiziminde yazılmış bir örnektir. Gördüğünüz gibi gidip değişkenlerin önüne yıldız öperatorunu koymadık. Neden? Çünkü fonksiyonumuz pointer of int tipinde bir veri bekliyordu. Mesela yıldızlı şekilde yazmak yanlış olacaktı:

foo_function(*a_ptr, *b_ptr); // NOT OK: Invalid

Burada fonksiyonumuza int tipinde iki tane veri sunuyoruz. Halbuki fonksiyon bizden pointer of int cinsinden bir veri tipi bekliyordu. Pointer’lerin önüne yıldız koyarak dereference etmiş oluyoruz, yani işaret ettiği veriyi kullanıyor oluyoruz artık. Bir fonksiyon doğrudan int tipinde veri de alabilir, örneğin:

bar_function(int a, int b);

O zaman gayet sorunsuz bir şekilde oluşturduğumuz pointer verilerimizi kullanabilirdik:

bar_function(*a_ptr, *b_ptr);

Bir başka atama şekli: ptr = my_array

Tekrar örnek değişkenlerimizi oluşturalım:

int *ptr;
int my_array[] = {13, 4, 9, -5, 45};

Sonra ptr’ye my_array’in adresini atalım:

ptr = my_array;  // ptr = &my_array[0]; ile aynı

Yukarıdaki örnek bir başka sözdizimi farklılığı. Yani istersek bu şekilde de yazabiliriz. Farklı bir şekilde yazılmış hali diyelim. Herhangi bir adres pointerini, örneğin &foo[i] , şu şekilde de yazabiliriz kısaca:

(foo + i)

Bizim my_array[0] örneği de haliyle (my_array +0) şekline dönüşüyor. Sıfır ve parantezleri attık mı, sonuç olarak üsteki sonucu elde ederiz:

ptr = my_array;

3[my_array] ve my_array[3] eşittir, nasıl mı ?

Bir önceki örnekte &foo[i] sözdiziminin (foo +i) sözdizimine eşit olduğunu söylemiştik. O zaman kısa bir hack yapalım. Mesela my_array[3] şu şekilde yazalım:

(my_array + 3)

yerlerini değiştrelim:

(3 + my_array)

Hiç bir şey değişmedi, hala aynı. Son olarak bunu da my_array sözdiziminde yazalım:

3[my_array]

Evet bu mümkün! Inanmakta güçlük çekiyorsanız deneyip sonucu kendi gözlerinizle görebilirsiniz. Hiç bir zaman böyle bir şey yazmayın, yazmayalım. Sadece neler olabileceğini görünüz diye yazdım.


Aşağıda yukarıdaki örneği denemeniz için kısa bir örnek kod mevcut:

Pointer Aritmetiği, örnekler, örnekler..

Şimdi buraya kadar gelmişken aşağıdaki örnek değişkenlere bakalım:

*ptr
*(ptr++)
(*ptr)++
++*ptr
*++ptr
*ptr--
*--ptr
--*ptr

Gördüğünüz gibi karman çoban bir şey. Her birini anlamı farklı, anlaması zor bir şeymiş gibi gözüküyor. Ama şu ana kadar anlattıklarımda bir sorun yoksa, bunlar da bir sorun olmayacak. Şimdi my_array örneğinden gidip her bir değişken ne yaptığını öğrenmiş olacaksınız. Array ve ptr tanımlarını hatırlatalım:

int *ptr;
int my_array[] = {13, 4, 9, -5, 45};

Sonra ptr’ye my_array’in adresini atalım:

ptr = my_array;  // ptr = &my_array[0]; ile aynı

Şimdi devam etmeden önce şunu unutmayın. * operatoru + operatorundan daha önceliklidir. Yukarıdaki listede parantez kullandıklarım olduğu gibi, kullanmadıklarım da var. Parantez kullanmıyorsanız ben o koda kötü derim. Özellikle C gibi bir dilde, pointerleri yukarıdaki gibi kullanacaksanız kesinlikle parantez kullanın. Parantez kullanılmayan değişkenlerin de neye dönüştüğünü tek tek öğreniyor olacaksınız birazdan. Başlayalım:

*ptr // print the value 13

Bu bize my_array değişkenindeki ilk değeri verecektir. Hatırlayınız ki * operatoru bir pointer’in önüne konulduğunda, o pointer doğrudan o veriyi bize gösterir.

*(ptr++) // print the value (13) and increment pointer

Bu bize o anki değeri verecektir, bizdeki örnekte 13 sayısını verecektir. Fakat ptr pointeri de bir artacaktır. Yani artık ptr bir sonraki indeksi işaret ediyor.

(*ptr)++ // print the value (4) and increment the value

Burada bir üsteki örneğin tersini yapıyoruz. İlk önce değeri alıyoruz. Sonra da bu değeri artırıyoruz. Çıktı olarak bize 4 verecektir, ama değişkenin değeri artık 5 olmuştur.

++*ptr // same as ++(*ptr), increment value (5) and print the value (6)

Burada * öncelikli her zamanki gibi. Bu yüzden ilk başta değişkeni bize verecektir. Yani 5 sayısını. Ama öndeki iki ++ artış operatoru olduğundan 5 sayısı bir artacak yani 6 olacaktır.

*++ptr // same as *(++ptr), increment the pointer and print the value (9)

Bu sefer ilk önce pointeri bir artırıyoruz. Sonra da işaret ettiği yerin değişkenini alıyoruz. Pointeri bir artırdığımız bir sonraki değeri almış olduk. Bu da (9) sayısı.

*ptr-- // same a (*ptr)--, print the value (9) and decrement the pointer

Şu anki işaret edilen yerinin değişkenin değerini alıyoruz. Yani bu da bizim örneğimizde (9) sayısı. Fakat sonra pointer düşürüyoruz. Artık bir önceki yere işaret ediyor.

*--ptr // same a *(--ptr), decrement the pointer and print the value (13)

Burada pointerin işaret ettiği yeri bir daha düşürüyoruz. Yani en başa döndük. Sonra da değeri alıyoruz, o da bizim örneğimizde (13)

--*ptr // same a --(*ptr), decrement value (13) and print the value (12)

Son olarak değerimizi alıyoruz ve bu değeri bir azaltıyoruz. Yani 13 sayısını 12 olarak geri dönüyor bize.


Tabi en iyi deneyim göze görülür bir deneyim. Bu yüzden yukarıda tek tek anlattığım örnekleri aşağıdaki C kodunu çalıştırıp da görebilirsiniz. Kolaylık olsun diye ptr adreslerinin de çıktısını veriyorum. Böylelikle pointer adreslerinin nasıl artığını gözünüzle göreceksiniz.

Evet hepsi bu kadar. Bir sonraki yazıma vakit ayırabilirsem, iki boyutlu arraylar (pointers to pointers to data type) ve struct ilişkisini yazmayı düşünüyorum. O zaman işler daha da karmaşık hale geliyor haliyle. Ama umarım yukarıdaki örnekler az çok işinize yaramıştır diye düşünüyorum. Sormak istediğiniz, aklınıza takılanlar varsa, yanlış bulduğunuz kısımlar varsa yorum kısmına yazın lütfen.

5 Haziran 2012

WP Twitter Auto Publish Powered By : XYZScripts.com