Konuyu Oyla:
  • Derecelendirme: 0/5 - 0 oy
  • 1
  • 2
  • 3
  • 4
  • 5
İşaretçiler(Pointer) Nedir? Nasıl kullanılır?
#1
Information 
Bu yazı, C++ dilinin işaretleyicilerini anlatmak ve UE üzerinde programlama yapmak için anlaşılması kolay bazı örnekler verilmiştir. JavaC# 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. Sonra daha da derine inelim.  Cool Daha sonra da Unreal Engine'da bir kaç örnek gösterelim.


İş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 fonksiyonun bellekte ayrılmış özel bir adresi vardır. İşaretçiler ise bu adresleri işaretler. Öncelikle adresleri anlayalım daha sonradan işaretçilere geçeriz...

Adresleri anlamak için olarak şöyle bir programımız olsun:

#include <iostream>

int main()
{
   int var1 = 11;
   int var2 = 22;
   int var3 = 33;
   return 0;
}

Bu program çalıştırıldığında, belleğinizin bir kısmında bu şekil gibi bir olay meydana gelir.

506bd6e3403144c7bdebc078d54c312c.png
 
Yukarıdaki resimde gördüğünüz ff0, fff2, fff4 adreslerdir. Adresler bilgisayarınızın mimarisine göre değişir.
fff0 adresi 33 değerini bulunduruyor yani bu da program içeriside olan var3'e denk geliyor. Aynı şekilde ff2 adresi 22 değerini bulunduruyor ve bunun değişkeni var2... 

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 yönetebiliriz. 
 
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-Dizi Ögelerine Erişme
2-Bir fonksiyondaki değerin eşsiz olarak aktarılması (Bir fonksiyona ya da metoda parametre olarak işaretçileri geçirebiliriz.) 
3-Dizileri ve Stringlerin fonksiyonlarını geçirmek (dizi için yöntem(Metod) veya fonksiyon yazacaksanız, işaretçi tanımlamanız gereklidir.)
4-Bağlantılı listelerde efektif bir şekilde veri yapıları oluşturmak (Temel Veri yapılarının bir çoğunda kullanılır)

İş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. 

* (asteriks operatörü):  işaretçi tanımlamak için bu sembolü(operatör) kullanırız.
& (ampersant operatörü): bir değişkenin, nesnenin veya fonksiyonun adresini almak için işe yarar. yani &degisken3 dediğimiz zaman aslında o değişkenin adresini alırız.


#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");



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.

// İş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...

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.

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.


Bu örneği yazın ve çalıştırın: Herhalde basit olarak pointerların temelini anlamış olursunuz.

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;
}

-> 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:
#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.
}

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:
Ogrenci* baskaOgrenci = nullptr; // baskaOgrenci işaretçisi artık hiçbir adresi referans etmiyor.

Aynı şekilde bir objenin adresinin geçerli veya geçersiz olduğunu anlamak için nullptr ile kontrol edebilriiz.
if(birIsareci != nullptr)
{
   std::cout << "birIsareci adinda bir işaretçi bir adresi işaretliyor. yani referans geçerlidir" << std::endl;
}

Parametre olarak işaretçi geçirmek:


#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;
}
//--------------------------------------------------------------

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.

// ATriggerVolume sınıfına ait işaretçi nesnesi(object) tanımlıyorum.
ATriggerVolume* PressurePlate;

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.


// 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.
}

*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.

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
3071cc9d703e4478a7aac00d6798ca23.png
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
Cevapla
#2
Güzel bir anlatım olmuş tebrikler. Ben de bazı noktalara değinmek istiyorum.

Pointer kavramını şöyle düşünmek gerekir. Bir pointer bir nesne yada değerin referansıdır. Bellek adresini işaret eder ama bellek adresi değildir. Pointerın değeri bellek adresidir.

Daha önce bahsetmiştim. Değişken veya sabitlerin türü (int, float, string, object, actor,...) ne olursa olsun her biri için 2 tip* vardır. Bunlar;
Referans(Reference): Özel olarak belirli bir nesneyi yada değişkeni işaret ederler. Bu o bilgisayar sisteminde benzersizdir.
Değer(Value): Bir nesnenin değerini belirtir.

