Menü Kapat

Göstericiler (Pointerlar)

 

Bilindiği üzere her değişken bellekte belli bir alan kaplar. Bu alanların adresleri vardır ve program çalıştırıldığında CPU tanımlanan değişkene ulaşmak için bu adrese gider. Programcının adreslerle uğraşmasına gerek yoktur; çünkü derleyici bunu yapar. Bazı durumlarda ise bu işlere dalmak kaçınılmazdır. Böyle durumlarda işaretçiler kullanılır.

Adres operatörü

Adres operatörünün işi değişkenlerin adreslerini döndürmektir. Sembolü “&”dir.

int x{5};

cout << x << " " << &x << endl;

 

Programın çıktısı: 5 0x1234FF

Program her bilgisayarda ve program her açıldığıda başka değer gösterecektir. Çünkü ramden alınan adresler belirsizdir.

Dereferans operatörü

Dereferans operatörü bize belli bir adresteki değeri döndürür.

#include <iostream>
int main()
{
    int x = 5;
    std::cout << x << '\n'; // x değişkeninin değeri yazdırılır.
    std::cout << &x << '\n'; // x değişkeninin bellekteki adresi yazdırılır.
    std::cout << *&x << '\n'; /// x değişkeinin adresindeki değer yazdırılır.

    return 0;

}

 

Gösterici

Gösterici bir değişkenin bellekteki adresini tutan bir değişken türüdür.

Pointer tanımlama

Gösterici tanımlarken değişkenin başına * operatörü koyulmalıdır.

int *iPtr;
double *dPtr;

int* iPtr2;
int * iPtr3;
int *iPtr4, *iPtr5;

 

Yukarıdaki tanımlama şekillerinin hepsi doğrudur. Ama birinci tanımlama şekli tercih edilir.

int* iPtr6, iPtr7; // iPtr6 bir göstericidir ama iPtr7 bir integer değişkendir.

 

Göstericiye değer atama

Göstericiye atanan değer bir adres olmalıdır. Göstericilerle en çok yapılan şey, göstericiye bir değişkenin adresini atayıp kullanmaktır.

Değişkenin adresini almak için adres operatörünü kullanabilirsiniz.

int value = 5;
int *ptr = &value; // ptr'yi bir değişkenin adresiyle tanımlama

 

Gösterici ismini bir şeyleri gösterdiği için alır. Gösterici bir değişkeni gösteren demektir.

Göstericinin tipi göstereceği değişken ile aynı olmalıdır.

int iValue = 5;
double dValue = 7.0;
int *iPtr = &iValue; // ok
double *dPtr = &dValue; // ok
iPtr = &dValue; // hatali -- int pointer double degiskeni gosteremez
dPtr = &iValue; // hatali -- double pointer int degiskeni gosteremez

 

C++ gostericilere bir sabit deger atamanıza izin vermeyecektir.

int *ptr = 5; //Hatali
int *ptr = 0x01232FA //Hatali

 

Pointerlerin tuttuğu adresin değerini gösterme

Dereferans edilmiş pointer tuttuğu adresin bulunduğu yerdeki veriyi gösterir. Örneğin:

int value = 5;
std::cout << &value; // valuenin adresini yazar
std::cout << value; // valuenin içeriğini yazar
int *ptr = &value;
std::cout << ptr; // ptrnin tuttuğu adresi yazar (valuenin adresi)
std::cout << *ptr; // ptrnin tuttuğu adresin tuttuğu değeri yazar. (valuenin değeri)

 

Program çıktısı: 00123ED3 5 00123ED3 5

  • ptr ile &value eşittir
  • *ptr ile value eşittir

Pointerlerin boyutu bilgisayarınızın mimarisine göre değişkenlik gösterir. 32 bit bilgisayarlarda 4 bayt, 64 bit bilgisayarlarda ise 8 bayt yer kaplayacaktır.

Eğer pointerler bir değer göstermeyecekse nullptr anahtar kelimesine eşitlenmeleri gerekmektedir.

int *ptr(nullptr);

int *ptr2 = nullptr;

 

Diziler bir göstericidir ve bulundurdukları ilk verinin adresini tutarlar. Örneğin:

