İşaretçiler

İşaretçiler bellek adresi tutan değişkenlerdir. İki değişkenden birincisi ikincisinin bellekteki adresini depoluyorsa birinci değişken ikinci değişkeni işaret ediyor denir ve işaretçi ismi buradan gelir.

Bir değişken işaretçi olacaksa şu şekilde ilan edilir:

veri_tipi *değişken_ismi;

Burada dikkat edilmesi gereken nokta veri tipinin doğrudan değişkenin tipi olmayacağıdır. Belirtilen değişken tipi işaretçi hangi tipte bir değişkenin adresini depolayacaksa o tip olmalıdır.

Örneğin bir işaretçi "int *p;" şeklinde tanımlanmış olsun. Bu; p işaretçisi int tipinde bir değişkenin adresini depolayacaktır manasına gelir, p'nin kendisinin int tipinde depolanacağını belirtmez.

Daha önceden işaretçi işlemlerine kısaca göz atmıştık. İşaretçiler için tanımlanmış iki işlem & ve * işlemleridir. & işlemi bir değişkenin adresini almaya yararken * işlemi bir adresteki değişkeni okumaya yarar.

#include <stdio.h>

int main(void){
    int a=5;
    int *c;
    c=&a;
    int b;
    b=*c;
    printf("%d",b);
    return 0;
}

Yukarıdaki örnek daha önceki konulardan alınmıştır. Örnekte görüldüğü gibi a'ya 5 değeri atanmış, c a'nın adresini almış ve c adresindeki değer b'ye atanmıştır. b'nin değeri yazdırıldığında sonucun 5 olduğu görülecektir.

Yukarıdaki örnekte görüldüğü gibi bir işaretçiye adres & simgesi kullanılarak atanmaktadır. Bunun dışında bir işaretçi başka bir işaretçiye de atanabilir. Bu durumda eşitttir işaretinin her iki tarafına da işaretçi işlemleri olmaksızın işaretçilerin isimleri yazılır.


İşaretçi Dönüşümü

Atama işlemine katılan iki işaretçi aynı tipler için tanımlanmışsa normal atama işlemi gerçekleştirilir. Fakat iki işaretçinin farklı tipler için tanımlandığı durumlarda tip dönüşümü gerçekleşecektir.

void * şeklinde tanımlanan işaretçiler her türde değişkenin adresini tutabilirler. Ayrıca bu şekilde tanımlanan işaretçiler her türe dönüştürülebilir  her tür de bu tipe dönüşebilir. Bu tipteki işaretçilerle ilgili atamalarda ek bir işlem gerekmemektedir. Bunun dışındaki atamalarda zorlama gereklidir. Örneğin p1 int tipi, p2 de float tipi için tanımlanmış birer işaretçi olsun. Bu durumda p1 işaretçisini p2 işaretçisine atamak için aşağıdaki ifade yazılmalıdır:

p2=(float *)p1;

Fakat bu tarz işlemler programın çalışmasını engellemese de yanlış sonuçlar elde edilmesine sebep olabilir.


İşaretçi Aritmetiği

İşaretçilerle sadece iki aritmetik işlem yapılabilir. Bunlar toplama ve çıkarmadır. İşaretçilerin aritmetiği normal aritmetikten biraz daha farklıdır. Örneğin int tipi için bir işaretçi tanımlamış olalım ve işletim sistemimize göre bir int'in büyüklüğü 4 byte olsun:

int *p;

Bu işaretçi için p++ veya p-- işlemi yapıldığında değeri 1 değil 4 artar veya azalır. Böylece p bir sonraki int'in adresini almış olur. Kısacası bir işaretçi için artım veya azaltım işlemi yapıldığında kendi baz tipindeki bir sonraki veya bir önceki elemanı işaret edecek değeri alır. Baz tip char olduğunda bu işlemler normal aritmetikteki gibi olacaktır çünkü char tipi her halükarda 1 byte büyüklüğündedir.

Toplama ve çıkarma işlemleri artım veya azaltım şeklinde olmak zorunda da değildir. Aşağıdaki ifadeler de doğru ifadelerdir:

p1=p1+4;
p2=p1+2;

Buradaki toplama işleminin de yine normal aritmetik işlem gibi olmadığı bilinmelidir. Yukarıdaki işlemlerde yeni değer mevcut değere eklenen değer kadar baz tipin boyutunun eklenmesi ile elde edilir. Örneğin p1 8 byte büyüklüğündeki double tipinde bir değişken için tanımlanmışsa "p1=p1+4" ifadesi p1'e 32 ekler. Böylece yeni değer 4 double sonraki değişkeni işaret eder.

İşaretçiler için geçerli olan işlemler sadece toplama ve çıkarma olmakla birlikte bu işlem de sadece tam sayılar ile yapılabilir. Küsuratlı sayı ekleme, çıkarma yapılamaz.