(*) Burada tip sıfatını ben koydum, literatürde ne kullanılabilir belirsizdir.

Bu ne demek? Buradaki senaryoya göre referans ve değere şöyle bir örnek verelim. Örneğin minecraft oyununda yan yana duran 2 adet toprak bloğunu göz ününde bulunduralım. Bu bloklar görünüş, doku, kırılganlık...vb gibi tüm özellikler açısından birbirinin tıpatıp aynısıdır ve sayısal olarak aynı değerler ile tanımlanırlar ama gündelik hayattaki gibi de biliyoruz ki mesela bir tanesi soldaki blok bir tanesi de sağdaki bloktur. İşte bu ayrım bilgisayarda bellekteki konumu ile yani dolayısı ile referans vererek yapılabilir.

Pratikte ise şöyle bir fark oluşabilir(madem gelenek devam ediyor ben de aslında C++ olan PHP kodu ile göstereyim  Smile ).

Bu fonksiyon kendisine değer olarak verilen bir değişkeni 1 arttırır. Değiştirmeden önce ve sonra bu değişkenin değerini ekrana yazar.
PHP Kod:
/**
 * Increase the value of v passed by value.
 * @param v Value to increase value.
 */
void pass_by_value_function(int v)
{
 
cout << "pass_by_value_function:" << << endl;
 
v++;
 
cout << "pass_by_value_function:" << << endl;


Bu fonksiyon ise kendisine referans olarak verilen bir değişkeni 1 arttırır. Değiştirmeden önce ve sonra bu değişkenin değerini ekrana yazar.
PHP Kod:
/**
 * Increase the value of v passed by reference.
 * @param v Reference to increase value.
 */
void pass_by_reference_function(intv)
{
 
cout << "pass_by_reference_function:" << (*v) << endl;
 (*
v)++;
 
cout << "pass_by_reference_function:" << (*v) << endl;


Son olarak da bu fonksiyonların kullanışına bakalım.
PHP Kod:
int main()
{
 
// Create a variable 'x' and assign value 5 to it.
 
int x 5;

 
// Print value of value 'x' before pass to a function.
 
cout << "Value before pass_by_value function:" << << endl;
 
// Pass by value
 
pass_by_value_function(x);
 
// Print value of value 'x' after pass to a function.
 
cout << "Value after pass_by_value function:" << << endl << endl;

 
// Print value of 'x' before pass to a function.
 
cout << "Value before pass_by_reference function:" << << endl;
 
// Pass by reference
 
pass_by_reference_function(&x);
 
// Print value of value 'x' after pass to a function.
 
cout << "Value after pass_by_reference function:" << << endl;

 return 
0;


Konsol çıktısı:
Kod:
Value before pass_by_value function:5
pass_by_value_function:5
pass_by_value_function:6
Value after pass_by_value function:5

Value before pass_by_reference function:5
pass_by_reference_function:5
pass_by_reference_function:6
Value after pass_by_reference function:6

Burada heap/stack gibi teknik konulara girmeden belirtmek gerekirse dikkat edilmesi gereken nokta şudur. Değer olarak ifade edilen bir değişken, örneğin bir fonksiyona parametre olarak verildiğinde onun değeri fonksiyon içerisinde değiştirilse dahi fonksiyon gövdesi tamamlandığında değerinin değişmemiş olduğu görülecektir. Ancak referans olarak verildiğinde, spesifik bir nesneyi belirttiğinden değişiklik kalıcı olacaktır.

Java ve C# gibi programlama dillerinde struct yapıları değer, class yapıları ise referans olarak saklanırken; C++ dilinde böyle bir ayrım yoktur, herhangi bir yapının değer yada referans olması pointer ve address operator ile kontrol edilebilir.

Ancak UBT(Unreal Build Tool), USTRUCT ve UCLASS makroları kullanılarak oluşturulmuş olan struct ve class tiplerini tıpkı C# ve Javadaki gibi sırasıyla değer ve referans olarak saklar(generate eder). Tüm USTRUCTlar değer, tüm UCLASSlar ise reference olarak kullanılırlar.


Ek Notlar

(12-01-2017, Saat: 07:53)Khubur Adlı Kullanıcıdan Alıntı:  İşaretçi(Pointer) Nedir?: Her bir bitin(byte), bilgisayarın hafızasında bir adresi vardır.
Burayı pek anlayamadım, belki de böyle demek istemediniz ama netleştirelim. Bitlerin adresi yoktur, bellekte tek başına 1 bit oluşturamaz/saklayamazsınız; byteların adresi vardır. Bellekte tek başına saklanabilen anlamlı en küçük yapı taşı bytedır.

(12-01-2017, Saat: 07:53)Khubur Adlı Kullanıcıdan Alıntı:  Diyelim ki şöyle bir programımız olsun(PHP Code dediğine bakmayın o C++ Kodu Smile):
PHP Kod:
int main(){
 
int var1 11;
 
int var2 22;
 
int var3 33;
 return 
0;


 506bd6e3403144c7bdebc078d54c312c.png
Yukarıdaki resimde gördüğünüz ff0, fff2, fff4 adreslerdir. fff0 adresi 33 değerini işaretliyor yani bu da var3'e denk geliyor. Aynı şekilde ff2 adresi 22 değerini işaretliyor ve bunun değişkeni var2... Yani değişkenler aslında adreslerin değerini tutar.  Bellekteki her adresin bir değeri vardır.
int türü 4byte(32bit) olduğundan en azından ..ff0, ..ff4, ...ff8, ...ffc şeklinde devam etmeliydi, yani en azından 2bytetan daha büyük aralıklara sahip olmalıydı ama belki de örnek 16bit(2byte) int türünden kalma.

Son olarak C/C++ kodu yazarken tanımlanan her değişkenin null olarak bile olsa mutlaka ilklenmesini(ilk değerini atama) tavsiye ederim. Yoksa derleyiciye göre bazen tahmin edemeyeceğiniz değerler alabilir yada ilklenmemiş değişkenin kullanımı şeklinde bir çalışma sırasında(runtime error) hata ile karşılaşabilirsiniz.

Yanlış:
PHP Kod:
int main()
{
 
int v;
 
intptr;

 
v++; // Error: The variable 'v' is being used without being initialized.
 
if (ptr == nullptr// Error: The variable 'ptr' is being used without being initialized.
 
{

 }


Doğru:
PHP Kod:
int main()
{
 
int v 0;
 
intptr nullptr;

 
v++;
 if (
ptr == nullptr)
 {

 }

Cevapla
#3
Eklediğiniz ekstra bilgiler için teşekkürler @cahitburak Minecraft blok örneği çok iyi şekilde anlaşılır olmuş.
Cevapla
#4
Ali Burak'a ve Cahit Burak'a teşekkürler.
Ara
Cevapla
#5
(13-01-2017, Saat: 16:52)nurisural Adlı Kullanıcıdan Alıntı:  Ali Burak'a ve Cahit Burak'a teşekkürler.

Kardeş değiliz, benim soyadım Kubur ))
Cevapla
#6
Bazı düzenlemeler yapıldı.
Cevapla
 


Konu ile Alakalı Benzer Konular
Konular Yazar Yorumlar Okunma Son Yorum
  [EĞİTİM] C++ Unreal Smart Pointer Libary (USPL) Kullanımı Khubur 0 508 24-03-2017, Saat: 19:24
Son Yorum: Khubur
  Static Library dosyası unreal engine nasıl import edilir ? Faruk 5 607 31-07-2016, Saat: 17:55
Son Yorum: Faruk
  UE4 için C++ nasıl öğrenilir? QuickPlay 3 1,025 12-04-2016, Saat: 20:35
Son Yorum: merbekta
  eski sürümü nasıl geri getirebilirim cetcet21 0 386 05-03-2016, Saat: 22:35
Son Yorum: cetcet21
  UObject Nasıl Replicate Edilebilir ? cahitburak 3 567 01-03-2016, Saat: 22:55
Son Yorum: cahitburak

Hızlı Menü:


Unreal Engine Türkiye

This forum is only for fans and support. It has nothing to do with Epic Games.

Bu site sadece fan ve destek amaçlıdır. Epic Games ile bir ilgisi yoktur.