#include <iostream>
int main()
{
    int array[5] = { 9, 7, 5, 3, 1 };
    std::cout << "The array has address: " << array << '\n';  //dizinin adresi
    std::cout << "Element 0 has address: " << &array[0] << '\n'; // ilk indexin adresi
    return 0;

}

 

Programın çıktısı:

The array has address: 0042AE34
Element 0 has address: 0042AE34

Akıllı Pointerlar

void Fonksiyon()
{
    BirSinif *ptr = new BirSinif;


    delete ptr;
}

 

Yukarıdaki fonksiyonda bir sınıf için pointer tanımlanmış ve bu pointera yer ayırma işlemi yapılmış. İşlem bittiğinde de bellek işletim sistemine geri verilmiş. Yukarıda hiçbir sorun yok gibi gözüküyor. Bellek sıkıntısı sorunu da yok. Sorun yokmuş gibi gözükse de yeri geldiğinde belleğin iade edilmesi unutulabilir. Örneğin:

void Fonksiyon()
{
    BirSinif *ptr = new BirSinif;

    int x;
    cin >> x;

    if(x == 0)
        return;

    delete ptr;
}

 

Burada görüldüğü üzere kullanıcı eğer sıfır girerse ayrılan yer iade edilmeyecek ve bellek sızıntısına sebep olacak.

Bunun için akıllı pointerları kullanabiliriz. Akıllı pointerlar ile delete işlemine de gerek kalmayacaktır. Akıllı pointerlar da dinamik olarak yer ayırma yaparlar. Ama birer nesne oldukları için kapsam bittiği zaman otomatik olarak yıkıcı fonksiyonları çağırılır. Böylece ramden alınan yer otomatik olarak teslim edilmiş olur.

C++11 ile dört farklı akıllı pointerımız var. unique_ptr, shared_ptr, weak_ptr ve auto_ptr. Auto_ptr kullanılmamalı. Zaten C++17 ile birlikte dilden silindi.

unique_ptr

unique_ptr C++ ile birlikte auto_ptr yerine getirilmiş bir sınıftır. Çoklu öğeler tarafından paylaşılmayacak tüm dinamik yer ayrılmış nesneler için unique_ptr kullanılmalıdır. unique_ptr içerisindeki nesne tamamen unique_ptr’aittir. Paylaşılmaz. unique_ptr <memory> başlığında bulunur. Basit bir unique_ptr örneği:

#include <iostream>
#include <memory>

class stuff
{
public:
    stuff() { std::cout << "nesne olusturuldu" << std::endl; }
    ~stuff() { std::cout << "nesne yok edildi" << std::endl; }
}

int main()
{
    std::unique_ptr<stuff> stu ( new stuff );

    return 0;
}

 

Program çıktısı:

nesne olusturuldu
nesne yok edildi

C++14 ile gelen std::make_unique fonksiyonunu kullanarak unique_ptr oluşturabilirsiniz. Kullanması oldukça kolaydır.

class stuff;

int main()
{

    auto ptr = std::make_unique<stuff>();

}

Fonksiyonlardan unique_ptr döndürebilirsiniz.

std::unique_ptr<stuff> createUnique()
{
    return std::make_unique<stuff>();
}

 

Fonksiyonlara unique_ptr gönderebilirsiniz.

#include <memory> // for std::unique_ptr
#include <iostream>

class Kaynak
{
public:
    Kaynak()
    {
        std::cout << "Kaynak alindi\n";

    }

    ~Kaynak()
    {
        std::cout << "Kaynak yok edildi\n";

    }

    friend std::ostream& operator<<(std::ostream& out, const Kaynak &res)	{
        out << "Bu bir kaynaktir\n";

        return out;

    }

};

void KaynakKullan(const std::unique_ptr<Kaynak> &res)
{
    if (res)
        std::cout << *res;

}

int main()
{

    auto ptr = std::make_unique<Kaynak>();

    KaynakKullan(ptr);

    std::cout << "Program bitiyor\n";
    return 0;


} // Kaynak burada yok edilir.

 

Direkt fonksiyonlara gönderebiliriz ve orada kullanabiliriz.

Unique_ptr’leri kullanmamız için (*) ve (->) operatörleri aşırı yüklenmiştir. Bu sayede direkt içeriğe erişebiliriz.

Eğe fonksiyonun, tüm verinin sahipliğini almasını istiyorsanız std::move() ile fonksiyona gönderebilirsiniz.

