Gösterici (pointer) hatalarının çok büyük bir kısmı sistemden tahsis edilen dinamik alanın geri iadesi kısmında ortaya çıkar. Ya geri iade işlemi unutulur ya da fazladan geri iade işlemi yapılır, ilki de çok fenadır ama ikincisi çok daha fenadır. Geri iade işemi unutulan bir örneğe bakalım.
#include <iostream>
int main()
{
int *pi;
pi = new int;
*pi = 3;
std::cout << "pi : " << *pi << std::endl;
return 0;
}
Yukarıdaki örnekte bir adet int kadar alan ( sizeof(int) ) sistemden tahsis ediliyor. Daha sonra içerisine bir tam sayı atanıyor ve standart çıktıya basılıyor. Fakat bir hata var, tahsis edilen alan delete global operatörü kullanılarak geri bırakılmamış, unutulmuş. Doğrusu şöyle olmalıydı.
#include <iostream>
int main()
{
int *pi;
pi = new int;
*pi = 3;
std::cout << "pi : " << *pi << std::endl;
delete pi;
return 0;
}
Aslında burada pek bir sorun yok gibi. Madem
new ile bir yer tahsis ettik, o zaman
delete ile geri bırakırız ve problem kalmaz. Bu doğru bir söz fakat tasarımın durumuna göre kodun belirli bir bölgesinde tahsis edilen alanın nerede ve ne zaman geri bırakılacağı kodlama zamanını uzatan bir durum. Yanlış noktada geri bırakılan bir bellek bölgesinin tahrifatı büyük olabilir.
Öyle ki bazen hiç bir şey olmaz, bir süre kullanırsınız, daha sonra patlamaya başlar. Kodun ilgili bölümüne artık konsantre olmadığınızdan dolayı böceği yakalamak çok vakit alabilir.
İşte bu durumu ortadan kaldıran akıllı göstericiler uzun zamandır hayatımıza girmiş bulunuyor. Akıllı gösterici kendisine verdiğiniz adresin tuttuğu alanı otomatikman sisteme geri iade eder. Sizin ayrıca
delete ile sisteme geri iade işlemini yapmanıza gerek yoktur. Basit olarak başlangıç fonksiyonunda (constructor) veya atama operatör (assign operator) fonksiyonunda kopyalanan adresin bitiş fonksiyonunda (destructor) otomatikman
delete işlemine sokulmasıdır.
İki çeşit akıllı gösterici tipinden bahsetmek istiyorum.
- Kopyalanamayan akıllı göstericiler
- Kopyalanabilen akıllı göstericiler
Kopyalanamayan akıllı göstericiler kopyalanabilenlerden daha hızlıdırlar. Bununla beraber kullanımları biraz daha sancılıdır. Basit bir örnekle başlayalım. Standard template library (stl) içindeki auto_ptr sınıfını kullanacağız.
#include <iostream>
#include <memory>
int main()
{
std::auto_ptr<int> p1(new int);
*p1.get() = 3;
std::cout << "p1.get() : " << *p1.get() << std::endl;
return 0;
}
Yukarıdaki örnekte herhangi bir delete işlemi yapmadık.
auto_ptr sınıfı otomatikman kendi bitiş (destructor) fonksiyonunda
delete işlemini gerçekleştirdi. Eminiz ki tahsis edilen alan sisteme geri bırakıldı. (Buradaki emin olma duygusu gerçekten çok önemli!)
Bu sınıf içerisindeki adresin başka bir
auto_ptr sınıfına kopyalanamamasının nedeni tahmin edebileceğiniz gibi iki kere delete işlemine otomatikman girmesini engellemek. Eğer kopyalanabilseydi
{
copyable_auto_ptr<int> p1(new int);
copyable_auto_ptr<int> p2(p1)
*p1.get() = 3;
}
blok sonunda hem p1 için hem de p2 için delete işlemi aynı adres üzerinden tahsis edilmiş alanı boşaltmaya çalışacaktı ve böylece patlama gerçekleşecekti.
Peki ben aynı adresi bir yerde değil de birden fazla yerde saklamak ve kullanmak istiyorsam ne olacak?
auto_ptr çözümü güzel ama her zaman efektif değil.
O zaman şu
auto_ptr içerisinde bir sayaç olsa da benim için kaç kopya olduğu bilgisini tutsa, en son kalan kopyanın bitiş (destructor) fonksiyonunda otomatik olarak
delete operatör fonksiyonunu çağırsa ne güzel olurdu değil mi?
Kulağa çok hoş gelen bu özelliği boost kütüphanesinin
shared_ptr sınıfı destekliyor.
Seni seviyorum boost - I love you boost - Я люблю тебя boost
Bu kadar tezahürat yeter sanırım.
Kopyalanabilen akıllı göstericiler kopyalanamayanlara göre daha yavaş olsalar da aynı anda birden çok kopya saklama avantajını ellerinde bulundururlar.
boost kütüphanesinin
shared_ptr sınıfı kendi içerisinde bir sayaç tutar. Her bir kopyalama işleminde bu sayacı bir arttırır ve her bir bitiş (destructor) fonksiyonu çağırıldığında bu sayacı bir azaltır. Sayaç sıfır olduğunda nesnenin artık sisteme geri verimesi gerektiği anlaşılır ve
delete işlemi gerçekleşir. Dolayısı ile her bir
shared_ptr sınıfının bitiş (destructor) fonksiyonunda
delete işlemi yapılmaz, sadece son nesne için yapılır.
#include <iostream>
#include <boost/shared_ptr.hpp>
int main()
{
boost::shared_ptr<int> p1(new int);
*p1 = 3;
boost::shared_ptr<int> p2(p1);
std::cout << "p1 : " << *p1 << std::endl;
std::cout << "p2 : " << *p2 << std::endl;
return 0;
}
Yukarıdaki örnekte
p1 ve
p2 akıllı göstericileri aynı nesnenin adresini gösteriyorlar. İkisi de kullanılabilir. Geri bırakma esnasında
p2 akıllı göstericisinin bitiş (destructor) fonksiyonu daha önce çağırılacaktır (daha sonra yaratıldığı için) ve içerisindeki sayacı bir eksiltecektir.
p1 nesnesinin bitiş fonksiyonu ise önce sayacı bir eksiltecektir sonra sayacı kontrol edecektir. Sayacın değerinin sıfır olduğunu görünce
delete işlemini gerçekleştirecektir.
Java'da bulunan Garbage Collector sistemi gibi güvenilir ama ne zaman silineceği belli olan (Java'da Garbage Collector'ün ne zaman
delete işlemini yapacağını bilmiyoruz) harika bir sistem.
Hata Nesneleri ile Akıllı Göstericiler
Akıllı göstericilerin bir güzel özelliği de hata nesnesi oluşumunda ortaya çıkar. Bir hata oluştuğunda fonksiyon bitmeden yukarı doğru
throw işlemi gerçekleşir.
detele işleminiz fonksiyonun sonunda ise ilgili kod kısmını try - catch bloğuna almanız, throw'u yakalamanız ve dinamik bölgeyi
delete işlemine soktukran sonra mevcut
throw işemini yukarı göndermeniz gerekir. Bunu yapmazsanız aşağıdaki gibi bir durum oluşur.
#include <iostream>
void a_func_send_throw()
{
throw 20;
}
int main()
{
int *pi;
pi = new int;
*pi = 3;
a_func_send_throw();
std::cout << "pi : " << *pi << std::endl;
delete pi;
return 0;
}
a_func_send_throw fonksiyonu throw işlemini gerçekleştiriyor ve
delete işlemi gerçekleşmiyor. Kodun akış yönü a_func_send_throw fonksiyonun gönderdiği
throw sayesinde değişiyor. Şöyle yapabilirdik.
#include <iostream>
void a_func_send_throw()
{
throw 20;
}
int main()
{
int *pi;
pi = new int;
*pi = 3;
try {
a_func_send_throw();
} catch (int e) {
delete pi;
return 0;
}
std::cout << "pi : " << *pi << std::endl;
delete pi;
return 0;
}
Görüldüğü gibi bu çok sancılı bir yöntem. Her türlü kontrol için bir sürü işlem yapmanız gerekiyor.
C++'ın bir özelliği
throw anında mevcuttaki nesnelerin bitiş (destructor) fonksiyonlarının çağırılmasıdır. Böylece throw işlemi yukarı doğru tırmanmadan nesnenizin bitiş fonksiyonu çağırılarak delete işlemi otomatik olarak gerçekleştirilir.
#include <iostream>
#include <boost/shared_ptr.hpp>
void a_func_send_throw()
{
throw 20;
}
int main()
{
boost::shared_ptr<int> pi(new int);
*pi = 3;
a_func_send_throw();
std::cout << "pi : " << *pi << std::endl;
return 0;
}
Artik dinamik olarak tahsis edilmiş alanımızın throw anında dahi sisteme iade edildiğinden eminiz.
boost kütüphanesinin
shared_array sınıfı aynı
shared_ptr gibidir fakat tek farkı
new operatör fonksiyonu yerine
new [] operatör fonksiyonunu kullanır ve bitiş (destructor) fonksiyonu içerisinde
delete operatör fonksiyonu yerine
delete [] operatör fonksiyonu çağırılır.
boost kütüphanesinin
scoped_ptr sınıfı da vardır. Bu sınıf aynı stl kütüphanesinin
auto_ptr sınıfı gibidir. Yani içerisinde sayaç yoktur. Eğer
new yerine
new [] kullanmak isterseniz
scoped_array size yardımcı olacaktır.
Bu güzel göstericileri bol bol kullanacağınız mutlu kodlamalı günler dilerim :)
Volkan Özyılmaz