Akıllı Göstergeler (smart pointers)

Giriş

Bu yazıyı daha önce bazı forumlara gönderdiğimi düşündüğüm bir mektuptan uyarladım. Arattığımda ne ceviz.net'te ne de cdili'nde bulamadığıma bakılırsa belki artık yaşamayan ocal.net'e göndermişimdir.

Yazıda akıllı göstergeleri tanıtıyor ve bir akıllı gösterge uyarlaması veriyorum.

Daha sonra C++'ın standardında bulunan auto_ptr'ı tanıtıyor ve onun yetersiz kaldığı durumlar için başka kütüphaneler öneriyorum.

Nedenler

Göstergelerin (pointer) öğrenilmeleri kadar, hatasız kullanılmaları da zordur. Hatta bence C programcılarının hemen hemen her gün yaşadıkları bir güçlük doğrudan göstergelerle ilgilidir:

int foo0() { char * p0, *p1;

    p0 = malloc(1);

    if (birSeylerYap(p0))
    {
        free(p0);
        return 1;
    }

    p1 = malloc(1);

    baskaSeylerYap(p0, p1);

    free(p1);
    free(p0);

    return 0;
}

Yukarıdaki 'foo0' işlevinde bellek sızıntısı olmaması, programcının dikkatine kalmıştır. İşlevden çıkılan her noktada bellekten ayrılan bütün alanların geri verilmelerinin unutulmaması gerekir.

Bu kadar küçük bir işlevde sorunun büyüklüğü pek anlaşılmıyor olabilir. 'foo0'ın p2 adında bir göstergenin gösterdiği yeni bir bellek ayırması gerektiğini; hatta işlevden çıkılabilecek yeni bir 'return' deyimi eklendiğini düşünün. Kod gittikçe daha karmaşık ve hatasız yazılması çok daha güç bir hale gelir. Her C programcısı bu tür işlevlerle karşılaşmıştır...

Daha ileri gitmeden önce, haksızlık olmasın diye, 'foo0'ı daha deneyimli bir programcının yazacağı şekliyle vereyim:

int foo0_dahaIyi()
{
    char * p0;

    p0 = malloc(1);

    if (birSeylerYap(p0) == 0)
    {
        char * p1 = malloc(1);

        baskaSeylerYap(p0, p1);

        free(p1);
        return 1;
    }
    
    free(p0);

    return 0;
}

Bu işlev bir öncekinden daha iyidir; çünkü

Biraz konudan ayrılmış olsam da, buraya kadar söylemek istediklerim, C'de kaynak denetiminin programcıya bırakılmış olduğu ve doğru kullanımının programcının dikkatini gerektirdiğidir.

Ayrıca söylediklerimin bellek denetimi ile kısıtlı olmadığına da dikkatinizi çekmek istiyorum. malloc satırlarını fopen'la, free satırlarını da fclose'la değiştirirseniz; sorun, bellek sızıntısı yerine kütük belirteci sızıntısı halini alır.

Bence bir C programcısı için C++'ın en çekici yanı, sınıfların kurucu ve bozucu işlevlerini kullanarak bütün bu kaynak sızıntılarından kurtulunuyor olmasıdır.

Biz insanlar her durumda hata yapmayı becerdiğimiz için, C++'ta da kaynak sızıntıları yaşarız. Ancak bunların çoğu, C++'ı C'den kalan alışkanlıklarımızla kullandığımız içindir. Hatta, eğer C++'ı C gibi kullanırsak, en azından kaynak sızıntıları açısından daha da kötü bir durumdayızdır. Bunun nedeni, C++'ın aykırı durum düzeneğidir (exception handling mechanism).

Aşağıdaki işlev, bir C programı içinde doğru yazılmış bir işlev olarak anılabilecekken, bir C++ programı içinde kullanıldığında hatalıdır:

void foo1()
{
    char * p = malloc(1);
    bar(p);
    free(p);
}

Çünkü C++'ta bir işlevden çıkmanın açıkça görülmeyen yolları da vardır. Örneğin 'bar' işlevinin çağrılması sırasında atılacak bir aykırı durum, bu işlevden free'nin çağrıldığı satır işletilmeden çıkılmasına neden olur. İşte C'de olmayan ama C++'ta her an karşılaşabileceğimiz bir kaynak sızıntısı budur.

