C DİLİNDE İŞARETÇİ (POINTER) KAVRAMI – Volume 1

Mesut Topuzlu
4 min readDec 8, 2020

--

Merhaba sevgili okurlar ve yazılıma gönül verenler. Bu yazımda C diline özellikle yeni başlayanların anlamakta güçlük çektiği fakat C dilinin olmazsa olmazı, çarkı döndüren ana dişlilerden biri olan işaretçileri (pointer) naçizane açıklamaya çalışacağım. Pointer, Türkçe’ye işaretçi olarak çevrilmektedir fakat ağız ve el alışkanlığı malum çoğu zaman pointer olarak telaffuz ederim. Ayrıca, işaretçi kavramı C diline özgü bir kavram olmayıp C++, C#, Go, Rust vb. dillerde de karşımıza değişik yetki ve kullanım biçimleriyle çıkmaktadır. İşin özeti şu ki; her ne programlayacaksak programlayalım; ister bir işletim sistemi isterse embedded bir uygulama… Eninde sonunda mutlaka işaretçiler ile yolumuz kesişecektir. Diğer türlü (en azından C dili için) yazılım bilgimiz yüzeysel kalacaktır.

Peki nedir bu işaretçi?

Adında da anlaşılacağı üzere bir nesnenin (evet nesne diyorum çünkü sadece built-in değişkenlere değil yapılara (struct), fonksiyonlara, bunlardan oluşan dizilere de işaretçi atanabilir) bellekteki adresini gösteren, işaret eden, tutan (artık siz nasıl isimlendirirseniz) özel bir değişkendir. Evet, işaretçi aslında kendisi de bir değişkendir ve tuttuğu adres yeterli boyuttaki başka bir değişkene aktarılabilir. Hatta bir işaretçiye bile işaretçi atayabilirsiniz.

İşaretçileri bildirmek (declaration);

İşaretçi bildirimi için aslında tek bir ifadeye ihtiyacımız var;

işaret edeceği tip + asteriks karakteri + işaretçi ismi

Vay be aslında çok da zor değilmiş. Normal tip bildiriminden tek farkı arada asteriks (*) karakterinin olmasıymış… Tabii nüansları da yok değil…

int * p;

int*p;

int *p;

int* p;

Yukarıdaki bildirimlerin gerçekte hepsi de aynıdır. Ben genellikle son gösterdiğim int* p notasyonunu kullanırım çünkü asteriks işareti işaretçi ismine birleşik yazıldığında, beyin bu noktada nasıl çalışıyor gerçekten bilmiyorum, onu komple değişken ismi gibi algılıyordu (en azından benim beynim…). Ama dediğim gibi nasıl bir notasyonla yazacağınız sizin yoğurt yemenizle alakalı bir durum.

DÜZELTME (14.01.2021): Yukarıda yazdıklarım yanlış değil lakin küçük bir ekleme yapmak istedim;

int* p1, p2; //bu bildirimde p1 ve p2 işaretçi gibi gözükebilir. Fakat gerçekte ise derleyici p1'i işaretçi, p2'yi integer bir bildirim olarak algılar.

int *p1, *p2; //bu notasyon hem göze hen gönüle hitap etti sanki…

İşaretçileri kullanmak;

Şimdiye kadar işaretçinin ne olduğunu ve nasıl bildirileceğini açıklamaya çalıştım. Peki biz bu işaretçileri nasıl kullanacağız? Şimdi bir örnek üzerinden bunu anlamaya çalışalım. Aşağıdaki gibi, günün herhangi bir anında, herhangi bir geliştirme ortamında yazdığımız minik kodu inceleyelim;

#include <stdio.h>
int main(void)
{
int* p; //int bir değişkenin bellek adresini tutacak işaretçi bildirimi
int i = 10; //int tipinde bir değişken
p = &i; //i değişkeninin adresini al ve işaretçiye ata
}

İşte hepsi bu kadar. İnanın daha fazlası değil. He burada kodu biraz daha kısaltabilir miyiz? Yanıt evet.

#include <stdio.h>
int main(void)
{
int i = 10; //int tipinde bir değişken
int* p = &i; //i değişkenin adresi işaretçiye bildirimde veriliyor (initialization)
}

