C Programlama

Ek Bilgiler

C'de İşaretçiler (Pointers)

► Detaylı anlatım

İşaretçiler, bilgisayar belleğindeki belli bir adresi gösteren ve gösterdiği bellek adresine erişimi sağlayan değişkenlerdir.

İşaretçilerin diğer değişkenlerden tek farkı başka bir değişkenin bellek adresini içeriyor olmasıdır. Aşağıdaki satır işaretçi bildiriminin genel yapısını göstermektedir:

veri-türü *değişken-adı;

Yukarıdaki satırda yer alan veri-türü ifadesi işaretçinin adresini gösterdiği değerin veri türünü, *değişken-adı ifadesi ise işaretçinin adını göstermektedir. * işareti işaretçi değişken bildiriminde kullanılmaktadır. Aşağıdaki işlem satırı ip1 adlı int bir işaretçi değişkeni oluşturmaktadır:

int *ip1;

İşaretçi tanımlamalarında veri türünü belirlerken, daha sonra adresini işaretçiye atanacak değişkenin veri türünü dikkate almamız gerekir. Başka bir ifade ile, işaretçi ile işaretçiye bellek adresi atanan değişkenin veri türü aynı olmalıdır.

İşaretçilere Değer Atanması

Bir işaretçiyi kullanmadan önce, bir bellek adresinin mutlaka işaretçiye atanması gerekir. Çünkü, işaretçi bellek adresleri ile işlem yapar. C dili işaretçilere bellek adresi atama işlemini, & işlemcisini kullanarak yapar.

C'de işaretçilerle kullanılan iki farklı işlemci vardır. Bunlardan biri * , diğeri ise & işlemcisidir. Bu iki işlemcinin görevlerinin anlaşılması işaretçi kavramını anlamak açısından çok önemlidir:

* : Hangi işaretçi değişken adından önce kullanılırsa, o işaretçi değişkene adresi
    atanan değişkenin değerini verir.

& : Hangi değişken adından önce kullanılırsa, o değişkenin adresini verir.
#include <stdio.h>

main()
{
  int *ip1, id1;

  id1 = 278;
  ip1 = &id1;

  printf("id1 değişken değeri: %d\n", id1);  /* id1 değerinin doğrudan yazılması */
  /* id1 değişken değerinin dolaylı olarak yazılması */
  printf("id1 değişken değeri: %d\n", *ip1); 
  /* id1 değişken bellek adresinin işaretçi yoluyla yazılması */
  printf("id1 değişkeni bellek adresi: %p\n", ip1);
  /* id1 değişken bellek adresinin & işlemcisi ile yazılması */
  printf("id1 değişkeni bellek adresi: %p", &id1);
}

Yukarıdaki örnekte, program ekrana aşağıdaki satırları yazar:

id1 değişken değeri: 278
id1 değişken değeri: 278
id1 değişkeni bellek adresi: 0022FF18
id1 değişkeni bellek adresi: 0022FF18

Program önce id1 adlı bir int değişken ve ip1 adlı bir işaretçi değişkeni oluşturur. id1 değişkenine 278 değerini atar. id1 değişkeninin adresini de ip1 işaretçisine atar. İlk iki printf() işlem satırında önce direk sonra dolaylı yöntemle id1 değişken değerini ekrana yazar. Son iki printf() işlem satırı ile de, %p format tanımlayıcısını kullanarak, önce direk işaretçi adı sonra & işlemcisi yoluyla id1 değişkeninin bellek adresini ekrana yazar.