Not 1: Söylediğimin aykırı durum atmayacağı bilinen 'bar' işlevleri için geçerli olmadığını biliyorum. Standart kütüphanede böyle işlevler var. Ben 'bar'la, genel bir işlevi kastediyorum. Bir projenin gelişmesi sırasında, 'bar'ın veya onun çağırdığı başka bir işlevin eninde sonunda aykırı durum atıyor olması olasılığı olduğu için, foo1 riskli bir işlevdir.

Vurgulamak istediğim, her işlevi aykırı durum atması olası bir işlev olarak görüp, hiçbir C++ programında foo1'deki gibi bir işlev yazmamamızın gerektiğidir.

Not 2: Bu örnekte malloc yerine new, free yerine de delete kullanmak hiçbir şeyi değiştirmez. 'bar'ın aykırı durum atması ve bizim delete satırına hiçbir zaman erişemeyebilecek olmamız o durumda da geçerlidir.

Tekrar söylüyorum: Burada söylediklerim, yalnızca bellek denetimiyle ilgili değil: başka kaynakları ayıran ve geri veren satırları da hiçbir zaman kodumuz içinde birbirlerinden böyle ayrı olarak yazamayız.

Böyle durumlarda yapmamız gereken, Stroustrup'un "resource allocation is initialization (RAII)" olarak adlandırdığı, ama daha iyi anlaşılır olması için keşke "resource deallocation is destruction" deseymiş diye düşündüren temel bir C++ yöntemini uygulamaktır.

Yukarıdaki İngilizce terimleri az sonra anlatacaklarıma uysunlar diye 'kurma' ve 'bozma' kavramlarını da içerecek şekilde sırasıyla şöyle çevirebilirim: "kaynak ayırmak, aslında kurmaktır" ve "kaynağı geri vermek, aslında bozmaktır."

Daha da açık olmak için bu yöntemi şöyle özetleyebiliriz: "kaynaklar nesnelerin bozucu işlevlerinde geri verilmelidirler."

Bunun nedeni, C++'ta bir kapsamdan (örneğin '{' ve '}' parantezlerinin arasındaki bir kod bloğundan) nasıl çıkılırsa çıkılsın, o kapsamda tanımlanmış olan otomatik değişkenlerin bozucu işlevlerinin mutlaka çağrılacak oluşudur. Yani, bir işlevden bir aykırı durum nedeniyle de çıkılıyor olsa, nesnelerin bozucu işlevleri mutlaka çağrılırlar.

Standart kütüphanede RAII yönteminden yararlanan sayısız sınıf ve sınıf şablonu vardır. Örneğin string, karakterleri tuttuğu bellek alanını bozucu işlevinde geriye verir; ofstream, açtığı kütüğü bozucu işlevinde kapatır; vs. Bu tür 'akıllı sınıflar', kendi başlarının çaresine bakmayı bilirler.

'Akıllı gösterge' deyimi de bir nesneyi göstermenin yanında, kendi temizliğini yapma gibi ek işleri de beceren sınıflar için kullanılır.

Ne yazık ki standart C++ kütüphanesi akıllı göstergelerden yana çok şanssızdır. Standardın içindeki tek akıllı gösterge olan auto_ptr'ın kullanılabilirliği çok kısıtlıdır. Hatta, aşağıda değineceğim kullanım amacının dışında kullanılması da yarardan çok zarar getirir.

Yazının geri kalanına kendimiz bir akıllı gösterge tasarlayarak devam edelim. Bu tasarımı yaparken, bir kaç noktada her duruma uygun bir karar vermenin olanaksız olduğunu görecek ve seçeneklerden birisinde karar kılmak zorunda kalacağız. Böylece auto_ptr'ın neden her işe yatkın olmadığını, ve C++ standartlaştırma komitesinin almak zorunda olduğu kararlar sonucunda son halini aldığını anlayacaksınız.

Amacımız, bir gösterge gibi kullanılabilecek bir sınıf tasarlamak... Bu sınıf, program içinde tıpkı bir gösterge gibi kullanılabilsin; buna ek olarak, sorumlusu olduğu nesneyi de zamanı geldiğinde doğru bir şekilde temizlesin... Ayrıca, işe bu akıllı göstergeyi 'char' türünden bir nesneyi gösterecek şekilde tasarlayarak başlayalım. Daha sonra bu sınıfı kolaylıkla bir sınıf şablonuna dönüştürecek ve böylece daha kullanışlı bir hale getireceğiz.