Peki biz yukarıda ne yaptık? Int türünden değişken bildirdik ve init ettik. Adresini ampersand (&) karakterini kullanarak işaretçiye aktardık. Bu arada işaretçinin ve değişkenin tipinin aynı olduğuna dikkat edin. Aksi halde (dikkatlice kullanmayı tenzih ederim…) C’de bolca karşılaştığımız pek meşhur “undefined behaviour” yani tanımsız davranış durumu ile karşılaşabiliriz. Peki… değişkenin adresini de aldık. Ya şimdi? C dilinin bize sunduğu kadim ve güzide fonksiyon printf ile mevzuyu biraz kurcalayalım. Main fonksiyonu içerisinde yukarıdaki tanımlamaları yaptığımızı varsayıyorum;

printf("%d",i); //ekrana tahmin edeceğiniz üzere i’nin değeri olan 10 yazar

Yukarıda printf içerisindeki %d bir format belirtecidir ve d decimal (bildiğimiz onluk düzen) olarak yazdır anlamına gelir ve signed int türleri için kullanılır.

printf("%d",*p); //o da ne...sonuç yine 10.

Demek ki neymiş biz bir değişkene, adresinin atandığı işaretçi ile de erişebiliyormuşuz (dereference işlemi). Ee.. süper bir şey değil mi bu? Hayır henüz değil :) Peki i değişkeni hangi adreste saklanıyor gibi bir soru sorsak kendimize? Cevap ne olurdu? Tabii bunu öğrenmenin yolu yine kadim dostumuz printf.

printf("i’nin adresi : %p\n",p); //%p işaretçinin tuttuğu adresi göstermek için kullanılan özel bir format belirtecidir ve adresi heximal (onaltılık) düzende gösterir. \n ile de bir satır aşağı geçiyoruz.

Şimdi aşağıdaki kodu derleyelim ve çalıştıralım. Kafamızın şimdilik işaretli sayılar ile karışmaması için unsigned yani işaretsiz bir tam sayı kullanacağım. Malum, işaretli sayılar ikinin tümleyeni olarak hafızada tutulurlar;

#include <stdio.h>
int main(void)
{
unsigned int i = 10;
unsigned int* p = &i;

printf("i’nin adresi : %p\n",p);
}

Çalıştırma işleminden sonra aşağıdaki gibi bir konsol ekranı karşımıza gelir;

Program çıktısından da anlaşılacağı üzere benim bilgisayarımda i değişkeni 0x000000000061FE14 mantıksal adresinden başlanarak yerleştirilmiştir. Mantıksal adres diyorum çünkü çalıştırdığımız program işletim sisteminin ona tahsis ettiği RAM bloğunda faaliyetini sürdürmektedir. RAM’deki gerçek (fiziksel) adresi muhtemelen farklı bir şeydir.

Şimdi yukarıdaki hafıza bloğuna baktığınızda ne görüyorsunuz? Tam da bizim p işaretçisinin gösterdiği adresten başlayarak 4 hücre boyutunda bir dizilim. Çünkü unsigned int tipi değişkenin boyutu (derleyiciye de bağlı olarak değişir) 32-bit yani 4-byte’dır. Bellekteki her hücre 1-byte veriyi tutabildiği için 4-byte’lık bir integer verinin (10 sayısı 0x0000000A heximal notasyonu ile gösterilir) nasıl konumlandırıldığını görüyoruz. Peki bir soru daha; neden ilk adreste i sayısının en düşük bayt’ı olan 0A konumlandı? Bu tamamen verilerin bellekte little-endian’mı yoksa big-endian formatındamı saklanacağı ile alakalıdır. Bazı işlemci mimarileri little-endian bazıları ise big-endian formatını destekler ki şu an benim bilgisayarım Intel işlemciye sahip ve little-endian formatını destekliyor. Eğer işlemcim big-endian formatını destekleseydi yukarıdaki byte dizilimi tam tersi olacaktı yani yüksek değerli bayt’lar düşük adreste tutulacaktı.

Yazımın sonuna geldik. Bir sonraki yazımda işaretçi kavramına devam edeceğim. İşaretçilerin varoluş amaçlarını, ilginç ve pekte ilginç olmayan kullanımlarını, boyutlarını, aritmetik operatörlerle etkileşimlerini vb. irdelemeye çalışacağım. Faydalı olabildiysem ne mutlu bana…

Sağlıcakla kalın…

--

--