Bu sayfadaki yazılar
Sayfanın metni
Sayfa 36
İşaretçiler(pointer), C++ dünyasına aslında C'den gelen bir kavram. C'ye de assembler'dan gelmişti. C++ programlarında pek çok hata bir kenarından işaretçiler ile ilgili oluyor. İşaretçilerle yapılan aritmetik işlemler, işaretçilerin sıfırlanmasının unutulması vs.
Yine C++ programlarında en sık yapılan hatalardan biri de bellek yönetimi hataları. Sistemden alınan ve iş bittikten sonra geri verilmeyen bellek, veya geri verilmiş belleğe bakmaya devam eden işaretçileri kullanmak vs.
İşte bu iki genel hata grubunun kesişiminde bulunan önemli bir bölümünü çözmek için akıllı işaretçiler (smart pointer) geliştirilmiş.
Pek çok farklı, akıllı işaretçi kütüphanesi geliştirilmiş olsa da, bellek yönetimi için şu an standart kullanmanız gereken akıllı işaretçiler STL ve boost içinde yer alanlar (ki boost içinde yer alanlar önümüzdeki yıllarda STL içine eklenecek).
Akıllı işaretçiler, bildiğimiz normal işaretçiler gibi görünüp kullanılabilen, ama belli koşullarda işaret ettikleri nesne üzerinde delete komutunu çağırıp, sisteme geri verilmesini sağlayan (yani akıllı) sınıflardır (aslında sınıf değil birer şablondurlar(template) ama kafanız karışmasın).
Basitten karmaşığa doğru elimizdeki seçenekler
1. boost::scoped_ptr<>
2. std::auto_ptr<>
3. boost::shared_ptr<> ve boost::weak_ptr<>
Normalde bu üç işaretçi tipini doğru şekilde kullanırsanız, programınızın hiçbir yerinde delete komutunu kullanmanıza gerek kalmamalı. Yani hiçbir şeyi açıkça siz delete etmeden ve hiç bellek sızdırmadan C++ programı yazmak mümkün.
Aklınıza eğer akıllı işaretçiler ile ilgili performans kaygıları geliyorsa, hemen bu kaygılarınızdan sıyrılmak için boost.org'daki akıllı işaretçi performans analizi yazılarını okuyabilirsiniz. Göreceksiniz ki pek çok durumda hiç kaygılanmanıza gerek yok (hatta Herb Sutter der ki, bir programı profiler araçlarından geçirip neresinin performans bakımından zayıf halka olduğunu tespit etmeden, böyle gaipten kaygılar duymamak lazımdır). Kısacası anafikir, gerek STL gerek boost içindeki akıllı işaretçiler son derece optimize edilmiş ve son derece iyi test edilmiş kod parçalarıdır. Bunlar hakkında performans kaygısı gütmek için, gerçekten neden bahsettiğini çok iyi biliyor olmak lazım.
scoped_ptr boost::scoped_ptr en basit akıllı işaretçi türü. Yaptığı iş sadece bir tane nesneye işaret etmek ve o nesnenin tek sahibi olup, silinirken de nesneyi de kendisi ile beraber silmek.
Önce probleme bakalım.
de()1 t new T(); // birseyler yap delete t;
void he Tx
Bu koddaki problem şu: Eğer fonksiyon ortadaki işlemler esnasında dönerse delete t komutu çalışmayacak ve t nesnesini sızdırmış olacağız. Bahsettiğimiz geri dönüş birkaç şekilde olabilir:
İlk etapta fonksiyon yazılırken orta bölümde koşullu dallardan birinde return komutu ile çıkış yapılmış, fakat oraya delete konmamış olabilir.
Başta olmasa bile sonrada kod geliştirilmeye devam edilirken ortaya bir return eklenmiş ve delete unutulmuş olabilir.
Kodda bir return olmasa bile çağırılan herhangi bir fonksiyondan istisna(exception) tetiklenmiş olabilir. Bu durumda istisna yüzünden, kontrol, istisna yakalayıcı bir kod bloğuna rastlayana kadar bir üst çağıran fonksiyona çıkmaya devam eder.
Bu üç durumda da t nesnesini sızdırmış oluruz. Oysa ki bu üç durumda da yığıt(stack) içinde yaşayan bütün yerel nesneler yıkıcıları(destructor) çağrılarak yok edilirler. İşte bu yüzden biz de normal işaretçiler yerine scoped_ptr nesnelerini yığıtta yaratırız:
void hede() ( boost::scoped ptr<T> p( new T() // birseyler yap
Vi //ve bakt### nesneyi de siliyor
Bu şekilde en başta p akıllı işaretçisi yeni T nesnesine bakacak şekilde yaratılıyor ve ilkleniyor(initialization). Ardından fonksiyondan nasıl çıkılırsa çıkılsın, yerel yığıt değişkeni olan p silinirken, yıkıcı metodunun içinde delete çağrılıyor. Böylece fonksiyonda ne olursa olsun T nesnesini sızdırmıyoruz.
Yani akıllı işaretçimiz yaratıldığı kapsam (scope) içinde olunduğu sürece sahip olduğu nesneye işaret ederken, kapsam dışına çıkıldığı anda, kendisi yok olurken, işaret ettiği nesneyi de siliyor. Bu yüzden adı Kapsam İşaretçisi (Scoped Pointer).
scoped ptr'nin bir diğer önemli özelliği de kopyalanamaması ve atanamaması (assignment). Bunun sebebi scoped ptr'nin sadece bir objeye sahip olması ve bu sahipliği başka bir scoped ptr nesnesine devredememesinin özel olarak hedeflenmiş olması.
// fonksiyon kapsam#ndan ç#k#l#rken p siliniyor
Klavyeyle gezinmek için ← ve → tuşları.
Sayfa 37
boost::scoped ptr<T> pl(new T() ); boost::scoped ptr<T> p2(pl); // olmaz!!! boost::scoped ptr<T> p3; p3 pl // olmaz!!! derleme hatasi verir
Bu kısıtlamanın konulmasındaki amaç, programcının yarattığı nesnenin ömrü ile ilgili daha net kod yazmasını sağlayarak, gelecekte olası problemleri önlemek. Örneğin yukarıdaki hede fonksiyonuna yıllar sonra başka bir programcı ilaveler yapmak durumunda kalırsa, orada yaratılan T objesinin ömrünün, o kapsam içinde sınırlı olması gerektiğini hemen anlayabilir. Böylece mesela yanlışlıkla, o ömrü daha uzun bir başka işaretçiye, p'nin değerini atamaz. Çünkü bunu denerse hemen derleme hatası alır.
Eğer bu kısıt olmasa ve bu atamayı yapabilseydi hata yapmış olacaktı. Mesela düz işaretçiler kullansa, atama yapılan daha uzun ömürlü işaretçi çürük (dangling) bir işaretçi haline gelecekti. Akıllı bir işaretçi kullansa da bu sefer T nesnesini, gerektiğinden uzun süre canlı tutmuş olacaktı.
scoped ptr'ın kopyalanamaz ve atanamaz olması, kullanımı da çok daha kolaylaştırmış oluyor. Bundan sonra alışkanlık olarak aynı kapsam içinde dinamik olarak yaratıp sildiğiniz bütün nesneler için her zaman scoped ptr kullanmanızı tavsiye ederim.
Bir uyarı daha. scoped ptr'ler kopyalanamaz ve atanamaz oldukları için std::vector, std::list gibi hiçbir STL kabına (container) konulamazlar. Çünkü STL kapları içlerine konulan nesne sınıflarının kopyalanabilir ve atanabilir olmasını gerektirir. Eğer scoped ptr'leri STL kaplarına koyabilseydik, yine kapsamının dışında o nesneleri yaşatıyor olurduk, ki bu belirttiğim gibi tasarım hedefine ters. STL kapları ile kullanacağımız akıllı işaretçiler boost::shared ptr ve boost::weak ptr olacak.
Son olarak scoped ptr'ler yerel nesneler dışında bir de pimpl (pimpıl diye okunur) yaklaşımında kullanılır. Bu yaklaşımdan şimdi bahsetmeyeceğim.
Auto ptr
std::auto ptr, geçen yazıda bahsettiğimiz boost::scoped ptr'den bir adım daha yetenekli bir akıllı işaretçidir. Tıpkı scoped ptr gibi auto ptr de bir nesnenin sahibidir ve kapsam dışına çıkılırken o nesnenin silinmesini sağlar.
void hedel) (| std::auto ptr<T> p( new T() p->hodo();
Vi
// kapsamdan cikilirken T nesnesi silinir
)
Yine bu fonksiyonda ne olursa olsun (erken bir return ya da istisna) auto ptr sayesinde temel istisna güvenliği (yani istisnalar tetiklendiği takdirde bellek sızdırmama garantisi) sağlanmış olur.
Ancak scoped ptr'den farklı olarak auto ptr, kopyalanabilir ve atanabilir. Kopyalama/atama durumlarında nesnenin sahipliği el değiştirir. Başka bir deyişle bir nesneye herhangi bir anda yalnız bir auto ptr sahip olabilir. Bir auto ptr bu şekilde sahiplik kaybettiğinde resetlenmiş olur.
derleme
std::auto ptr<T> pl( new T() pl->hodo(); std:tauto ptr<T> p2l pl ); p2->hodo(); ptr<T> p3; p3 p2; // artik p2 de resetlendi // sadece p3 nesnemizin sahibi
Vi
// su anda artik pl resetlendi
pl->hodo() // zamani hatasi p2->hodo () // 1!!!calisma zamani hatasi p3->hodo() // OK. cunku p3 nesneye bakiyor
) // pl ve p2 resetlendigi icin hicbirsey yapmiyor, p3 nesne
Dolayısıyla auto ptr'ler sahip oldukları nesneyi kopyalama/atama yoluyla birbirlerine devredebilirler. Bu sebeple bazı durumlarda fonksiyonlara argüman olarak geçirilmeye veya fonksiyonlardan geri döndürülmeye çok müsaittirler.
std::auto ptr<T> Üretici()( return std::auto ptr<T>( new T()
Vi ) void Tuketici( std::auto ptr<T> p )( p->hodo();
) void Tuketici(
Vi
Üretici ve Tüketici metodlar karşımıza çok sık çıkarlar. Örneğin birbirine mesaj gönderen veya iş atayan sınıflar mesaj nesneleri üretip tüketiyor olabilirler. Normalde bu yapıda nesnelerin üretilip, el değiştirip, tüketilip, silinmeleri pek çok potansiyel bug/sızıntı noktası taşır. Eğer düz işaretçiler kullanılırsa, nesnelerin sahibinin kim olduğu, hangi işaretçilerin ne zaman sıfırlanacağı gibi konular problem yaratabileceği gibi, herhangi bir basamakta olacak bir istisna tetiklemesi de sızıntıya sebep olur. auto ptr kullanmak bu sorunların hepsini çok zarif bir şekilde çözer. Buna benzer bir problemle bir sonraki karşılaşmanızda auto ptr kullanmak aklınızda olsun.
Son olarak, gerek scoped ptr, gerekse auto ptr, sınıflarda üye değişken olarak da kullanılabilir. Böyle kullanıldıklarında sınıfın yıkıcı metodu içinde delete çağrılmasına gerek kalmamış olur.
elass Tİ public: T() : u( new Ul) ) () // u ilkleme listesinde sistemden ali // burada delete yapmaya gerek yok cunku nesnenin ke private: T( const T&); // kopyalanamaz T& operator-( const T& ); // atanamaz std:tauto ptr<U> ü; VW»,
Burada dikkat etmeniz gereken şey, kopya yapıcı ve atama operatörünün private yapılarak, sınıfın kopyalanamaz ve atanamaz hale getirilmesi. Nitekim sınıfın kopyalanmasına izin verseydik, bu sınıftan bir nesneyi başka bir nesneye kopyaladığımız anda, nesnelerden biri sahip olduğu nesneyi kaybedecekti. Çünkü bir nesnenin auto ptr'si diğer nesneninkine kopyalanacak ve sadece bir auto ptr nesneye bakmaya devam edecekti.
Aynı sebepten ötürü auto ptr de tıpkı scoped ptr gibi STL kaplarında kullanılmaya uygun değildir. Kopyalanabilir olduğu halde, auto ptr kopyaları birbirine denk olmadıkları için, STL kaplarının metodları kendi içlerinde kopyalamalar yaparken auto ptr'leri istemeden resetlemiş olurlar. Mesela içi auto ptr'lerle dolu bir std::vector kabına std::sort işlemini uyguladığınızı düşünün.