Akıllı gösterge sınıfımızın adı 'Gosterge' olsun...

Not: Aşağıda 'yalın gösterge' ile, C ve C++'ın herhangi bir sınıf içine alınmadan açıkça kullanılan sıradan göstergelerini kastedeceğim. Örneğin,

   char * p;

gibi bir satırda tanımlanan p, yalın bir göstergedir.

Akıllı göstergemizi tasarlarken, yalın bir göstergenin ne gibi durumlarda kullanıldığına ve onunla ne yapabildiğimize bakalım:

Nesneyi göstermek

Yalın göstergeler nesneleri gösterirler. Bizim de bu işi yapabilmemiz için kendi içimizde bir yalın gösterge tutmamız gerekir:

class Gosterge
{
	char * p_;  // asıl nesneye gösterge

/* ... */
};

İlk değer

Bir yalın gösterge, ilk değeri verilmeden de kullanılabilir:

char * p;
cout << *p; // HATA! p'nin rasgele bir değeri var

Her ne kadar bu bir programcı hatası olsa da, yalın göstergelere benzeyen bir akıllı gösterge tasarlıyoruz diye biz de bu hatalı kullanıma izin mi verelim, yoksa daha güvenli olup, kurulduğumuz sırada mutlaka ilk değerimizin verilmesini mi isteyelim?

Yalın göstergelerin aynısı gibi davranan bir akıllı gösterge, yalın göstergelerden daha akıllı olamayacağını bildiğimiz için, burada güvenli davranışı seçelim ve kurucumuzun mutlaka bir 'char *' almasını gerektirelim. Asıl nesneyi gösterebilmek için kendi içimizde tuttuğumuz göstergeyi de bize verilen göstergeye eşitleyelim:

class Gosterge
{
/* ... */
public:

	explicit Gosterge(char * p)
		:
		p_(p)
	{}

/* ... */
};

Nesneye erişim

Yalın göstergenin gösterdiği nesneye * işleciyle erişebiliriz. Bizim göstergemiz de böyle kullanılabilmelidir.

Hem sabit erişim, hem de değiştirmeye izin veren erişim sağlamak için işlecin her iki türünü de tanımlıyoruz:

class Gosterge
{
/* ... */

    char       & operator* ()       { return *p_; }
    char const & operator* () const { return *p_; }

/* ... */
};

Kopyalanabilirlik

Yalın göstergeler kopyalanabilirler. Bu durumda aynı nesneyi gösteren birden fazla gösterge elde edilir:

char * p0 = new char;
char * p1 = p0;

Bu durumda hem p0 hem de p1 aynı bellek alanını gösterirler. Bizim akıllı göstergemiz kopyalandığında ne yapmalı? Herhalde iki göstergemizin de aynı nesneyi göstermeleri doğaldır...

Peki belleği geri verme sorumluluğu hangisinin olmalı? Acaba ilk kurduğumuz akıllı göstergenin mi? Peki o gösterge yaşam süreci bittiği için belleği geri verdiğinde kopyaları üzülmezler mi? :o) Böyle yaptığımızı düşünürsek, aşağıdaki koddaki foo işlevi dışarıya bir akıllı gösterge (g1) gönderdiğini sanıyor olacak, ama asıl nesnenin bulunduğu bellek, foo'dan çıkılırken g0 tarafından geri verilmiş olacak.

foo'yu çağıran kodun elinde de geri verilmiş bir belleği gösteren bir gösterge kalacak:

Gosterge foo()
{
    Gosterge g0(new char);
    Gosterge g1 = g0;  // aynı belleği gösteriyorlar
    return g1;
} /*
    g0'ın yaşam süreci burada sona erer;
    İşlevden çıkarken belleği geri verir...
  */

Hımmm... Demek ki böyle yapamayız. Acaba bu akıllı gösterge nesnesinden kaç kopya bulunduğunu bir tarafa mı yazsak, ve oluşturulan her bir kopya için sayıyı bir tane arttırsak ve yaşam süreci sona eren her bir nesne için bir tane eksiltsek mi?

