Bu yazı, C++ dilinin işaretleyicilerini anlatmak ve UE üzerinde programlama yapmak için anlaşılması kolay bazı örnekler verilmiştir. Java, C# gibi dillerde temel olarak işaretçiler yoktur. Bu dillerde Çöp Toplayıcı(Garbage Collector) otomatik olarak bellek yönetimini sizin yerinize gerçekleştirir.)
Bir Java veya C# programlama dilini bilen, C++ dilini öğrenirken belki de en çok işaretçiler konusunda zorlanır.
Bu yazıdan bir Alıntı yapacaksanız Ali Kubur'a teşekkürler demeniz benim için keyif olacaktır.
Öncelikle, temelini öğrenelim. Nelerde kullanacağız onları görelim. Daha sonra işaretçileri nasıl kullanacağız görelim.
İşaretçi(Pointer) Nedir?: Her bir byte, bilgisayarın hafızasında bir adresi vardır. Adresler numaralardır, mahallenizdeki evlerin kapı numaraları gibi... Programınız belleğe yüklendiğinde(çalıştırıldığında), programınızdaki her değişkenin, her nesnenin, her fonksiyonun bellekte ayrılmış özel(eşsiz) bir adresi vardır.
Adresleri anlamak için olarak şöyle bir programımız olsun:
[code-sh=cpp]#include <iostream>
int main()
{
int var1 = 51;
int var2 = 25;
int var3 = 5;
return 0;
}[/code-sh]
Bu program çalıştırıldığında, belleğinizin bir kısmında aşağıdaki şekil gibi bir olay meydana gelir.
Yukarıdaki resimde gördüğünüz ff0, fff4, ff8 her bir değişken, fonksiyon veya nesne için yaratılacak eşsiz olan bellek adreslerdir. Adresler bilgisayarınızın mimarisine göre değişir.
fff0 adresi 51 değerini bulunduruyor yani bu da program içeriside olan var3'e denk geliyor. Aynı şekilde ff4 adresi 25 değerini bulunduruyor.
Yani her değişkenin, fonksiyonun veya nesnenin bellekte tutulan bir adresi vardır. İşaretçileri kullanırız çünkü, bu adreslere sahip oluruz ve belleği dinamik olarak yönetebiliriz. Belleği yönetmekle birlikte kendi tanımladığımız kodları kullanarak işaretçiler ile bellekte bir alan yaratabiliriz/silebiliriz/yerini değiştirebiliriz.
Çöp toplayıcı(Java, C# gibi..) olan dillerde bellek yönetimi otomatik olarak yapılıyor. C, C++ gibi dillerde ise programcı bellekte istediği gibi alan tahsis edebilir.
Neden işaretçileri kullanırız?
0- Belleği daha efektif bir şekilde kullanmak. Çünkü bir değişkenin veya nesnenin adresini işaretçi bilir.
1- İşaretçiler diziler için kullanabiliriz. Pointer aritmetiğine daha sonra değineceğim.
2- Bir fonksiyondaki değerin eşsiz olarak aktarılması (Bir fonksiyona ya da metoda parametre olarak işaretçileri geçirebiliriz. Mesela void Yazdir(veri_tipi* arguman))
3- Bağlantılı listeler(Linked List) gibi veri yapılarında daha efektif bir şekilde programlamak(Temel Veri yapılarının bir çoğunda kullanılır).
4- C programlama dilinin sağladığı(malloc, free, realloc) fonksiyonlarını çağırarak bellek üzerinde kendimiz yarattığımız bir hafızayı dinamik olarak yaratabilip/yerleştirebilip/silebiliyoruz.
Dinamik Olarak Hafıza Yönetimi
Kısaca bahsetmek istediğim üç kavramı iyi anlamanız gerekiyor. Yazdığımız programda bu adreslerin hafızada bir şekilde var olduğunu anladık. Şimdi ise bu var olan adreslerin bellekte nerede var olduğundan bahsedeceğiz.
UFAK NOT: Günümüzde modern C++ programcıları artık bellek yönetimini bu şekilde(malloc, free, new operatörü kullanarak vs..) tahsis etmek hammallık. STL kütüphanelerinin getirdiği unique_ptr, shared_ptr ve weak_ptr gibi standart şablon kütüphaneleri tarafından geliştirilen hazır şablonlar var. Bunlara bu yazıda değinmeyeceğim. Onları kullanmamızın sebebinden önce üç kavramı iyice anlamanız gerekiyor.
1 - Stack(Yığın)
Daha çok kısa süreli değerlendirilen bir veri yapısıdır. Stack veri yapısını kullanır. Mesela tanımladığınız local variable, fonksiyon parametreleri ve dinamik olarak yaratılmayan değişkenler, nesneler vs.. stack üzerinden hallolur.
Mesela basit olarak bir Öğrenci sınıfımız olsun.
[code-sh=cpp]#include <iostream>
class Ogrenci
{
public:
// int tipinde bir veri üyesi
int numara;
};
int main()
{
// x stack üzerinden değerlendirilecek ve stack eklenecek
int x;
// Kendi yarattığımız Ogrenci veritipinden olan Ahmet yine stack eklenecek...
Ogrenci Ahmet;
// . (nokta) operatörü kullanarak, nesneye erişip numarasını 42 yapıyoruz.
Ahmet.numara = 42;
std::cout << Ahmet.numara << std::endl;
// Program bu evreye geldiğine Ahmet stack üzerinden otomatik olarak pop edilecek
}
[/code-sh]
Burada yarattığımız nesne(Ahmet) stack üzerinden işlem yapılacak. sürekli pop ve push edilip durur. Aynı şekilde yarattığımız nesnenin(Ahmet nesne oluyor) bellekte olan bir referansı vardır.
2 - Heap(Düzensiz Yığın)
Dinamik olarak yarattığımız hafıza Heap üzerinden değerlendilrilir. Burada yarattığımız işaretçiler(pointerlar) aslında stack üzerinden yarattığımız bir adresin başlangıcını temsil eder. Veyahutta hiçbir adres referans etmeyip "nullptr" değerini alabilir.
[code-sh=cpp]#include <iostream>
class Ogrenci
{
public:
// int tipinde bir veri üyesi
int numara;
};
int main()
{
// x bir işaretçidir ve y'nin adresinin başlangıcını temsil eder.
int y = 8;
int* x = &y;
// Aynı şekilde new opeartörünü kullanarak heap üzerinde yarattık.
Ogrenci* Ahmet = new Ogrenci();
Ahmet->numara = 42;
std::cout << Ahmet->numara << std::endl;
// Ahmet nesnesi Heap üzerinden serbest bırakılıyor
delete Ahmet;
// Ahmet artik bos bir bellek alanini temsil ediyor
Ahmet = nullptr;
}
[/code-sh]
Dinamik olarak yönetilen hafıza programcı tarafından bellekten serbest bırakılmalıdır. Yoksa memory leak oluşur.
Göreceğiniz gibi var olan bir veri tipininin adresinin başlangıcını işaretleyebiliriz veya new opearatörünü kullanarak veya malloc fonksiyonunu kullanarak heap üzerinde dinamik olarak alan yaratabiliriz.
ONERI: STL'nin sundugu unique_ptr, shared_ptr veya weak_ptr gibi sablon siniflarini kullanarak otomatik olarak pointerin bellekten serbest birakilmasi icin var olan sablonlari kullanin.
3 - Static(Durağan): Bütün program üzerinde sadece bir örneği bulunan hafıza alanını temsil ediyor. Yani durağan, statik, bütün program boyunca sadece bir adres lokasyonundan çağrılır. Başka örneği yaratılamaz. Compile-Time(derleme zamanında) adreslenir.
İşaretçileri C++'da kullanmak.
Öncelikle, adresleme ve belleğin mantığını basit olarak anladığımıza göre, işaretçileri nasıl kullanacağımıza bakalım.
* (de-referans operatörü): işaretçi tanımlamak için bu sembolü(operatör) kullanırız.
& (adres operatörü): bir değişkenin, nesnenin veya fonksiyonun adresini almak için işe yarar. yani °isken3 dediğimiz zaman aslında o değişkenin adresini alırız.
[code-sh=cpp]#include <iostream>
using namespace std;
int main()
{
// iki tane tam sayı değişkeni var
int var1 = 11;
int var2 = 22;
cout << &var1 << endl // var1'in adresini yazdırıyoruz.
<< &var2 << endl << endl; // var2'nin adresini yazdırıyoruz.
// tamsayıya bir işaretçi tanımadık
int* ptr;
// ptr adlı işaretçi var1'in adresini işaretliyor
ptr = &var1;
// işaretçinin değerini yaz. işaretçi bir adres tuttuğundan dolayı, var1'in adresini yazacaktır.
cout << ptr << endl;
// ptr adlı işaretçi var2'in adresini işaretliyor
ptr = &var2;
// işaretçinin adresini tekrardan yazdırdık. fakat şu anda işaretçiyi var2'nin adresine işaretledik.
cout << ptr << endl;
system("pause");
}
[/code-sh]
Program Çıktısı (Adres numaraları sizin bilgisayarınız için değişecektir.)
0x084F790 yani var1'in adresi
0x084F784 yani var2'nin adresi
0x084F790 ptr işaretçisi ile var1'in adresi gördüğünüz gibi ptr işaretçisini var 1'in referansı olarak aldık.
0x084F784 ptr işaretçisi ile var2'in adresi
Tekrardan gözden geçirelim.
[code-sh=cpp]// İşaretçi Tanımlanması
int* ptr; // Burada ptr adında bir tamsayıya ait işaretçi oluşturduk.
//Adreslerin işaretlemesi:
ptr = &var1; // burada ptr işaretçisini var1'in adresine işaretledik.
// Tekrardan ptr adresinin işaretlemesi:
ptr = &var2; // burada ptr işaretçisini var2'in adresine işaretledik. ptr'nin aldığı değerde değişti herhalde...
[/code-sh]
Anlayacağınız gibi işaretçi tanımlanan tipte (burada tanımlanan tip int oluyor) adresi işaretler. Adresi tutmakla birlikte bu adresin içerisindeki değeri de tutar.
İşaretçinin değerini almak: (DE-REFERANS ETMEK)
Normal olarak bir işaretçi, referans olarak adresi tutar. Peki biz bu adres yerine işaretçinin tuttuğu değeri almak için ne yapacağız?
İşaretçinin başına * koyarak bu işaretçinin adresi yerine değerini alacağız.
[code-sh=cpp]float x = 42.3; // float veri tipinde bir değişken oluşturduk ve 42.3 sayısını atadık.
float* ptr = &x; // float veri tipinde bir işaretçi oluşturduk ve referans olarak x'in adresini aldık.
// İşaretçilerin değerini ortaya çıkarmak(de-referans etmek)
std::cout << *ptr << std::endl; //Bu sayede adresi yerine şu andaki var olan değerini alırsınız. Eğer *ptr yerine ptr yazarsak o zaman işaretçinin adresine ulaşırız.
// İşaretçinin adresine ulaşmak:
std::cout << ptr << std::endl; //Bu sayede adresi yerine şu andaki var olan değerini alırsınız. Eğer *ptr yerine ptr yazarsak o zaman işaretçinin adresine ulaşırız.
[/code-sh]
Bu örneği yazın ve çalıştırın: Herhalde basit olarak pointerların temelini anlamış olursunuz.
[code-sh=cpp]int main()
{
int var1, var2; // iki tamsayı değişkeni atadık
int* ptr; // tamsayı için ptr adında işaretçi belirledik
ptr = &var1; // ptr işaretçisini değişken 1'in adresine işaretledik.
std::cout << *ptr << std::endl; // Bu satırda ptr adlı işaretçimizin değerini yazdırdık. Eğer işaretçinin adresi yerine değerini bulmak istiyorsanız başına * işaretini yerleştirin.Bu işleme de-referans deniliyor.
*ptr = 37; // var1=37 ile aynı şey demektir. başına konulan yıldız o işaretçinin referansını çıkartır.
var2 = *ptr; //var2=var1 ile aynı şey demektir. *ptr kullanmamızın amacı işaretçiyi değiştirmek.
cout << var2 << endl; // var2 değişkeninin 37 olacağını kontrol edelim.
return 0;
}
[/code-sh]
-> operatörünü kullanmak.
Normal olarak, bir pointerin değerini alırken onun başına * koyarak de-referans ediyoruz. Fakat bu sınıfların, nesnelerini yaratırken için gerçekten güzel bir görüntü sağlamayabilir.
Bunun için pointer olan bir nesneye erişmek için -> operatörünü kullanırız.
Örnek:
[code-sh=cpp]#include <iostream>
#include <string>
class Ogrenci
{
public:
// int tipinde bir veri üyesi
int numara;
// basit bir üye fonksiyonu
void NumaraYazdir(){ std::cout << numara << std::endl; }
};
int main()
{
Ogrenci* yeniOgrenci = new Ogrenci(); // burada Ogrenci veri tipinde yeni bir işaretçi tanımladım ve bu işaretçi yeniOgrenci nesnesini temsil ediyor.
yeniOgrenci->numara = 5; // Bu nesne içerisinde -> operatörünü kullanarak Öğrenci sınıfının içerisindeki numara veri üyesine eriştim ve değerini 5 yaptım.
yeniOgrenci->NumaraYazdir(); // numarayı yazdıralım.
}
[/code-sh]
Aslında -> operatörü var olan bir işaretçiyi dereferans ederek onun değerini ortaya çıkarır.
-> operatörünü kullanmadan önce işaretçiyi deferans edip daha sonra nokta operatörü ile de erişebilirdik. (*yeniOgrenci).NumaraYazdir();
nullptr kavramı:
Bir işaretçi hiçbir adresi işaret etmeyebilir. O zaman boş işaretçi değerini alır. Özellikle bir nesnenin var olup olmadığını bu şekilde kontrol edebiliriz. Veya bir işaretçi sabit olarak tanımlanmadıysa, işaretçinin işaret ettiği adresi boş olarak tanımlayabiliriz. Java dilindeki"null" mantığı ile aynı şekilde çalışır. Fakat bu işaretçiler için geçerlidir.
Örnek:
[code-sh=cpp]Ogrenci* baskaOgrenci = nullptr; // baskaOgrenci işaretçisi artık hiçbir adresi referans etmiyor.
[/code-sh]
Aynı şekilde bir objenin adresinin geçerli veya geçersiz olduğunu anlamak için nullptr ile kontrol edebilriiz.
[code-sh=cpp]if(birIsareci != nullptr)
{
std::cout << "birIsareci adinda bir işaretçi bir adresi işaretliyor. yani referans geçerlidir" << std::endl;
}
[/code-sh]
Parametre olarak işaretçi geçirmek:
[code-sh=cpp]#include <iostream>
using namespace std;
// böyle bir fonksiyon tanımlıyorum, parametre olarak bir işaretçi alıyor, ve bu işaretçinin değerini de-referans edip 2.54 ile çarpıyor daha sonra bu değere atıyor.
void centimize(double* ptrd)
{
*ptrd *= 2.54; // *ptrd işaretçisi ile gelen değeri 2.54 sayısı ile çarpıp santimetreye çevirdik. en solda kullandığımız * işareti ile de-referans edip işaretçinin değerini alıyoruz.
// diğer * ise çarpıp üzerine eklediğimiz için kullandığımız çarpma olayı.
}
int main()
{
double var = 10.0; // var değişkenine 10.0 atadık.
cout << “degisken = “ << var << “ inc ” << endl;
centimize(&var); // değişkenin adresini fonksiyona uyguladık.
cout << “var = “ << var << “ santimetre” << endl;
return 0;
}
//--------------------------------------------------------------
[/code-sh]
Parametre olarak işaretçi geçirmenin c++ programlamada bir çok örneği mevcut, bu yazıda temel mantığı anladıktan sonra Unreal Engine'da kullandığım bir kod parçacağından anlatmak istiyorum.
[code-sh=cpp]// ATriggerVolume sınıfına ait işaretçi nesnesi(object) tanımlıyorum.
ATriggerVolume* PressurePlate;
[/code-sh]
Aşağıdaki örnek biraz karışık gelebilir fazla takmayın. Öncelikle -> "ok" göstergeçin ne olduğunu belirtelim. Bir sınıftan bir pointere ulaşmak istiyorsanız -> kullanmanız lazım. Kullanılan döngü uzak-tabanlı "ranged-based" ve auto anahtar kelimesi değişkenin int, char, float ya da başka bir şey gibi belirtmeden otomatik olarak tanıtılmasını algılayan bir kelime. neyse bunlardan ziyade aşağıda örnekte işaretçileri (*) ve referans adreslerini(&) nasıl kullanıldığına bakın.
[code-sh=cpp]// Diyelim ki içerisinde aktörleri bulunduran bir işaretçi dizimiz olsun.
TArray<AActor*> OverlappingActors;
for (const auto& Actor : OverlappingActors)
{
TotalMass += Actor->FindComponentByClass<UPrimitiveComponent>()->GetMass(); // kütleleri topla
UE_LOG(LogTemp, Warning, TEXT("Total actor on plate : %s"), *Actor->GetName()); // burada ismine ait bir stringi de-referans ettik.
}[/code-sh]
[code-sh=cpp]*Actor->GetName(); // burada, gördüğünüz üzere aktörün ismini pointer ile aldığım için ve metoda erişmek için -> operatörü kullanılmış.
// daha sonradan GetName bir string işaretçi döndürüyor, bunun değerini almak için * operatörünü kullanıldı ve de-referans edildi.[/code-sh]
Bir diğer yaptığımız bir çok şey ise nullptr yani işaretçinin değerini boşaltmak, yani işaretçi var olan Lamb aktörü için hiçbir nesnenin adresini almıyor.
[/code-sh]
*AActor Lamb = nullptr; // gibisinden. eğer aktör yoksa gibisinden düşünebilirsiniz.
[/code-sh]
Ayrıca Unreal Engine API referansına bakarsanız parametrelerin işaretçi aldığını görebileceksiniz
Yukarıda gördüğünüz üzere, TArray kalıbı(template) ait Append metodumuzun iki tane parametresi var. İlk parametre olarak, yaratılan tipin bir işaretçisini alıyor. Bu sayede değeri eşsiz olarak geçirebiliyor.
const ElementType * Ptr,
int32 Count
Bir Java veya C# programlama dilini bilen, C++ dilini öğrenirken belki de en çok işaretçiler konusunda zorlanır.
Bu yazıdan bir Alıntı yapacaksanız Ali Kubur'a teşekkürler demeniz benim için keyif olacaktır.
Öncelikle, temelini öğrenelim. Nelerde kullanacağız onları görelim. Daha sonra işaretçileri nasıl kullanacağız görelim.
İşaretçi(Pointer) Nedir?: Her bir byte, bilgisayarın hafızasında bir adresi vardır. Adresler numaralardır, mahallenizdeki evlerin kapı numaraları gibi... Programınız belleğe yüklendiğinde(çalıştırıldığında), programınızdaki her değişkenin, her nesnenin, her fonksiyonun bellekte ayrılmış özel(eşsiz) bir adresi vardır.
Adresleri anlamak için olarak şöyle bir programımız olsun:
[code-sh=cpp]#include <iostream>
int main()
{
int var1 = 51;
int var2 = 25;
int var3 = 5;
return 0;
}[/code-sh]
Bu program çalıştırıldığında, belleğinizin bir kısmında aşağıdaki şekil gibi bir olay meydana gelir.
Yukarıdaki resimde gördüğünüz ff0, fff4, ff8 her bir değişken, fonksiyon veya nesne için yaratılacak eşsiz olan bellek adreslerdir. Adresler bilgisayarınızın mimarisine göre değişir.
fff0 adresi 51 değerini bulunduruyor yani bu da program içeriside olan var3'e denk geliyor. Aynı şekilde ff4 adresi 25 değerini bulunduruyor.
Yani her değişkenin, fonksiyonun veya nesnenin bellekte tutulan bir adresi vardır. İşaretçileri kullanırız çünkü, bu adreslere sahip oluruz ve belleği dinamik olarak yönetebiliriz. Belleği yönetmekle birlikte kendi tanımladığımız kodları kullanarak işaretçiler ile bellekte bir alan yaratabiliriz/silebiliriz/yerini değiştirebiliriz.
Çöp toplayıcı(Java, C# gibi..) olan dillerde bellek yönetimi otomatik olarak yapılıyor. C, C++ gibi dillerde ise programcı bellekte istediği gibi alan tahsis edebilir.
Neden işaretçileri kullanırız?
0- Belleği daha efektif bir şekilde kullanmak. Çünkü bir değişkenin veya nesnenin adresini işaretçi bilir.
1- İşaretçiler diziler için kullanabiliriz. Pointer aritmetiğine daha sonra değineceğim.
2- Bir fonksiyondaki değerin eşsiz olarak aktarılması (Bir fonksiyona ya da metoda parametre olarak işaretçileri geçirebiliriz. Mesela void Yazdir(veri_tipi* arguman))
3- Bağlantılı listeler(Linked List) gibi veri yapılarında daha efektif bir şekilde programlamak(Temel Veri yapılarının bir çoğunda kullanılır).
4- C programlama dilinin sağladığı(malloc, free, realloc) fonksiyonlarını çağırarak bellek üzerinde kendimiz yarattığımız bir hafızayı dinamik olarak yaratabilip/yerleştirebilip/silebiliyoruz.
Dinamik Olarak Hafıza Yönetimi
Kısaca bahsetmek istediğim üç kavramı iyi anlamanız gerekiyor. Yazdığımız programda bu adreslerin hafızada bir şekilde var olduğunu anladık. Şimdi ise bu var olan adreslerin bellekte nerede var olduğundan bahsedeceğiz.
UFAK NOT: Günümüzde modern C++ programcıları artık bellek yönetimini bu şekilde(malloc, free, new operatörü kullanarak vs..) tahsis etmek hammallık. STL kütüphanelerinin getirdiği unique_ptr, shared_ptr ve weak_ptr gibi standart şablon kütüphaneleri tarafından geliştirilen hazır şablonlar var. Bunlara bu yazıda değinmeyeceğim. Onları kullanmamızın sebebinden önce üç kavramı iyice anlamanız gerekiyor.
1 - Stack(Yığın)
Daha çok kısa süreli değerlendirilen bir veri yapısıdır. Stack veri yapısını kullanır. Mesela tanımladığınız local variable, fonksiyon parametreleri ve dinamik olarak yaratılmayan değişkenler, nesneler vs.. stack üzerinden hallolur.
Mesela basit olarak bir Öğrenci sınıfımız olsun.
[code-sh=cpp]#include <iostream>
class Ogrenci
{
public:
// int tipinde bir veri üyesi
int numara;
};
int main()
{
// x stack üzerinden değerlendirilecek ve stack eklenecek
int x;
// Kendi yarattığımız Ogrenci veritipinden olan Ahmet yine stack eklenecek...
Ogrenci Ahmet;
// . (nokta) operatörü kullanarak, nesneye erişip numarasını 42 yapıyoruz.
Ahmet.numara = 42;
std::cout << Ahmet.numara << std::endl;
// Program bu evreye geldiğine Ahmet stack üzerinden otomatik olarak pop edilecek
}
[/code-sh]
Burada yarattığımız nesne(Ahmet) stack üzerinden işlem yapılacak. sürekli pop ve push edilip durur. Aynı şekilde yarattığımız nesnenin(Ahmet nesne oluyor) bellekte olan bir referansı vardır.
2 - Heap(Düzensiz Yığın)
Dinamik olarak yarattığımız hafıza Heap üzerinden değerlendilrilir. Burada yarattığımız işaretçiler(pointerlar) aslında stack üzerinden yarattığımız bir adresin başlangıcını temsil eder. Veyahutta hiçbir adres referans etmeyip "nullptr" değerini alabilir.
[code-sh=cpp]#include <iostream>
class Ogrenci
{
public:
// int tipinde bir veri üyesi
int numara;
};
int main()
{
// x bir işaretçidir ve y'nin adresinin başlangıcını temsil eder.
int y = 8;
int* x = &y;
// Aynı şekilde new opeartörünü kullanarak heap üzerinde yarattık.
Ogrenci* Ahmet = new Ogrenci();
Ahmet->numara = 42;
std::cout << Ahmet->numara << std::endl;
// Ahmet nesnesi Heap üzerinden serbest bırakılıyor
delete Ahmet;
// Ahmet artik bos bir bellek alanini temsil ediyor
Ahmet = nullptr;
}
[/code-sh]
Dinamik olarak yönetilen hafıza programcı tarafından bellekten serbest bırakılmalıdır. Yoksa memory leak oluşur.
Göreceğiniz gibi var olan bir veri tipininin adresinin başlangıcını işaretleyebiliriz veya new opearatörünü kullanarak veya malloc fonksiyonunu kullanarak heap üzerinde dinamik olarak alan yaratabiliriz.
ONERI: STL'nin sundugu unique_ptr, shared_ptr veya weak_ptr gibi sablon siniflarini kullanarak otomatik olarak pointerin bellekten serbest birakilmasi icin var olan sablonlari kullanin.
3 - Static(Durağan): Bütün program üzerinde sadece bir örneği bulunan hafıza alanını temsil ediyor. Yani durağan, statik, bütün program boyunca sadece bir adres lokasyonundan çağrılır. Başka örneği yaratılamaz. Compile-Time(derleme zamanında) adreslenir.
İşaretçileri C++'da kullanmak.
Öncelikle, adresleme ve belleğin mantığını basit olarak anladığımıza göre, işaretçileri nasıl kullanacağımıza bakalım.
* (de-referans operatörü): işaretçi tanımlamak için bu sembolü(operatör) kullanırız.
& (adres operatörü): bir değişkenin, nesnenin veya fonksiyonun adresini almak için işe yarar. yani °isken3 dediğimiz zaman aslında o değişkenin adresini alırız.
[code-sh=cpp]#include <iostream>
using namespace std;
int main()
{
// iki tane tam sayı değişkeni var
int var1 = 11;
int var2 = 22;
cout << &var1 << endl // var1'in adresini yazdırıyoruz.
<< &var2 << endl << endl; // var2'nin adresini yazdırıyoruz.
// tamsayıya bir işaretçi tanımadık
int* ptr;
// ptr adlı işaretçi var1'in adresini işaretliyor
ptr = &var1;
// işaretçinin değerini yaz. işaretçi bir adres tuttuğundan dolayı, var1'in adresini yazacaktır.
cout << ptr << endl;
// ptr adlı işaretçi var2'in adresini işaretliyor
ptr = &var2;
// işaretçinin adresini tekrardan yazdırdık. fakat şu anda işaretçiyi var2'nin adresine işaretledik.
cout << ptr << endl;
system("pause");
}
[/code-sh]
Program Çıktısı (Adres numaraları sizin bilgisayarınız için değişecektir.)
0x084F790 yani var1'in adresi
0x084F784 yani var2'nin adresi
0x084F790 ptr işaretçisi ile var1'in adresi gördüğünüz gibi ptr işaretçisini var 1'in referansı olarak aldık.
0x084F784 ptr işaretçisi ile var2'in adresi
Tekrardan gözden geçirelim.
[code-sh=cpp]// İşaretçi Tanımlanması
int* ptr; // Burada ptr adında bir tamsayıya ait işaretçi oluşturduk.
//Adreslerin işaretlemesi:
ptr = &var1; // burada ptr işaretçisini var1'in adresine işaretledik.
// Tekrardan ptr adresinin işaretlemesi:
ptr = &var2; // burada ptr işaretçisini var2'in adresine işaretledik. ptr'nin aldığı değerde değişti herhalde...
[/code-sh]
Anlayacağınız gibi işaretçi tanımlanan tipte (burada tanımlanan tip int oluyor) adresi işaretler. Adresi tutmakla birlikte bu adresin içerisindeki değeri de tutar.
İşaretçinin değerini almak: (DE-REFERANS ETMEK)
Normal olarak bir işaretçi, referans olarak adresi tutar. Peki biz bu adres yerine işaretçinin tuttuğu değeri almak için ne yapacağız?
İşaretçinin başına * koyarak bu işaretçinin adresi yerine değerini alacağız.
[code-sh=cpp]float x = 42.3; // float veri tipinde bir değişken oluşturduk ve 42.3 sayısını atadık.
float* ptr = &x; // float veri tipinde bir işaretçi oluşturduk ve referans olarak x'in adresini aldık.
// İşaretçilerin değerini ortaya çıkarmak(de-referans etmek)
std::cout << *ptr << std::endl; //Bu sayede adresi yerine şu andaki var olan değerini alırsınız. Eğer *ptr yerine ptr yazarsak o zaman işaretçinin adresine ulaşırız.
// İşaretçinin adresine ulaşmak:
std::cout << ptr << std::endl; //Bu sayede adresi yerine şu andaki var olan değerini alırsınız. Eğer *ptr yerine ptr yazarsak o zaman işaretçinin adresine ulaşırız.
[/code-sh]
Bu örneği yazın ve çalıştırın: Herhalde basit olarak pointerların temelini anlamış olursunuz.
[code-sh=cpp]int main()
{
int var1, var2; // iki tamsayı değişkeni atadık
int* ptr; // tamsayı için ptr adında işaretçi belirledik
ptr = &var1; // ptr işaretçisini değişken 1'in adresine işaretledik.
std::cout << *ptr << std::endl; // Bu satırda ptr adlı işaretçimizin değerini yazdırdık. Eğer işaretçinin adresi yerine değerini bulmak istiyorsanız başına * işaretini yerleştirin.Bu işleme de-referans deniliyor.
*ptr = 37; // var1=37 ile aynı şey demektir. başına konulan yıldız o işaretçinin referansını çıkartır.
var2 = *ptr; //var2=var1 ile aynı şey demektir. *ptr kullanmamızın amacı işaretçiyi değiştirmek.
cout << var2 << endl; // var2 değişkeninin 37 olacağını kontrol edelim.
return 0;
}
[/code-sh]
-> operatörünü kullanmak.
Normal olarak, bir pointerin değerini alırken onun başına * koyarak de-referans ediyoruz. Fakat bu sınıfların, nesnelerini yaratırken için gerçekten güzel bir görüntü sağlamayabilir.
Bunun için pointer olan bir nesneye erişmek için -> operatörünü kullanırız.
Örnek:
[code-sh=cpp]#include <iostream>
#include <string>
class Ogrenci
{
public:
// int tipinde bir veri üyesi
int numara;
// basit bir üye fonksiyonu
void NumaraYazdir(){ std::cout << numara << std::endl; }
};
int main()
{
Ogrenci* yeniOgrenci = new Ogrenci(); // burada Ogrenci veri tipinde yeni bir işaretçi tanımladım ve bu işaretçi yeniOgrenci nesnesini temsil ediyor.
yeniOgrenci->numara = 5; // Bu nesne içerisinde -> operatörünü kullanarak Öğrenci sınıfının içerisindeki numara veri üyesine eriştim ve değerini 5 yaptım.
yeniOgrenci->NumaraYazdir(); // numarayı yazdıralım.
}
[/code-sh]
Aslında -> operatörü var olan bir işaretçiyi dereferans ederek onun değerini ortaya çıkarır.
-> operatörünü kullanmadan önce işaretçiyi deferans edip daha sonra nokta operatörü ile de erişebilirdik. (*yeniOgrenci).NumaraYazdir();
nullptr kavramı:
Bir işaretçi hiçbir adresi işaret etmeyebilir. O zaman boş işaretçi değerini alır. Özellikle bir nesnenin var olup olmadığını bu şekilde kontrol edebiliriz. Veya bir işaretçi sabit olarak tanımlanmadıysa, işaretçinin işaret ettiği adresi boş olarak tanımlayabiliriz. Java dilindeki"null" mantığı ile aynı şekilde çalışır. Fakat bu işaretçiler için geçerlidir.
Örnek:
[code-sh=cpp]Ogrenci* baskaOgrenci = nullptr; // baskaOgrenci işaretçisi artık hiçbir adresi referans etmiyor.
[/code-sh]
Aynı şekilde bir objenin adresinin geçerli veya geçersiz olduğunu anlamak için nullptr ile kontrol edebilriiz.
[code-sh=cpp]if(birIsareci != nullptr)
{
std::cout << "birIsareci adinda bir işaretçi bir adresi işaretliyor. yani referans geçerlidir" << std::endl;
}
[/code-sh]
Parametre olarak işaretçi geçirmek:
[code-sh=cpp]#include <iostream>
using namespace std;
// böyle bir fonksiyon tanımlıyorum, parametre olarak bir işaretçi alıyor, ve bu işaretçinin değerini de-referans edip 2.54 ile çarpıyor daha sonra bu değere atıyor.
void centimize(double* ptrd)
{
*ptrd *= 2.54; // *ptrd işaretçisi ile gelen değeri 2.54 sayısı ile çarpıp santimetreye çevirdik. en solda kullandığımız * işareti ile de-referans edip işaretçinin değerini alıyoruz.
// diğer * ise çarpıp üzerine eklediğimiz için kullandığımız çarpma olayı.
}
int main()
{
double var = 10.0; // var değişkenine 10.0 atadık.
cout << “degisken = “ << var << “ inc ” << endl;
centimize(&var); // değişkenin adresini fonksiyona uyguladık.
cout << “var = “ << var << “ santimetre” << endl;
return 0;
}
//--------------------------------------------------------------
[/code-sh]
Parametre olarak işaretçi geçirmenin c++ programlamada bir çok örneği mevcut, bu yazıda temel mantığı anladıktan sonra Unreal Engine'da kullandığım bir kod parçacağından anlatmak istiyorum.
[code-sh=cpp]// ATriggerVolume sınıfına ait işaretçi nesnesi(object) tanımlıyorum.
ATriggerVolume* PressurePlate;
[/code-sh]
Aşağıdaki örnek biraz karışık gelebilir fazla takmayın. Öncelikle -> "ok" göstergeçin ne olduğunu belirtelim. Bir sınıftan bir pointere ulaşmak istiyorsanız -> kullanmanız lazım. Kullanılan döngü uzak-tabanlı "ranged-based" ve auto anahtar kelimesi değişkenin int, char, float ya da başka bir şey gibi belirtmeden otomatik olarak tanıtılmasını algılayan bir kelime. neyse bunlardan ziyade aşağıda örnekte işaretçileri (*) ve referans adreslerini(&) nasıl kullanıldığına bakın.
[code-sh=cpp]// Diyelim ki içerisinde aktörleri bulunduran bir işaretçi dizimiz olsun.
TArray<AActor*> OverlappingActors;
for (const auto& Actor : OverlappingActors)
{
TotalMass += Actor->FindComponentByClass<UPrimitiveComponent>()->GetMass(); // kütleleri topla
UE_LOG(LogTemp, Warning, TEXT("Total actor on plate : %s"), *Actor->GetName()); // burada ismine ait bir stringi de-referans ettik.
}[/code-sh]
[code-sh=cpp]*Actor->GetName(); // burada, gördüğünüz üzere aktörün ismini pointer ile aldığım için ve metoda erişmek için -> operatörü kullanılmış.
// daha sonradan GetName bir string işaretçi döndürüyor, bunun değerini almak için * operatörünü kullanıldı ve de-referans edildi.[/code-sh]
Bir diğer yaptığımız bir çok şey ise nullptr yani işaretçinin değerini boşaltmak, yani işaretçi var olan Lamb aktörü için hiçbir nesnenin adresini almıyor.
[/code-sh]
*AActor Lamb = nullptr; // gibisinden. eğer aktör yoksa gibisinden düşünebilirsiniz.
[/code-sh]
Ayrıca Unreal Engine API referansına bakarsanız parametrelerin işaretçi aldığını görebileceksiniz
Yukarıda gördüğünüz üzere, TArray kalıbı(template) ait Append metodumuzun iki tane parametresi var. İlk parametre olarak, yaratılan tipin bir işaretçisini alıyor. Bu sayede değeri eşsiz olarak geçirebiliyor.
const ElementType * Ptr,
int32 Count