İşaretçiler (pointers) başlıklı yazımda konuyla ilgili güzel bir girizgah ve temel atma merasimi tertip ettik. Şimdi, işaretçilerle ilgili daha ileri konulara değinmenin tam sırası. İşin aslı işaretçilerin kullanım alanları çok çok geniş. Tilkiler normalde yalnız dolaşır ama işaretçiler değince aklımda bin bir tilki sürü halinde dolaşıyor. Hepsini burada yazmak çok mümkün mü bilmiyorum ancak, elimden geldiğince yazmaya çalışacağım.
Efendim özellikle belleği etkin kullanma noktasında, işaretçiler ziyadesiyle önemli bir faktör oluyor. Bu sebepledir ki gömülü programlama yapacak yiğitlerin işaretçilerin suyunu sıkması gerekmektedir. Mevzunun özütü yine aynı olsa da, her bir kullanım alanından alınacak sayısız ibretler olduğundan, farklıca örnekler üzerinden işaretçileri inceleyemeye çalışacağım. Yine de eksik kalan bir kullanım alanı olduğunu düşünüyorsanız, yorum olarak ekleyebilirsiniz.
Bu yazıyı okumayı bitirdiğinizde varacağımız noktada iki yol var.
- Bu şeyler sizler için yeni şeyler ise, bunları sindirdiğinizde o çok korkulan işaretçiler konusunda harbici bir uzman olmuş olacaksınız.
- Bu şeylerin tamamını zaten biliyorsanız, zaten bir uzmansınız. Lütfen siz de blog yazın, haber edin biz de takip edelim yeni şeyler öğrenelim 🙂
Neyse şimdi derin bir nefes alıp başlayalım. Zira bu konular ciddi konular. Tek nefes yetmeyecek ama her nefesi derin almakta faydalar var.
Mutlak Adres İşaretçileri (Pointers to Absolute Addresses)
İşaretçilerin genel felsefesini bir önceki yazıda konuştuk. Genelde bir tipteki bir işaretçi, genelde aynı tipteki bir değişkeni işaret ediyordu. Peki ya bir işaretçi, sabit bir adresi işaret ederse?
Hmm. İyi güzel, etsin de niye etsin? Gömülü sistemlerde göreceksiniz ki, çalıştığınız platformun (mikrokontrolör, fpga, SoC, vs) sabit bir bellek haritası var. Yani ROM,RAM, çevreseller filan hep sabit adreslerden başlar. Doğal olarak, register’lar da sabit adreslere sahiptir.
Register Ne Ki?
İlk defa duyanlar için register; gömülü sistemde donanımın izin verdiği bazı donanımsal konfigürasyonları saklamak için kullanılan bellek alanıdır. Örneğin bir pinin giriş olarak mı, çıkış olarak mı konfigüre edildiği bir regsiter’da tutulur. Türkçe’ye “kütük” olarak çevrilmiştir ancak bu çeviri kütük gibi bir çeviri olduğundan, ben düzgün bir çeviri çıkana kadar register diyeceğim.
Register’lar sabit adreslere sahiptir dedik. Öyleyse biz bunlara erişirken yani buraları işaret ederken aslında sabit adresleri işaret edeceğiz. Bu vesile ile mutlak adresleri işaret eden işaretçilerin nerede kullanılacağı hususunu apaçık bir şekilde gözler önüne sermiş olduk.
Öyleyse şimdi bir örnek üzerinden gidelim. Diyelim ki bizim mikrokontrolörün üç tane versiyonu var: A,B,C. Ve elimizdeki mikrokontrolörün 0xC0FFE nolu bellek adresinde bu kıymetli bilgi yazıyor. Bu bilgiyi nasıl okuyacağız? Hemen yazalım!
|
#include <stdint.h> #include <stdio.h> #define REG_DEVICE_TYPE_ID_CHAR (0xC0FFE) int32_t main() { int8_t *pRegDevTypeIdChar = (int8_t*)REG_DEVICE_TYPE_ID_CHAR; printf("Device Type ID Char = %c", *pRegDevTypeIdChar); return 0; } |
Notasyon ve kod gayet temiz diye düşünüyorum. Peki diyelim aynı mikrokontrolörün 0xFACE adresinde bulunan register da mikrokontrolörü başlatmak için olsun. Bizim mikrokontrolörü başlatmak için ille de 0xFACE adresine ‘S’ yazmak gereksin. Onu nasıl yapardık?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
#include <stdint.h> #include <stdio.h> #define REG_DEVICE_START_CMD (0xFACE) #define CMD_DEVICE_START ('S') #define CMD_DEVICE_PAUSE ('P') #define CMD_DEVICE_RESET ('R') int32_t main() { int8_t *pRegDevStartCmd = (int8_t*)REG_DEVICE_START_CMD ; //Start device by writing relevant register to 'S' *pRegDevStartCmd = CMD_DEVICE_START; //... //Pause Device by writing relevant register to 'P' *pRegDevStartCmd = CMD_DEVICE_PAUSE ; return 0 } |
ÖNEMLİ NOT!
NOT: Yukarıdaki kodu bilgisayarınıza atıp çalıştırmayın diye return 0’dan sonraki virgülü sildim ve sistemin özellikle derleme hatası vermesini istedim. Bilgisayarımızda 0xFACE alanı korunan, özel bir bellek alanı olacaktır. Yukardaki koddan yalnızca ibret çıkarınız, kodu bilgisayarınızda çalıştırmanıza bence pek gerek yok 🙂 Çalıştırsanız da zaten yazma koruması olduğundan kod donacaktır ve işletim sistemi tarafından sonlandırılacaktır.
Gördüğünüz gibi muhteremler, sabit adresleri işaret eden işaretçileri sıklıkla kullanacağız. Şu ana kadar bellekteki bilgiyi hep int8_t olarak anlamlandırdık. Gerçekçi durumlarda genelde konfigürasyonlar mikrokontrolör register’larında struct gibi saklanır. O sebeple struct’ı nasıl sabit bir adrese gösteririz onu düşünmekte faydalar var.
Benzer bir mevzu ama, registeri doğrudan işaret eden bir etiket de aşağıdaki gibi tanımlanabilir:
|
#define NVIC_ST_CTRL_R (*((volatile uint32_t *)0xE000E010)) #define NVIC_ST_RELOAD_R (*((volatile uint32_t *)0xE000E014)) #define NVIC_ST_CURRENT_R (*((volatile uint32_t *)0xE000E018)) #define GPIO_PORTF_DATA_R (*((volatile uint32_t *)0x400253FC)) |
Fonksiyon İşaretçileri (Function Pointers)
Fonksiyon işaretçilerinin bir örneğini aslında karar yapıları yazısında vermiştim ama pek açıklamamıştım. Şimdi bu vesileyle bu önemli konuyu da bir nebze daha pekiştirme fırsatı bulmuş olacağız.
C dilinde fonksiyonlar da esasen bir nevi değişkendir. Ancak fonksiyonlar, daha önceden de detaylıca açıkladığımız üzere biraz pahalı değişkenlerdir. Bu vesileyle bedeli ödenerek tanımlanmış fonksiyonların, ikinci aşamada işaret edilmesi ihtiyacı doğmaktadır. Hemen örneğimizi hatırlayalım.
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
|
#include <stdint.h> #include <stdio.h> #define STATE_DEMO_LOOP_CNT (10) typedef void (*myStateHandler) (void); typedef enum { E_STATE_NONE=0, E_STATE_1, E_STATE_2, E_STATE_3, E_STATE_4, E_STATE_LAST }E_STATES; myStateHandler myStateList[E_STATE_LAST]; E_STATES e_state; void STATE1_HANDLER (void) { printf("State1n"); e_state = E_STATE_3; } void STATE2_HANDLER (void) { printf("State2n"); e_state = E_STATE_4; } void STATE3_HANDLER (void) { printf("State3n"); e_state = E_STATE_2; } void STATE4_HANDLER (void) { printf("State4n"); e_state = E_STATE_1; } void Init() { myStateList[E_STATE_1] = STATE1_HANDLER; myStateList[E_STATE_2] = STATE2_HANDLER; myStateList[E_STATE_3] = STATE3_HANDLER; myStateList[E_STATE_4] = STATE4_HANDLER; e_state = E_STATE_1; } int main() { uint8_t bCnt; Init(); for(bCnt=0;bCnt<STATE_DEMO_LOOP_CNT;bCnt++) { myStateList[e_state](); } while(1); //stop here return 0; } |
Örneğimiz aslında ballı kaymak. Neden? Çünkü fonksiyon işaretçisini tanımlamak ile yetinmemişiz, o özel işaretçi tipini bir de typedef ile tanımlamışız ki, ihtiyacı olan herkes bu yeni tipten faydalansın. Bir nevi hayrat yaklaşımı olmuş. Neyse… Fonksiyon işaretçisi aşağıdaki gibi tanımlanmış:
|
void (*myStateHandler) (void); |
Bu yazım tipi sabit. “donusTipi (*isim) (argumanlar)” şeklinde bir kalıbımız var. Fonksiyon işaretçisi böyle tanımlanıyor. Peki buradaki hikmeti ne bu işaretçinin? Switch-case yerine bunu kullandık tamam, aynı şekilde de çalıştı ama bu mevzunun altında yatan felsefe ne? Nesneye dayalı programlamada polymorphism yani çok şekillilik dediğimiz mevzunın C dili ile gerçeklenmesi aslında bu. Yukarıdaki kodda her durummun bir durum fonksiyonu var. Bu fonksiyonun şekli şemali hepsi için ortak. Ve hangi durum olursa, o durumun fonksiyonu çağırılıyor. Her biri aynı şekilde şemalde bu fonksiyonların, hepsini aynı işaretçi işaret ediyor sırayla ama o anki durum ne ise, o an onun fonksiyonu işaret edildiğinden her seferinde ilgili durumun fonksiyonu çağırılmış oluyor. Bu da gerçek hayatın modellenmesi konusunda bizlere büyük avantajlar sunuyor.
Fonksiyon işaretçilerinin kullanım alanları elbette ki bununla sınırlı değil. Eğer bir SDK geliştiriyorsanız, muhtemelen Callback sözcüğü ile yatıp, Callback sözcüğü ile kalkacaksınız. Bu callback mevzusunda da fonksiyon işaretçileri kullanılıyor. Şimdi diyelim ki SDK geliştiriyorsunuz ve SDK’da özel bir olay olduğunda, kullanıcının size söylediği bir fonksiyonun çağırılmasını istiyorsunuz. Yani çağırılacak fonksiyonu aslında bilmiyorsunuz bile. Sadece o olay gerçekleştirildiğinde, sisteme kaydedilmiş bir callback var ise o çağırılsın istiyorsunuz. Bu işi nasıl yazarız?
|
#ifndef H_MYSDKFILE #define H_MYSDKFILE #include <stdint.h> typedef void (*fpEventCallback)(void); void SDK_RegisterOverflowCallback(fpEventCallback OverflowCallback); #endif |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
#include "mysdkfile.h" #define SDK_EVENT_INFO_LIMIT (0x02EA) fpEventCallback SdkOvfCallback = NULL; void SDK_RegisterOverflowCallback(fpEventCallback OverflowCallback) { if(NULL != OverflowCallback) { SdkOvfCallback = OverflowCallback; } } void MyEvent(uint32_t eventInfo) { if(eventInfo > SDK_EVENT_INFO_LIMIT && (NULL != SdkOvfCallback)) { SdkOvfCallback(); // call the callback for overflow if it is registered } //.. } |
Şimdi yukarıda, SDK’yı yazanlar olarak biz çiçek gibi event callback register mekanizmasını verdik. Artık MyEvent olayı geldiğinde, eğer olay bilgisi, sınır değerden büyükse, kullanıcının verdiği herhangi bir fonksiyon çağırılacak. Bu fonksiyon herhangi bir iş yapabilir, kullanıcı ne isterse! Ama içinde while(1); gibi terbiyesizliklerin olmaması lazım! Şimdi SDK’yı kullanmak isteyen, usturuplu, terbiyeli bir yazılımcı kendi fonksiyonunu nasıl bizim SDK’ya kaydeder onu yazalım.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
#include <stdint.h> #include "mysdkfile.h" void OverflowHandler(void) { printf("There is an overflow! Mom, help me!!!rn"); OverflowFixerFunc(); } int32_t main() { InitializeSystem(); SDK_RegisterOverflowCallback(OverflowHandler); for(;;) { DoWhatIsNeeded(); } } |
Gördüğünüz gibi, bir fonksiyona parametre olarak başka bir fonksiyonu verdik. 🙂 Bu ibretlik olayı fonksiyon işaretçilerinin varlığına borçluyuz. Buradan indirilecek ibreti beyinlerimize indirdiğimizi düşünüyorum. Öyleyse bir sonraki konuya geçebiliriz.
Genel İşaretçi (Void Pointer, void*)
Hiç uzatmadan konuya gireceğim. C dilinde, bir nevi joker olarak kullanabileceğimiz, her yere yedirebileceğimiz, her şeye cast edebileceğimiz bir veri tipi bulunuyor. Bu veri tipi, çok açık söylüyorum void*’dır. Genel işaretçi olarak tarif edebileceğimiz bu işaretçi tipi ile, her şeyi işaret edebilirsiniz. Daha doğrusu, herhangi bir şeyi işaret edebilirsiniz. Bu da bize jenerik kodlama yapma şansı sağlıyor.
Diyelim iki veriyi toplayan bir fonksiyon yazacak olalım. Her bir veri tipi için ayrı ayrı topla fonksiyonu yazmadan nasıl yaparız bu işi? Tüm veri tipleri için olmasa da, 32 bite kadar olan standart veri tipleri için şöyle yaparız:
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
|
#include <stdint.h> #include <stdio.h> /*Maximum 32 bit numbers summation*/ uint32_t Summation(void* pParam1, void* pParam2 ) { uint32_t retVal; if(NULL != pParam1 && NULL != pParam2) { retVal = ((*(uint32_t*)pParam1) + (*(uint32_t*)pParam2)); } return retVal; } int32_t main() { int32_t i32Num1 = 29; int32_t i32Num2 = 11; int8_t i8Num1= 5; int8_t i8Num2= -2; printf("Summation of int32_t numbers: %d\r\n", (int32_t)Summation(&i32Num1,&i32Num2)); printf("Summation of int8_t numbers: %d\r\n", (int8_t)Summation(&i8Num1,&i8Num2)); return 0; } |
Gördüğünüz gibi büyük oranda jenerik bir fonksiyon yazmış olduk. Ayrıca void işaretçisini diğer tipteki işaretçilere cast ettiğimize dikkatinizi çekerim. Bu mereti, her tipteki işaretçiye cast etmek (aktarmak) mümkün.
Dikkat
Yukarıdaki fonksiyon float veri tipi için doğru çalışmayacaktır. Float veri tipleri, derleyici tarafından IEEE754 formatına uygun olarak -özel yöntemlerle- saklandığından ve toplandığından, uint32 formatı üzerinden iki float sayının toplanması doğru sonucu vermez. Yukarıdaki kodu, profesyonel uygulamalarda kullanmayınız.
Son olarak kısa bir hatırlatma yapayım. Bir sistemin kaç bitlik olduğunu öğrenmek için:
|
#include <stdint.h> #include <stdio.h> int32_t main() { printf("Bu platform %d bitliktirrn", 8*sizeof(void*)); getchar(); return 0; } |
Bence void* büyük oranda anlaşıldı. Şimdi yine her zamanki sevimli hareketlerden birini yapacağız. Bilgilerimizi harmanlayacağız. Fonksiyon işaretçileri ile genel işaretçileri birleştirdiğimizde ne kadar güçlü bir şey elde ettiğimizi düşünelim.
|
typedef void (*DataSendOverSerial)(void* pData, in32_t lenInBytes); |
Yukarıdaki fonksiyon işaretçisi tipi, giriş parametresi olarak genel işaretçiyi alıyor. Yukarıdaki fonksiyon tipindeki bir fonksiyon, tip ayrımı yapmaksızın her türlü veriyi seri porttan gönderebilir. Float olsun, int olsun, char olsun, int32_t olsun, hatta ve hatta bir fonksiyonu bile seri port üzerinden gönderebilirsiniz 🙂 Ve aynı zamanda size polimorfik bir yapı sunar. Kızarmış ekmek üstünde bal kaymak gibi… Mis.. Afiyet olsun 🙂 Öyleyse devam!
Kısaltma İçin İşaretçiler
C dilinde, kompleks veri yapıları diye bir mevzu var. İlk başta şaka gibi geliyor bu ifade ama bazı veri yapıları hakikaten kompleks olabiliyor 🙂 Bir sturct içinde başka bir struct, onun içinde afedersiniz başka bir struct, onun içinde de bir işaretçi düşünelim. Bu durumda en alt seviyedeki değikene ulaşmak hakikaten bayağı zaman alacaktır. Özellikle tembel yazılımcıları, klavyede bu kadar tuşa basmaktan nefret ederler. Bu gibi durumlarda da kısaltma amaçlı olarak işaretçilerden faydalanabiliriz. Misal:
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
|
typedef struct { int8_t test1; int8_t mest1; int8_t jest1; int8_t kest1; int8_t lest1; }s_Struct3; //myStruct3'ün tipi s_Struct3* olsun. agaogluMyStruct0.myStruct1.myStruct2[index]->myStruct3->test1 = 1; agaogluMyStruct0.myStruct1.myStruct2[index]->myStruct3->mest1 = 2; agaogluMyStruct0.myStruct1.myStruct2[index]->myStruct3->jest1 = 3; agaogluMyStruct0.myStruct1.myStruct2[index]->myStruct3->kest1 = 4; agaogluMyStruct0.myStruct1.myStruct2[index]->myStruct3->lest1 = 5; //yazmak yerine; s_Struct3* kucukStruct3; kucukStruct3 = agaogluMyStruct0.myStruct1.myStruct2[index]->myStruct3; kucukStruct3->test1 = 1; kucukStruct3->mest1 = 2; kucukStruct3->jest1 = 3; kucukStruct3->kest1 = 4; kucukStruct3->lest1 = 5; //yazmak suphesiz daha kisa oluyor |
İlk bakışta komik bir kullanım alanı gibi gözükse de, işin içine girdiğimizde bu tip kullanımların yadsınamaz miktarda olduğunu görüyoruz. Belki de tembelliğin böylesi candır canandır 🙂 Tabi burada bir pointer’ı ekstradan kullanarak bellekten biraz çaldık. Değer mi değmez mi o kararı size bırakıyorum.
Diziler ve İşaretçiler
Diziler ve işaretçiler birbirlerine çok çok yakın iki kavramdır. Nihayetinde her bir dizi, aslında bir işaretçidir. Ancak bu özel işaretçiler tanımlanırken, işaret edecekleri bellek alanları önceden ayrılır.
Diziler de tıpkı diğer değişkenler gibi ilk değer ataması mevzularına tabidir. İlk değer ataması yapılmamış bir dizi tanımı aşağıdaki gibidir:
Burada yalnızca dizinin 10 elemanlı olacağı bilgisi verilmiş. yani u8TestArray değişkeninin işaret ettiği bellek adresinden başlamak üzere 10 byte bu dizinin elemanları olarak sıralanacak.
Şimdi ilk değer ataması yapılmış ancak boyut verilmemiş bir diziye bakalım.
|
int8_t testArray[] = "Hello!"; |
Burada test array aslında bildiğimiz işaretçidir. Gördüğünüz üzere kendisine “Hello!” şeklindeki karakter dizisini yani string’i atamış olduk. C dilinde string, ayrı bir tip değildir ancak karakterlerden oluşan bir dizidir. Nitekim yukarıdakinin bir benzeri şöyledir:
|
int8_t *testPtr = (int8_t*) "Hello!"; |
testArray dediğimiz şey aslında şu aşağıdaki ile aynıdır:
Çünkü dizinin ismi yani değişken adı (testArray), aslında dizinin ilk elemanının adresini taşır (&testArray[0]).
İşte bunun farkında olmakta ve bunu unutmamakta faydalar var.
Yukarıdaki örnekte verdiğimiz Hello! yazısını karakter karakter yazdırmak istesek dizinin her bir elemanını yazdırabileceğimiz gibi, pointer aritmetiği ile de aynı işi yapabiliriz.
|
#include <stdint.h> #include <stdio.h> int32_t main() { int8_t* testPtr = (int8_t*)"Hello!"; while(*testPtr) { printf("%c",*testPtr++); } getchar(); return 0; } |
Struct ve İşaretçiler
Yapılar başlıklı yazımda esasen bu konuya değinmiştim ancak ufaktan tekrar etmekte fayda var diye düşünüyorum. Struct genelde kocaman bir yapıyı modellediğinden bellekte kapladığı yer ciddiye alınmalıdır. Hal-i hazırda küçücük olup ciddiye almadığınız bir struct, yazılımın bir sonraki versiyonunda büyüyebilir ve size ciddiye almak zorunda kalacağınız bazı problemler yaratabilir. O sebeple saygıyı baştan göstermekte faydalar var.
Bir struct tanımlandıktan ve onunla ilgili bilgi belleğe yazıldıktan sonra, o veriyi kullanmak için başka bir sturct yaratmak çoğu durumda anlamsız ve mantıksızdır. Onun yerine struct işaretçisi tanımlanarak, ilgili struct’ı işaret edecek şekilde değer ataması yapılmasında sayısız faideler ve feyizler vardır.
Union ve İşaretçiler
Union bildiğiniz üzere; her elemanı belleğin aynı adresini gösteren özel bir yapı. Kendisinin feyizli özelliklerini sizlerle paylaşmıştık. Ancak tanımından da anlaşılabileceği üzere kendisi aslında bir çeşit özel işaretçidir. Dolayısıyla union kullanarak yapabileceğimiz bir çok işi, çok daha çirkin şekilde doğrudan işaretçilerle de yapabiliriz. Misal:
|
uint32_t nRead; uint32_t index; typedef union { uint32_t u32Data; int16_t as16Data[2]; int8_t as8Data[4]; }tu_DataGroup32; tu_DataGroup32 UData; // ( Code to read in nRead, from the user or a file, has been omitted in this example ) UData.u32Data = nRead; for( index= 0; index < sizeof(uint32_t); index++ ) { printf( "Byte number %d of %ud is %udn", index, nRead, UData.as8Data[index] ); } |
yerine benzer işi yapan şöyle bir kod da yazabiliriz.
|
uint32_t nRead; uint32_t index; for( index= 0; index < sizeof(uint32_t); index++ ) { printf( "Byte number %d of %ud is %udn", index, nRead, (*(int8_t*)((&nRead) + index)) ); } |
Ama dediğim gibi çirkin olur kem olur. Parantez manyağı olmak zorunda kalırız ve kodumuz da anlaşılmaz.
Ancak yine de olayın altında yatan felsefeyi bilmekte faideler var.
NULL İşaretçisi
Bu NULL denen nane çok meşhurdur, her yerde de kullanılır. NULL işaretçisi esasen belleğin 0 numaralı adresini gösteren bir işaretçidir. Kendisinin tipi void*’dır.
Kütüphanelerdeki (stddef.h) tanımına baktığımız zaman şunu görürürüz:
NULL dediğimiz nane işte budur. Belleğin 0 nolu bölgesini gösteren bir işaretçi. Peki neden bir işaretçinin “boş” olup olmadığını kontrol etmek için bunu kullanırız? Çünkü bir işaretçi ilk değer ataması yapılana kadar sıfır nolu bellek adresini gösterir. Dolayısıyla bu ona ilk değer ataması yapılmadığını anlamanın güzel bir yoludur.
Fonksiyonlara Argüman Olarak İşaretçiler
Fonksiyonun pahalı bir değişken olduğunu her fırsatta söylüyorum, söylemeye de devam edeceğim. Şimdi bu pahalılıkta önemli bir gider kalemi de fonksiyonların argümanlarıdır. Bir fonksiyonun argümanları belleğin özel bir bölgesine kopyalanır. Sözü uzatmadan mevzuya dalacağım. Eğer kopyalanan şey devasa bir struct ise vay halinize. Ne demiştik? Struct kopyalamak çoğu durumda mantıksızdır. Demek ki fonksiyona argüman olarak struct yerine struct işaretçisini vermekte fayda var. Bu aynı zamanda fonksiyonun kullanacağı belleğin, fonksiyonu çağıracak kişi tarafından ayrılmasına (allocate etmek) izin verdiğinden, (caller allocation prensibi) fevkalade lezzetli bir olaydır.
Bu mevzu hakkında detaylı bilgiyi ve örnek kodları BURADAN, “Struct’ların Pointer(İşaretçi) ile İmtihanı” başlıklı bölümden bulabilirsiniz.
Fonksiyonlara argüman olarak işaretçi verilmesinin bir diğer önemli kullanım alanı da, o fonksiyonun argümanını fonksiyon içinde değiştirmesine olanak vermektir. Bu özellik yadsınamaz derecede önemlidir. Her fonksiyon tek iş yapmalıdır demiştik ama bazı durumlarda bir fonksiyonun birden fazla değer döndürmesi gerekebilir. Öyleyse o değerlerin argümanlarda işaretçi olarak alınması yeterli olacaktır çünkü bir kez değişkenin adresi bilindi mi, onun içindeki veriyi manüple etmek problem olmayacaktır 🙂
Hemen kısa bir örnek vereyim:
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
|
#include <stdio.h> #include <stdint.h> void Function1(int8_t a); void Function2(int8_t* a); int32_t main() { int8_t a = 55; printf("a'nin degeri %drn",a); Function1(a); printf("a'nin degeri %drn",a); Function2(&a); printf("a'nin degeri %drn",a); getchar(); return 0; } void Function1(int8_t a) { a++; printf("%s icinde a'nin degeri %drn",__FUNCTION__,a); } void Function2(int8_t* a) { (*a)++; printf("%s icinde a'nin degeri %drn",__FUNCTION__,*a); } |
Bu kod çalıştırıldığında çıktısı şöyle olur:
Konsol Çıktısı
a’nin degeri 55
Function1 icinde a’nin degeri 56
a’nin degeri 55
Function2 icinde a’nin degeri 56
a’nin degeri 56
——————————–
Process exited with return value 0
Press any key to continue . . .
Gördüğünüz gibi Fonksiyon1, a değişkenini fonksiyonun dışında etkili olacak şekilde değiştirememiş, ancak Fonksiyon2 değiştirebilmiştir. Bunun sebebi aslında basittir, Fonksiyon1 a değişkeninin kendisini değil, adi bir kopyasını değiştirmiştir. Nitekim a’ların adresleri yazdırılsa farklı çıkacaktır. Bu merete Japonlar bunshin diyor 😀 Bilenler bilir.
Komut Satırı Girişi
Son olarak, komut satırı argümanlarından bahsedeceğim. Bu, gömülü sistemlerde çok da önemli değil ancak bilmekte faydalar var.
|
int main( int argc,char * argv[ ] ); int main( int argc, char **argv ); |
Yukarıdakiler hemen hemen aynı mevzular. Siz yazdığınız kodu derleyip konsoldan çağırdığınızda ona parametre verebilirsiniz. Bunun adı komut satırı argümanlarıdır ve program çağırılırken bu bilgi programa verilebilir. Gömülü sistemlerde kodu komut satırından çağırmadığınızdan bu olay bu amaçla kullanılmaz ama başka amaçlarla kullanılabilir 🙂 Önemli olan mevzu şudur; verdiğiniz argümanlar arasındaki boşluk bulunan kelimelerdir. Bu kelimelerin sayısı argc’ye, kendileri de argv’ye işletim sistemi tarafından aktarılır.
Hemen çok sık verilen şu kodu biz de örnek olarak verelim:
|
#include<stdio.h> int main(int argc,char **argv) { int i; for(i=0;i<argc;i++) { printf(“%sn”,argv[i]); } return 0; } |
Bu kodu selam.c olarak kaydedip derlediğimizi, ve çıktı olan programın adının da selam olduğunu düşünüyorum. Komut satırından programı şöyle çağıralım “selam kardes kafan cok guzelmis nerden aldin?”
Bu durumda komut satırına girilen argüman sayısı argc = 7 olacaktır. argv[0]’ın gösterdiği yerde programın adı yani selam yazacakken argv[1]’in gösterdiği yerde kardes yazacaktır. Dolayısıyla bu kodun çıktısı şöyle olur:
Konsol Çıktısı
selam
kardes
kafan
cok
guzelmis
nerden
aldin?
Sık Yapılan Hatalar
Arkadaşlar işaretçilerin biliçsiz kullanıması çok sayıda hataya yol açabilir ve bunlar gerçekten zorlu hatalar olacaktır. Ancak sizler, işaretçileri buradaki bilgilerle birlikte kullandığınızda bu sorunları zaten yaşamayacaksınız 🙂
*Misal aşağıdaki gibi bir kod yazmayacağınızı umuyorum:
|
int32_t * ptr , m = 100 ; ptr = m ; //eyvah! dogrusu ptr = &m olacak! |
*Bir diğer hata da ilk değer atanmamış bir işaretçinin göstermediği belleğin içine bir şeyler yazmaya çalışmaktır. Kemdir, puan götürür, yapmayınız. Misal:
|
int32_t * ptr , m = 100 ; *ptr = m ; // eyvah! daha ptr'nin nereyi gösterdiği belli degil |
Daha ptr’nin nereyi gösterdiğini ayarlamadan, bu belleğin içine m’nin değerini yazmaya çalışıyoruz. Önce bu ptr bir yeri göstermeli. Misal:
|
int32_t * ptr; int32_t m = 100 ,n = 20; ptr = &n; *ptr = m ; |
*Diğer bir hata da, bir işaretçinin, ilk değer atanmamış bir değişkeni işaret etmeye çalışmasıdır. Bu şık bir kullanım değildir. Önce işaret edilecek değişkene ilk değer atamakta faydalar vardır.
|
int32_t *ptr, int32_t m; ptr = &m; |
*İki işaretçiyi karşılaştırmak da iyi bir fikir sayılmaz. İşaretçiler, bellekteki rastgele alanları göstereceğinden, işaretçileri doğrudan karşılaştırmak anlamsızdır. Anlamlı olabilecek şey, bunların gösterdiği değerleri karşılaştırmaktır.
|
char str1[10],str2[10]; char *ptr1 = str1; char *ptr2 = str2; if(ptr1 > ptr2) ..... // Eyvah { .... .... } if(*ptr1 > *ptr2) ..... // Eyvallah { .... .... } |
Daha bunu sıralamakla bitmez ama siz anladınız mevzuyu.
Yazıları beğendiyseniz eğer, faydalanabilecek arkadaşlarınızla da paylaşabilirseniz sevinirim.
Şimdi devam…
Önceki Sayfa Sonraki Sayfa