Bu yöntemi uygularsak, kopya sayısı sıfıra indiği zaman asıl nesneyi temizleriz. Bu yöntem 'reference counting' olarak adlandırılır. (Eminim Türkçe'de de bir adı vardır ama ben bilmiyorum :o). Ancak bu yöntem, her bir akıllı gösterge yanında bir de bellekten sayıyı tutacak bir yer ayrılmasını gerektirdiği için, masraflı bir yöntemdir.

Görüldüğü gibi, bir akıllı göstergenin tek bir öğe işlevinin bile nasıl davranması gerektiği konusu açık değildir. Yolumuza devam edebilmek için, biz göstergemizin kopyalanmasını tümden yasaklayalım. Görüldüğü gibi, bu da başka bir çözümdür.

Aynı sorunlar atama işleci için de geçerli olduğu için, her iki işleci de yasaklamaya karar verelim. Bunu yapmanın geleneksel yolu, işleçleri tanımlarını vermeden sınıfın özel erişim alanında bildirmektir:

class Gosterge
{
/* ... */
	// Kopyalayıcı:
	Gosterge(Gosterge const &);

	// Atama işleci:
	Gosterge & operator= (Gosterge const &);
/* ... */
};

Yalın gösterge gibi kullanmak

Gosterge türümüzün kullanışlı olması için, onun nesnelerini yalın gösterge bekleyen işlevlere aynı bir yalın gösterge gibi gönderilebilmeliyiz. Bunu yapabilmenin bir yolu, kendimizi 'char *' gereken yerlerde de kullandıracak bir tür dönüşümü işleci tanımlamaktır:

class Gosterge
{
/* ... */
	operator char * ()
	{
		return p_;
	}
/* ... */
};

Bu kararları verdikten sonra, şimdi de bizi akıllı yapacak olan olanaklarımıza geçelim:

Bellek temizliği

Bütün amacımız zaten bozucu işlevimiz çağrıldığında sorumlusu olduğumuz belleği geri verebilmek olduğu için, bozucu işlevimizde gereken temizliği yapalım:

class Gosterge
{
/* ... */
	~Gosterge()
	{
		delete p_;
	}
/* ... */
};

Aslında burada da garip bir durum var: Kurucu işlevimize verilen belleğin nasıl alındığını bilmiyoruz. Ya kullanıcı 'malloc' işlevini veya 'new[]' işlecini kullandıysa? Bu durumlarda sırasıyla 'free' işlevini veya 'delete[]' işlecini çağırmamız gerekirken nasıl olur da hep 'delete'i çağırabiliriz?

Bu durumda yapacak tek bir şey vardır: Bir karar vermek ve kullanıcılara bunu açıkça söylemek: "Gosterge sınıfını ancak 'new' ile aldığınız belleklerle birlikte kullanabilirsiniz" gibi...

Sorumluluğu devretmek

Akıllı göstergemizi bir yalın gösterge gibi başka işlevlere vermemiz (bkz. 5. madde) o kadar da iyi olmayabilir. Ya o işlev belleği kendi sorumluluğuna aldığını düşünen bir işlevse ve belleği geri vermeye kalkarsa? Bunun üstüne bir de bizim 6. maddede 'delete'i çağırmamız yanlış olur.

Kullanıcıya sorumluluğu geri almasını sağlayan bir işlev sunmak yararlı olabilir:

class Gosterge
{
/* ... */
	void birak()
	{
		p_ = 0;
	}
/* ... */
};

Bu kadar lafı yalın bir gösterge gibi kullanılabilecek akıllı bir gösterge tasarlayabilmek için yaptık. Bütün bunların gereksiz olduğunu düşünenlerin yukarıdaki foo1 işlevi için yazdıklarımı hatırlamaları gerekir.

Özellikle bu yaptıklarımızın lüks veya havalı şeyler olduğunun sanılmaması gerektiğini vurgulamak istiyorum. Böyle akıllı göstergeler, C++'ı doğru olarak kullanmak için gerekli sınıflardır.

Tekrar söylüyorum: foo1'de olduğu gibi kod yazıyorsanız, başınız eninde sonunda C++'la belaya girecek demektir.

Gosterge sınıfının tümü ve onu kullanan küçük bir program:

#include <iostream>

using namespace std;

class Gosterge
{
	char * p_;  // asıl nesneye gösterge

	// Kopyalama kurucusu:
	Gosterge(Gosterge const &);

	// Atama işleci:
	Gosterge & operator= (Gosterge const &);

public:

	explicit Gosterge(char * p)
		:
		p_(p)
	{
		cout << "kuruluyor\n";
	}

	~Gosterge()
	{
		cout << "bozuluyor\n";
		delete p_;
	}

	// Gösterdiğimiz nesneye erişilebilsin diye:
	char & operator* ()
	{
		return *p_;
	}

    char const & operator* () const
	{
		return *p_;
	}

	// Yalın gösterge gibi kullanılabilmek için:
	operator char * ()
	{
		return p_;
	}

	// Sorumluluğu devretmek için:
	void birak()
	{
		p_ = 0;
	}
};

// Yalın gösterge bekliyor:
void bar(char * p)
{
	cout << "bar icinde: " << *p << '\n';
}

void foo()
{
	Gosterge g(new char);
	*g = 'a';
	cout << "foo icinde: " << *g << '\n';

	// Kopyalama ve atama yapamayız:
// 	Gosterge g1(g);
// 	Gosterge g2(new char);
// 	g2 = g;

	// Yalın gösterge gibi kullanalım:
	bar(g);
}

int main()
{
	foo();
}

Bu sınıfın başka bir kısıtlaması da, yalnızca 'char' türünden nesneleri gösterebiliyor olmasıdır. Herhangi bir türden nesneyi gösterebilmek için onu bir sınıf şablonuna dönüştürmemiz gerekir.

Bunu yapmak çok kolay:

1) Başına 'template <class T>' ekleyeceğiz