İşaretçiler ve Diziler

İşaretçilerle dizilerin elemanlarına erişme hakkında daha önceden diziler konusunda birtakım bilgiler vermiştik. Bir dizinin bir elemanına işaretçiler vasıtasıyla iki farklı şekilde erişebileceğimizi söylemiştik. Bunlardan birisi işaretçi aritmetiğini kullanmak diğeri de işaretçileri indislemekti. Bu konu hakkındaki örnekler aşağıdadır:

#include <stdio.h>

int main(void){
    int a[2][2][2];
    int *p;
    p=a;
    p[3]=15;
    printf("%d",a[0][1][1]);
    return 0;
}

#include <stdio.h>

int main(void){
    int a[2][2][2];
    int *p;
    p=a;
    *(p+3)=15;
    printf("%d",a[0][1][1]);
    return 0;
}

Yukarıdaki örneklerde işaretçi bir int değişkenini tutacak şekilde tanımlanmıştır. Dizinin ilk elemanı da bir int olduğu için kodda bir sıkıntı çıkmamıştır. Fakat her ne kadar bu örnekte bahsi geçen durum bir sorun teşkil etmese de birden fazla boyutlu dizileri işaret edecek olan işaretçilerin buna uygun olarak tanımlanmaları uygun olandır. Bu tanımlama şu şekilde yapılır:

veri_tipi (*işaretçi ismi) [2. boyutun büyüklüğü][3.boyutun büyüklüğü]...;

Üstelik bu durumda işaretçiler dizilerle aynı şekilde indislenebilir:

#include <stdio.h>

int main(void){
    int a[2][2][2];
    int (*p)[2][2];
    p=a;
    p[0][1][1]=15;
    printf("%d",a[0][1][1]);
    return 0;
}

Böyle tanımlanan işaretçilerle aritmetik yapılmak istendiğinde ise zorlama gereklidir:

#include <stdio.h>

int main(void){
    int a[2][2][2];
    int (*p)[2][2];
    p=a;
    *((int*)p+3)=15;
    printf("%d",a[0][1][1]);
    return 0;
}

Meseleyi daha detaylı incelemek isteyenlerin "Diziler" konusundaki "İşaretçileri İndislemek" kısmını okumaları yararlı olacaktır.


İşaretçi Dizileri

İşaretçilerle de dizi oluşturulabilir. İşaretçilerle dizi oluşturmak için de şu kalıp kullanılır:

Baz_veri_tipi *değişkenin_adı [1. boyut][2. boyut]...[n. boyut]

Bu dizilerin içerisindeki elemanlara da normal dizilerdekiyle aynı şekilde erişilir:

#include <stdio.h>

int main(void){
    int a=100;
    int *p[5][2];
    p[2][2]=&a;
    printf("%d",*p[2][2]);
    return 0;
}

Yukarıdaki örnekte bir işaretçi dizisi tanımlanmış ve bu dizinin elemanlarından birine a değişkeninin adresi atanmıştır. Son olarak da bu adreste bulunan değer yazdırılmıştır.

İşaretçi dizileri fonksiyonlarda kullanılmak istenirse yine dizilerin fonksiyonlarda kullanılması hususundaki kurallar geçerlidir. Bir fonksiyona bir işaretçi dizisini argüman vermek için herhangi bir parantez olmadan sadece dizinin adı kullanılır. Fonksiyonu tanımlamak için de baz tip, değişkenin adı ve en soldaki hariç dizinin tüm boyutları belirtilmelidir. Aşağıdaki örnekte "int *x[2][2];" şeklinde tanımlanmış bir işaretçi dizisinin sırasıyla bir fonksiyona argüman olarak verilmesi ve bir fonksiyon tanımında kullanılması gösterilmiştir:

f(x);

void f(int* x[ ][2]){
...
}


Çok Kademeli Adres Atama

İşaretçilerin de birer değişken oldukları unutulmamalıdır. Bu nedenle onların da adresleri bir başka işaretçiye atanabilir. Bir işaretçinin adresini alacak olan işaretçi şu şekilde tanımlanır:

baz_tip **değişken_ismi;

Örneğin int tipindeki bir değişkenin adresini alacak olan işaretçinin adresini alacak olan q işaretçisi şu şekilde tanımlanır:

int **q;

Her bir yeni işaretçi kademesinde görüldüğü gibi yeni bir * işlemi kullanılmalıdır. İki kademeli bir işaretçi kullanımı örnekte görülebilir:

#include <stdio.h>

int main(void){
    int a=5;
    int *p;
    int **q;
    p=&a;
    q=&p;
    int b=**q;
    printf("%d",b);
    return 0;
}