Konumuza devam etmeden önce, 0022FF18 ifadesine kısaca inceleyelim. Bu ifade bilgisayar belleğinin belirli bir adresini Hexadecimal(16'lık sayı sistemi) sayı sisteminde göstermektedir. Yukarıdaki örnek programı çalıştırdığınızda farklı bir bellek adres değeri alabilirsiniz. Burada gösterilen bütün adres değerleri, sizin bilgisayarınız için farklı olabilir.

İşaretçiler Yoluyla Değişkenlere Değer Atanması

İşaretçileri kullanarak, işaretçinin adresini içerdiği değişkene bir değer atayabilirsiniz. Bu özelliği bir örnek üzerinde incelemeye çalışalım:

#include <stdio.h>

main()
{
  int *ip1, *ip2, id1, id2;     /* 1 */

  ip1 = &id1;                   /* 2 */
  ip2 = &id2;                   /* 3 */
  id1 = 42;                     /* 4 */
  *ip2 = 67;  /* Dolaylı değer atama yöntemi */ /* 5 */
  
  printf("id1 değişkeninin değeri: %d\n", *ip1);
  printf("id2 değişkeninin değeri: %d\n", *ip2);
  printf("id1 değişkeninin bellek adresi: %p\n", ip1);
  printf("id2 değişkeninin bellek adresi: %p\n", ip2);
}

Yukarıdaki örnekte, program aşağıdaki satırları ekrana yazar:

id1 değişkeninin değeri: 42
id2 değişkeninin değeri: 67
id1 değişkeninin bellek adresi: 0022FF14
id2 değişkeninin bellek adresi: 0022FF10

Program önce ip1 ve ip2 adlı 2 adet işaretçi değişkeni ile id1 ve id2 adlı 2 adet int değişken tanımlar. id1 değişkeninin bellek adresini ip1 işaretçisine id2 değişkeninin bellek adresini ip2 işaretçisine atar. id1 değişkenine direk olarak 42 sayısını atar. id2 değişkenine ise, ip2 işaretçisini kullanarak, 67 sayısını dolaylı yöntemle atar. Sonra id1 ve id2 değişkenlerinin değerlerini ve bellek adreslerini ekrana yazar. Programın her işlem satırından sonraki bellek durumu ise aşağıda gösterilmektedir:

Bir işaretçi kullanmadan önce, bir değişkenin adresi mutlaka işaretçiye atanmalıdır. Eğer bir bellek adresi atamadan işaretçi kullanılırsa, istenen netice elde edilemez. Çünkü, bir işaretçi tanımladığınızda sadece bir değişkenin bellek adresini atayabilecek bir değişken oluşturmuş olursunuz. Ancak, işaretçi değişkenine atanmış herhangi bir bellek adres değeri yoktur. Aynı şekilde eğer bir işaretçi değişken bildiriminden sonra, bir değişkenin bellek adresini işaretçiye atamadan, bir int değeri işaretçi değişkenine atarsanız, yine istediğiniz neticeyi elde edemezsiniz.

Aşağıda yer alan işlem satırlarından 1 ve 2 sayıları ile gösterilen satırlar, C dilinin kurallarına göre geçersizdir:

int *ip1;

*ip1 = 357;           /* 1 */
printf("%d", *ip1);   /* 2 */

Diğer İşaretçi İşlemcileri ve İşlemci Aritmetiği

Daha önce incelediğimiz * ve & işlemcilerine ek olarak, işaretçilerle kullanabileceğiniz 4 işlemci daha vardır:

+   -   ++  --

Bir işaretçi değere sadece tanımlandığı veri türünden bir değer ekler veya çıkarabilirsiniz.

Aşağıdaki son iki satırda yer alan ifadeler birbirine eşittir:

int ip1, id1;

ip1 = &id1;

id1 = 25;

ip1 + id1;                   /* id1 değerini değil sadece int boyutunu ekler */  
ip1 + (id1 * sizeof (int));

İlk anda, yukarıdaki ilk işlem satırında, id1 değişken değerinin direkt olarak ip1 işaretçisine ekleneceği akla gelebilir. Ancak, programın ilk işlem satırı kullanıldığında, derleyici ikinci işlem satırını kullanmışsınız gibi işlem yapar. Başka bir deyişle, işaretçi aritmetiğinde, işlemler işaretçi veri türüne bağlı olarak yapılır. Şimdi, bu özelliği bir örnek üzerinde incelemeye çalışalım:

#include <stdio.h>

main()
{
  int *ip1;
  int idizi[5] = { 5, 17, 21, 34, 46 };
  int id1;

  printf("İşaretçi bellek adresi: %p\n\n", &ip1);

  ip1 = &idizi[0];

  for (id1=0; id1<5; id1++, ip1++) { /* 1 */
       printf("İşaretçinin içerdiği adres: %p %p\n", ip1, &idizi[id1]);
       printf("İşaretçinin gösterdiği değişken değeri: %d %d\n\n", *ip1, idizi[id1]);
  } 
}

Yukarıdaki örnekte, program ekrana aşağıdaki satırları yazar:

İşaretçi bellek adresi: 0022FF08

İşaretçinin içerdiği adres: 0022FEF4 0022FEF4
İşaretçinin gösterdiği değişken değeri: 5 5

İşaretçinin içerdiği adres: 0022FEF8 0022FEF8
İşaretçinin gösterdiği değişken değeri: 17 17

İşaretçinin içerdiği adres: 0022FEFC 0022FEFC
İşaretçinin gösterdiği değişken değeri: 21 21

İşaretçinin içerdiği adres: 0022FF00 0022FF00
İşaretçinin gösterdiği değişken değeri: 34 34

İşaretçinin içerdiği adres: 0022FF04 0022FF04
İşaretçinin gösterdiği değişken değeri: 46 46

Program, önce ip1 adlı int bir işaretçi ile idizi adlı 5 elemanlı int bir dizi oluşturur. İşaretçi bellek adresini ekrana yazdıktan sonra, dizinin ilk elemanının adresini işaretçiye atar. Bir for döngüsü içinde, her defasında işaretçi değerini int boyutu kadar artırmak suretiyle, işaretçinin bir sonraki dizi elemanının adresini göstermesini sağlayarak işaretçinin içerdiği adresi ve gösterdiği dizi değişken değerini hem işaretçi adı hem de dizi adını kullanarak ikişer defa ekrana yazar.

For döngüsüne ilk girdiğinde bellek durumu aşağıdaki ilk şekildeki gibidir. Döngü içinde ip1++ ifadesi ilk çalıştığında ise, ip1 int bir işaretçi olduğundan, int boyutu kadar (32 bit bilgisayarlarda 4 byte) yukarıdaki bellek adresinin değerini alır. Yani, daha önce idizi[0] değişken adresini içeren ip1 işaretçisi artık idizi[1] değişkeninin adresini içermektedir. Aslında, 1 sayısı ile gösterilen işlem satırındaki ip1++ ifadesinin gerçek değeri aşağıdaki gibidir.

ip1 = ip1 + (1*sizeof(int));

Sonuç olarak, for döngüsünün her tekrarında ip1++ işlemi ile ip1 işaretçisinin içerdiği adres int boyutu kadar artarak bir sonraki dizi değerinin adresini gösterir.

* Bir işaretçiye eklenen veya çıkarılan değer mutlaka int bir değer olmalıdır.

* İşaretçilerle toplama veya çıkarma dışında bir işlem yapılamaz.

İşaretçiler Yoluyla Değişken Değerlerinin Değiştirilmesi

++ ve -- operatörleri ile normal toplama ve çıkarma işlemlerini kullanarak işaretçinin adresini gösterdiği değişken değerini dolaylı yöntemle artırabiliriz:

#include <stdio.h>

main()
{
  int *ip1, id1;

  id1 = 274;

  ip1 = &id1;

  printf("id1 değişken değeri: %d\n", id1);

  /* ip1 işaretçisinin adresini gösterdiği değişken değerini artırır! */
  (*ip1)++; /* 1 */

  printf("id1 değişken değeri: %d\n", id1);
}

Yukarıdaki örnekte, program ekrana aşağıdaki satırları yazar:

id1 değişken değeri: 274
id1 değişken değeri: 275

Program, ip1 adlı int bir işaretçi ve id1 adlı int bir değişken oluşturur. id1 değişkenine 274 sayısını atar. id1 değişkeninin adresini ip1 işaretçisine atar. Değişken değerini ekrana yazdıktan sonra ip1 işaretçisini kullanarak 1 sayısı ile gösterilen işlem satırında id1 değişken değerini artırarak değişken değerini tekrar ekrana yazar.

#include <stdio.h>

main()
{
  int *ip1, id1;

  id1 = 541;

  ip1 = &id1;

  printf("id1 değişken değeri: %d\n", id1);

  *ip1 = *ip1 - 20;

  printf("id1 değişken değeri: %d\n", id1);
}

Yukarıdaki örnekte, program ekrana aşağıdaki satırları yazar:

id1 değişken değeri: 541
id1 değişken değeri: 521

Program, bir önceki programın yaptığı işlemin aynısını gerçekleştirir. Tek fark, değişken değerini tek sayı artırmak yerine 20 sayı azaltmasıdır.

Şimdi öğrendiklerimizi örneklerle pekiştirmeye çalışalım:

#include <stdio.h>

main ()
{
  int *ip1;
  int idizi[5] = { 36, 64, 127, 152, 183 };

  ip1 = &idizi[0];

  printf("%d %d %p\n", idizi[0], *ip1, ip1);

  *ip1++;     /* 1 */ /* ip1++ ile aynı işlemi gerçekleştirir! */

  printf("%d %d %p\n", idizi[0], *ip1, ip1);

  (*ip1)++;   /* 2 */ /* idizi[1]++ ile aynı işlemi gerçekleştirir! */

  printf("%d %d %p\n", idizi[0], *ip1, ip1);
}

Yukarıdaki örnekte, program ekrana aşağıdaki satırları yazar:

36 36 0022FEF8
36 64 0022FEFC
36 65 0022FEFC

Program, ip1 adlı bir işaretçi ile 5 elemanlı idizi adlı int bir dizi oluşturur. Dizinin ilk eleman adresini ip1 işaretçisine atar. Dizinin ilk eleman değeri ile ip1 işaretçisinin gösterdiği adresi ve adresini gösterdiği değişken değerini ekrana yazar. 1 sayısı ile gösterilen işlem satırında ip1 işaretçinin gösterdiği adresi int boyutu kadar artırdıktan sonra değerleri tekrar ekrana yazar. Bu durumda 2 ve 3 ncü sütun değerleri değişir. 2 sayısı ile gösterilen işlem satırında ise ip1 işaretçisinin adresini gösterdiği değişkenin değerini artırdıktan sonra değerleri tekrar ekrana yazar. Bu durumda ise, sadece ikinci sütun değeri değişir. Başka bir deyişle, program p1 işaretçisini değerini int boyutu kadar arttırdığından, dizinin ikinci eleman adresini otomatik olarak ip1 işaretçisine atar.

*ip1++;     /* İşaretçinin gösterdiği bellek adresini artırır. */

(*ip1)++;   /* İşaretçinin gösterdiği bellek adresindeki değişken değerini artırır. */