2) İçindeki bütün 'char'ları 'T' ile değiştireceğiz

3) Kullanırken hangi türden nesneleri göstereceğini belirteceğiz

İlk iki adımı kendiniz yaptıktan sonra üçüncü adımın bir örneği olarak şu programı kullanabilirsiniz:

int main()
{
// int türünü gösteren Gosterge:
	Gosterge<int> g(new int);
	*g = 42;
	cout << *g << '\n';
}

Yapı göstergesi olarak kullanmak

Bu durumda akıllı göstergemizi daha da akıllı yapmamız gerekiyor. Yalın yapı veya sınıf göstergeleri kullandığımız zaman, gösterilen nesnenin öğelerine -> işleci ile erişebiliriz. Aynı olanağı Gosterge'nin de sunmasını istiyorsak bir de -> işlecini eklememiz gerekir.

Yine hem sabit erişim hem de değiştirmeye izin veren erişim sağlamak için işlecin iki türünü de tanımlıyoruz:

class Gosterge
{
/* ... */

	T * operator -> ()
	{
		return p_;
	}

	T const * operator -> () const
	{
		return p_;
	}
    
/* ... */
};

Sizin bu eklemeyi yaptığınızı varsayarak bir de onu kullanan bir örnek veriyorum:

struct Yapi
{
	double sayi;
};

int main()
{
	// Yapi türünden nesne gösteren gösterge:
	Gosterge<Yapi> g(new Yapi);
	g->sayi = 1.2;
	cout << g->sayi << '\n';
}

std::auto_ptr

Gelelim auto_ptr'a...Yukarıdaki tasarımdan da görüldüğü gibi, akıllı bir göstergenin ne durumda ne yapması gerektiği her zaman için açık değildir. Standardın içinde yer alan auto_ptr, standartlaştırma komitesinin elinde değişik aşamalardan geçtikten sonra, sonuçta 'kaynağın sorumluluğunu devretme işine yarayan' bir gösterge halini almıştır.

Bellekteki nesneyi gösteren birden fazla auto_ptr olamaz.

auto_ptr, kopyalandığı veya atandığı zaman, belleğin sorumluluğunu devrettikten sonra, kendisini de geçersiz hale getirir. auto_ptr'ın bu davranışını anlatmakta kullanılan geleneksel bir benzetme vardır: bir fotokopi makinası düşünün; bu makina, üstüne koyduğunuz sayfanın kopyasını veriyor olsun, ama sayfanın aslını da yok etsin... İşte auto_ptr böyle, kopyalandığında veya değeri başka bir auto_ptr'a atandığında kendisini geçersiz hale getiren bir akıllı göstergedir.