#include <memory> // for std::unique_ptr
#include <iostream>

class Kaynak
{
public:
    Kaynak()
    {
        std::cout << "Kaynak alindi\n";

    }

    ~Kaynak()
    {
        std::cout << "Kaynak yok edildi\n";

    }

    friend std::ostream& operator<<(std::ostream& out, const Kaynak&  res)	
    {
        out << "Bu bir kaynaktir\n";

        return out;

    }

};

void KaynakKullan(const std::unique_ptr<Kaynak> &res)
{
    if (res)
        std::cout << *res;

} // Kaynak burada yok edilir.

int main()
{

    auto ptr = std::make_unique<Kaynak>();

    KaynakKullan(std::move(ptr));

    std::cout << "Program bitiyor\n";
    return 0;


}

 

unique_ptr’nin yanlış kullanımları

İlk olarak iki farklı unique_ptr’nin aynı pointera sahip olması yanlış bir kullanımdır.

İkinci hatalı kullanım ise unique_ptr’ye gönderilen pointerın sonradan silinmesidir.

stuff *ptr = new stuff;

std::unique_ptr<stuff> u1(ptr);
std::unique_ptr<stuff> u2(ptr);

stuff *ptr2 = new stuff;

std::unique_ptr<stuff> u3(ptr2);

delete ptr2;

shared_ptr

shared_ptr’nin verisi, unique_ptr’nin aksine bir çok shared_ptr tarafından erişilebilirdir.Birden fazla share_ptr aynı veriye sahip olabilir. shared_ptr kaç tane shared_ptr’nin veriye sahip olduğu bilgisini tutar. Sonuncu shared_ptr’de kapsam dışına çıktığında ya da yıkıcısı çağrıldığında bellekten ayrılan yer işletim sistemine iade edilir. shared_ptr de <memory> başlığında bulunur.

#include <iostream>
#include <memory>

class stuff
{
public:
    stuff() { std::cout << "nesne olusturuldu" << std::endl; }
    ~stuff() { std::cout << "nesne yok edildi" << std::endl; }
}

int main()
{
    std::shared_ptr<stuff> stu ( new stuff );

    {
    std::shared_ptr<stuff> stu2 ( stu );

    //Kopyalama islemi yapiliyor. Copy initialization
    std::cout << "bir shared_ptr yok edildi\n"

    } //stu2 kapsam disi ama veri yok edilmedi.

    std::cout << "baska bir shared_ptr yok edildi\n.";
    return 0;
}//stu kapsam disi ve veri yok edildi.

 

Program çıktısı:

nesne olusturuldu
bir shared_ptr yok edildi
baska bir shared_ptr yok edildi
nesne yok edildi

İlk shared_ptr yok edildiğinde tuttuğu veri yok edilmedi çünkü hala bir shared_ptr o veriye sahipti. İkinci shared_ptr yok edildiğinde ise veriyi tutan başka bir shared_ptr kalmadığı için veri yok edildi.

Burada önemli olan birinci shared_ptrden sonraki tüm shared_ptrler direct initialization ile başlatılmalı. Aşağıdaki örnek bunun sebebini açıklıyor:

#include <iostream>
#include <memory>

class stuff
{
public:
    stuff() { std::cout << "nesne olusturuldu" << std::endl; }
    ~stuff() { std::cout << "nesne yok edildi" << std::endl; }
}

int main()
{
    stuff *ptr = new stuff;

    std::shared_ptr<stuff> stu ( ptr );

    {
    std::shared_ptr<stuff> stu2 ( ptr );

    std::cout << "bir shared_ptr yok edildi\n"

    } //stu2 kapsam disi ama veri yok edilmedi.

    std::cout << "baska bir shared_ptr yok edildi\n.";
    return 0;
}//stu kapsam disi ve veri yok edildi.

 

Programın çıktısı:

nesne ousturuldu
birinci shared_ptr yok edildi
nesne yok edildi
iknci shared_ptr yok edildi
nesne yok edildi

Bu tanımlanmamış davranıştır ve büyük ihtimalle program hata alıp kapanacaktır. Bundan kaçınmak için direct initialization kullanın.

Bir yorum yazınız. Yorumlarınız bizim için değerlidir.