Örnekte önce bir a değişkeni tanımlanmış ve bu değişkene 5 değeri atanmıştır. Daha sonra p ve q işaretçileri tanımlanmıştır. p işaretçisi a değişkeninin adresini ve q işaretçisi de p işaretçisinin adresini almıştır. Daha sonra b değişkeni tanımlanmış ve b'nin değeri olarak q adresinde tutulan adresteki değer atanmıştır. Bu da a'nın değerine eşit olacaktır. Sonucumuz 5'tir.


İşaretçilere İlk Değer Verme

İşaretçiler ilk değer verilmeden tanımlanırsa bilinmeyen bir değer veya "0" değerini alırlar. Statik olmayan yerel değişkenler bilinmeyen bir değer alırken statik yerel değişkenler ve global değişkenler kendiliğinden "0" değeri alırlar.

Bir işaretçiye bir değişkenin adresi & işlemi ile atanabilir:

...
int a=2;
int *p=&a;
...

İşaretçilere ilk değer olarak "0" da verilebilir. Bunun iki yolu vardır. İki yolu da aşağıdaki örneklerde görebilirsiniz:

int *p=0;
int *p=NULL;

Bir işaretçinin değerinin "0" olması o işaretçinin tuttuğu adresin "0" olduğu manasına gelir. Dikkat edilmesi gereken nokta değeri "0" olan bir işaretçinin adresine değer atanmaması ve bu adresten değer okunmaması gerektiğidir. Çünkü "0" adresi gerçekte yoktur.

#include <stdio.h>

int main(void){
    int *p=NULL;
    printf("%d",*p);
    return 0;
}

#include <stdio.h>

int main(void){
    int *p=NULL;
    *p=a;
    int a=*p;
    printf("%d",a);
    return 0;
}

Yukarıdaki iki örnek de düzgün şekilde derlenecek olmasına rağmen derlenen program düzgün çalışmayacaktır. Çünkü 0 adresine değer atanmaya veya oradan değer okunmaya çalışılmıştır.

Bunun dışında işaretçiler direkt bir dizgi sabitine eşitlenerek de başlatılabilirler. Bu özel bir durumdur ve bu durumda işaretçi dizgi sabitine değil dizgi sabitinin tutulduğu belleğe eşitlenir. Fakat işaretçi şeklinde ilan edilen dizgiler de normal dizgiler gibi kulanılmalıdır.

#include <stdio.h>

int main(void){
    char *p="Test";
    printf("%s",p);
    return 0;
}

Bu örnek gayet düzgün çalışacaktır. Dahası p[0], p[1] gibi ifadelerle dizginin karakterlerine de ulaşmak mümkündür. Yani kısacası yukarıdaki örnekteki dizgi de normal dizgi gibi kullanılabilir.


Fonksiyon İşaretçileri

Fonksiyonlar da bellekte belli bir adreste depolanırlar ve başladıkları adres bir işaretçiyle alınabilir. Bu durumda fonksiyon işaretçi ile çağrılabileceği gibi fonksiyonun bir başka fonksiyona argüman olarak verilebilmesi de sağlanır.

Bir fonksiyonun ismini parantezler olmadan yazmak o fonksiyonun adresini ifade eder. Bu şekilde fonsiyonun adresi alınabilir.

Bir fonksiyonun adresinin atanacağı işaretçi şu şekilde tanımlanmalıdır:

fonksiyonun_dönüş_tipi (*işaretçi_ismi)(veri_tipi1, veri_tipi2,...);

Fonksiyon da aşağıdaki iki şekilde çağrılabilir:

işaretçi_ismi(argüman1,argüman2,...);
(*işaretçi_ismi)(argüman1,argüman2,...);

Aşağıdaki örnekte verilen iki sayının toplamlarının karesini veren bir fonksiyon yazılmış ve bu fonksiyon işaretçiyle çağrılmıştır:

#include <stdio.h>

int main(void){
    int (*p)(int, int);
    int f1(int x, int y);
    p=f1;
    int a=5;
    int b=3;
    int c=(*p)(a,b);
    printf("%d",c);
    return 0;
}

int f1(int x, int y){
    return ((x+y)*(x+y));
}

Yukarıdaki örnekte fonksiyon "(*p)(a,b)" yerine "p(a,b)" şeklinde de çağrılabilirdi. Yukarıdaki yazım genellikle kodu okuyan birinin fonksiyonun işaretçi ile çağrıldığını anlaması için kullanılır.

İşaretçi fonksiyon içinde kullanılacaksa formal parametre olarak yine aynen yukarıdaki kalıp kullanılmalıdır. Fonksiyonlarda zaten daha detaylı örnekleri göreceğiz.


Dinamik Atamalar