Not: Microsoft MSVC++'ın en azından bazı sürümleriyle (6.0 ve öncesi) gelen auto_ptr, 'kendisini geçersiz kılma' kuralına uymaz. Hem kaynak auto_ptr, hem de hedef auto_ptr kullanılabilir durumda kalır. Sizin kütüphanenizde gelen auto_ptr da böyle standart dışı davranıyor olabilir.

auto_ptr'ların tanımı verilmemiş sınıflarla kullanılması da sorunludur. Bununla ilgili bilgi için http://ceviz.net/index.php?case=article&id=152 adresindeki yazıyı okuyabilirsiniz.

auto_ptr'ın kullanılmasının özellikle yanlış olduğu başka bir durum, C++ standardı içinde gelen ve nesneleri bir araya getirmekte kullanılan vector, set, map, vs. gibi çok yararlı sınıf şablonlarıdır. Bu şablonlar kendi işlerini yaparken nesneleri kopyaladıkları ve/veya birbirlerine atadıkları için, içlerinde tuttukları auto_ptr'ları da farkında olmadan geçersiz hale getirirler. Bu yüzden, auto_ptr'ların bu sınıf şablonlarıyla kullanılmaları olanaksızdır.

Bütün bu yazılanlara bakarak auto_ptr'ın tamamen kullanışsız olduğunu düşünmeyin; ne iş için tasarlandıysa onu yapar. auto_ptr'ın tek sorunu, standardın içinde onun yapamadığı işleri yapacak başka akıllı göstergelerin bulunmamasıdır. Böyle akıllı göstergelerin eksikliği; bazı programcıların, uygun olmadığı durumlarda bile auto_ptr'ı kullanmalarına yol açabilmektedir.

auto_ptr'ın kullanılmasının uygun olduğu bir örnek verdikten sonra, onun uygun olmadığı durumlarda ne yapmak gerektiğini anlatacağım.

İşte auto_ptr kullanan, ama sanırım biraz fazla uzun kaçarak amacının dışına taşmış olan bir örnek: :o)

#include <iostream>
#include <memory>
#include <sstream>
#include <exception>

using namespace std;

class Hayvan
{
public:

	/*
      Sanal işlevleri olan her sınıf gibi bunun
      da bozucusunu sanal yapmamız gerekiyor:
    */
	virtual ~Hayvan() {}
	
	/*
      Her hayvanın ses çıkartmasını istiyoruz ama
      bunun nasıl yapılacağını alt sınıfların
      tanımlamalarını şart koşmak için bu işlevin
      bildirimini '= 0' ile sonlandırıyoruz.
    */
	virtual void sesCikart(ostream & os) = 0;
};

class Kedi : public Hayvan
{
public:
	Kedi()
	{
		cout << "Kedi kuruluyor\n";
	}

	virtual ~Kedi()
	{
		cout << "Kedi bozuluyor\n";
	}
	
	virtual void sesCikart(ostream & os)
	{
		os << "miyav";
	}
};

class Kopek : public Hayvan
{
public:

	Kopek()
	{
		cout << "Kopek kuruluyor\n";
	}

	virtual ~Kopek()
	{
		cout << "Kopek bozuluyor\n";
	}

	virtual void sesCikart(ostream & os)
	{
		os << "hav";
	}
};

// Bilmediğimiz bir durumda bu hatayı vereceğiz
class TaninmayanHayvan : public exception
{
	string neden_;
	
public:
	
	TaninmayanHayvan(string const & neden)
		:
		neden_(neden)
	{}

	~TaninmayanHayvan() throw()
	{}

	char const * what() const throw()
	{
		return neden_.c_str();
	}
};


/*
  Bu işlev, bellekte oluşturduğu nesnenin
  sorumluluğunu kendisini çağırana geçirdiğini
  bir auto_ptr döndürerek belirtiyor.
*/
auto_ptr<Hayvan> hayvanOlustur(string const & tur)
{
	if (tur == "kedi")
	{
		return auto_ptr<Hayvan>(new Kedi);
	}
	else if (tur == "kopek")
	{
		return auto_ptr<Hayvan>(new Kopek);
	}
	else
	{
		ostringstream akim;
		akim << tur << " turunde bir hayvan tanimiyorum";
		throw TaninmayanHayvan(akim.str());
	}
}