Bazen programda zaman içinde boyutu büyüyen ya da küçülen veri yapıları olabilir. Bu durumlarda program dışındaki uygulamaların kullandığı bellekten ek bellek alınabilir ve bu bellek istendiği zaman geri bırakılabilir.

Bu iş için C'nin "stdlib.h" kütüphanesinde malloc( ) ve free( ) fonksiyonları tanımlanmıştır. Bu iki fonksiyonun yapıları şöyledir.

void *malloc(size_t istenen_byte_sayısı);
void *free(void *işaretçi_ismi);

İki fonksiyon da görüldüğü gibi void baz tipindeki birer işaretçidir. Daha önceden işaretçi dönüşümlerinde anlattığımız gibi void baz tipindeki bir işaretçi her türlü tipteki işaretçiye atanabilir. Fakat bu fonksiyon çalıştırıldığında yeteri kadar bellek bulunamazsa sonuç olarak elde edilecek değer "0" adresi olacaktır. Bu da programın yanlış çalışmasına sebep olabilir.

...
char *p,
p = malloc(1000);
...

Yukarıdaki işlem bellekten 1000 byte alacaktır ve p bellekteki 1000 byte'ı işaret edecektir. Bellekte yer bulunamadığı zaman uyarı verecek kod parçalarının da programa yerleştirilmesi iyi olacaktır.

Yukarıdaki kod parçası yazıldıktan sonra istenen bellekle işimizin bittiğini düşünelim. Bu durumda bu belleği artık serbest bırakabiliriz. Bu da şu satır ile yapılacaktır:

...
free(p);
...

Şimdi malloc( ) fonksiyonu ile alınan belleğin bir diziye atamak isteyelim. Daha önceden hep önce bir dizi tanımlanmış ve bunun adresi işaretçiye atanmıştı. Fakat bu sefer önce işaretçiyi tanımlayıp adresleri aldık ve bu adrese bir dizi atayacağız. Burada yapmamız gereken daha önceden diziler konusunda gördüğümüz işaretçi indislemeyi kullanmaktır.

Burada işaretçi şu şekilde tanımlanmalıdır:

int (*işaretçi_ismi)[a2][a3]...[aN];

Yukarıdaki örnekte görüldüğü gibi işaretçinin adresini tutacağı dizinin ilk boyutu hariç tüm boyutları girilmelidir ki adreslerin hangi değerden sonra bir diğer boyuta geçeceği bilinsin .İşaretçi isminin etrafındaki parantezler de gereklidir. Çünkü aksi durum bir dizinin adresini tutacak olan işaretçi oluşturmak yerine işaretçi dizisi oluşturulmuş olur.

#include <stdio.h>

int main(void){
    long int (*p)[5];
    p=malloc(120);
    char i,j;
    for(i=0;i<6;i++){
        for(j=0;j<6;j++){
            p[i][j]=(i+1)*(j+1);
            printf("%d\t",p[i][j]);
        }
        printf("\n");
    }
    free(p);
    return 0;
}

Yukarıdaki örnekte her bir elemanında satırıyla sütununun çarpımını gösteren 6x5 boyutlarında bir matris oluşturulmuştur.


restrict

C99 restrict isimli  ve sadece işaretçiler için kullanılan bir tip niteleyici daha tanımlamıştır. Bu tip niteleyiciyi kullanan bir tanımlama şöyle yapılır:

veri_tipi *restrict işaretçi_ismi;

restrict tipi derleyiciye başında kullanıldığı işaretçinin işaret ettiği değişkene sadece bu işaretçiyle erişileceğini söyler. Fakat bunun gerçekten böyle olup olmadığını denetlemek kodu yazanın sorumluluğundadır. Bu tip niteleyicinin kullanılması derleyicinin daha optimize çalışmasını sağlayabilir:

#include <stdio.h>

int f(int *restrict x, int *restrict y, int *restrict z);

int main(void){
    int a=5;
    int b=3;
    int *p=&a;
    int *q=&b;
    int *r=&a;
    int result=f(p, q, r);
    printf("%d",result);
    return 0;
}

int f(int *restrict x, int *restrict y, int *restrict z){
    *x+=*z;
    *y+=*z;
    return (*x)*(*y)*(*z);
}

Örneğin yukarıdaki kod çalıştırılırken fonksiyon z işaretçisinde depolanan değeri x ve y işaretçilerinde depolanan değişkenlere atama yapılırken yalnızca bir kez kontrol etmeyi seçebilir. Çünkü derleyiciye z işaretçisindeki değere sadece z işaretçisiyle erişileceği söylenmiştir ve bu nedenle derleyici fonksiyon içerisindeki kodda z'deki değerin değişmeyeceğini varsayabilir. Halbuki x işaretçisindeki değer değiştiğinde aynı zamanda z'de depolanan değer de değişmektedir.




Diziler <<<<< Temel C >>>>> Fonksiyonlar