void calistir(string const & tur)
{
	/*
      Bu noktada belleğin sorumluluğunu aldığımızı
      bir auto_ptr kullanarak biliyoruz.
    */
	auto_ptr<Hayvan> hayvan = hayvanOlustur(tur);

	// Onu bir gösterge gibi kullanabiliyoruz:
	hayvan->sesCikart(cout);
	cout << '\n';

	/*
      Burada belleği açıkça geri vermemize gerek
      yok. O işi auto_ptr'ın bozucu işlevi yapacak
    */
}

int main(int parametreSayisi, char * parametreler[])
{
	int donusDegeri = 0;
	
	if (parametreSayisi != 2)
	{
		cerr << "Kullanim:\n"
			 << parametreler[0] << " {kedi|kopek}\n";
		donusDegeri = 1;
	}
	else
	{
		try
		{
			calistir(parametreler[1]);
		}
		catch(exception const & hata)
		{
			cerr << "HATA: " << hata.what() << '\n';
			donusDegeri = 2;
		}
	}

	return donusDegeri;
}

Sıra geldi standart kütüphanenin sağlamadığı akıllı gösterge kaynaklarına:

boost akıllı göstergeleri

boost.org'daki boost kütüphanelerinin bir sonraki C++ standart kütüphanesinde yer alacaklarına kesin gözüyle de bakılan beş tane akıllı göstergesi vardır:

scoped_ptr: Yalnızca bir kapsam alanı içinde kullanılabilen ve kopyalanamayan gösterge. Tanımladığımız Gosterge sınıfına çok benzer ve kaynak sızıntılarını önlemekte çok yararlıdır.

scoped_array: scoped_ptr'ın bir dizi nesne ile kullanılan türüdür. Ondan tek farkı, bozucu işlevinde 'delete' yerine 'delete[]' çağırmasıdır.

shared_ptr: 'Reference counted' bir akıllı gösterge. Bu göstergelerin istediğiniz kadar kopyasını alabilirsiniz. Kopyaların hepsi tek bir nesneyi gösterirler.

Asıl nesne, son kopya ortadan kalktığında otomatik olarak temizlenir.

shared_array: shared_ptr'ın bir dizi nesne ile çalışan türüdür. Tek farkı, temizlik sırasında 'delete' yerine 'delete[]'i çağırmasıdır.

weak_ptr shared_ptr veya shared_array göstergelerinin bir gözlemcisidir. Asıl nesne ortadan kalktığında bunlar otomatik olarak sıfırlanırlar.

Hem bu göstergelerle ilgili ayrıntılı bilgi için hem de başka yararlı kütüphaneler için boost.org sitesine mutlaka bakmalısınız.

Loki akıllı göstergeleri

Loki, Andrei Alexandrescu'nun 'Modern C++ Design' adlı kitabında tanıttığı ve nette serbestçe bulunabilen bir kütüphanedir.

Alexandrescu, C++'ın şablon olanaklarını Stroustrup'un bile baştan tahmin edemediği derecede ustaca kullanan ve onlarla inanılmaz sonuçlar alan bir sihirbazdır. :o) Fazla ileri düzey şablon teknikleri kullandığı için çoğu derleyici bu kütüphaneyi derleyemez bile.

Bunların dışında, nette "smart pointer" diye aratınca başka akıllı gösterge örnekleri bulacağınızdan eminim.

Şimdilik bu kadar... boost'un akıllı göstergeleri ile ilgili örnekleri başka bir yazıya saklıyorum.

Sonuç

RAII yöntemini benimsemeden yazılan C++ kodu hatalara, özellikle de bellek sızıntılarına açıktır.

Bellek sızıntılarını önlemenin en kolay yolu akıllı göstergeler kullanmaktır.

Standarttaki tek akıllı gösterge olan auto_ptr'ın kullanım alanı çok dar olduğu için, en iyisi boost.org'un akıllı göstergelerinden yararlanmaktır.


Ali Çehreli - acehreli@yahoo.com