C++ ile Etkili Kod Geliştirmek

C++’ı pek severim. Siz de eğer nesne yönelimli programlamayı ve ne programladığınızı bilmeyi seviyorsanız C++’ı tavsiye ederim. C++ ile yapılabilecek çok şey var fakat bunu etkili biçimde de yapabiliyor olmamız ne güzel olur. Elimden geldiğince yazdığımız C++ kodlarının nasıl daha etkili olacağından bahsedeceğim.
Aslında C++ OOP destekler demek daha doğru olacaktır. Aslında dillerin federasyonu kavramı yanlış olmayacaktır. C++ birçok paradigmanın birlikteliğini destekler bir dildir. Buna çok paradigmalı (multiparadigm) dil deniyor yani, C++ bugün itibariyle, prosedürel, fonksiyonel, generic, metaprogramming ve nesne yönelimli programlama paradigmalarını destekler. Bunların nasıl olduğunu bir başka blogumuzda açıklayabiliriz. Birinci değerlendirmemiz bu olduğuna göre, ilk kuralımız:
1- Etkili C++ kodu geliştirmek için hangi tür paradigma ile hareket ettiğimizi biliyor olmamız gerek.
C++ kodu geliştirirken, derleyiciyi önderleyiciye tercih etmeliyiz. Bu ne ifade eder: define yerine const, enums ve inline anahtar kelimelerini kullanalım. Peki neden, şu örneğe bakalım:
#define PI 3.1416
sembolik “PI” ismi derleyiciler tarafından hiç farkedilmeyebilir veya derleyiciye hiç ulaşmadan önderleyici tarafından silinebilir. Sonuç olarak PI sembol tablosuna hiç katılamayabilir. define kullanıldığında derleyici bize PI ile ilgili bir yanlış durum varsa bu hatayı hiç bulamayabilir, debug ederken karışıklık yaşayabiliriz. Hele ki bu define cümlesi bizim yazmadığımız bir header dosyasındaysa vay halimize.
Bunun çözümü basit, yapılması gereken aşağıdaki gibi bir cümle ile değiştirmektir.
const double Pi = 3.1416
O halde kuralımız:
2- Derleyiciyi, ön derleyiciye tercih et.
C++ kodlarında sürekli const görürüz. Açıkçası beni biraz rahatsız bile etmiştir çünkü gevşek kod yazmaya alışmıştım. Bu gevşeklik tabii ki sürekli hata olmasına sebep oluyordu. Ne zaman bir değişkenin değiştirilmesini istemiyorsanız mutlaka “const” kullanmalısınız. Bu gerçek anlamda bir encapsulation işlemidir.
const’un ne olduğundan çok bahsetmek istemiyorum ama operatorlere uygulanan çok güzel bir ornek var inceleyelim:
class Sayi {....}; const Sayi operator* (const Sayi& lhs, const Sayi& rhs);
Evet kodumuz böyle olmalıdır. Şöyle bir girişime izin vermeyecektir kodumuz:
Sayi a,b,c; (a * b) = c;
Eğer operator* in sonucu const Sayi olmasaydi bu işlem başarılı olabilirdi fakat bu haliyle böyle bir hatanın önüne geçmiş oluyoruz.
Çok daha basit ve encapsulation’ı net bir şekilde gösteren örneğimize bakalım:
class Sayi { int raw; int foo; public: int getRaw () const {return raw;} };
getRaw bir member function’dır. dikkat ederseniz parantezlerden sonra bir const ifadesi var. Bu, üye fonksiyon (getRaw) hiç bir üye değişkenin (raw veya foo) değerini değiştirmeyecektir anlamındadır. Yani getRaw içerisinde “foo = 2;” gibi bir cümle yazdığımız zaman derleyici hata verecektir.
3- Const bizi sınırlar, gereken her yerde const kullanmalıyız.
C++’ın en sevdiğim taraflarından biri de kaynakları nasıl kullanacağınıza kendinizin karar vermesidir. Fakat bu da beraberinde bir başka problemi getiriyor ki kimi diller bu yüzden prim bile yapmıştır. Garbage Collector. Belki duymuşsunuzdur RAII adında bir kavram vardır, resource acquisition is initialization, en temelinde şunu söyler, başladığın işi bitir birader. initialization varsa finalization da yapılmalıdır. Bir nesne yaratıldığı zaman constructor çağırılır ve öldürüldüğü zaman da destructor çağırılır. Buraya kadar tamam. Eğer ölüyorsa zaten sorun yok. Fakat heap yönetiminde durup tekrar bakmamız gereken yerler var. Örneğin bir pointer yaratıldı ise nesneyle birlikte, destructor çağırıldığı zaman bu pointer’ın da özellikle silinmesi gerekir. Yoksa memory leak dediğimiz probleme davetiye çıkarmış oluruz. RAII sınıflarımızı nasıl yaratacağımızı bir başka blogda detaylıca anlatacağım. Burada pointerlar ile ilgili en hızlı resource management araçlarının bir kaçından bahsedeceğim.
Eğer pointer öldü mü kaldı mı endişe etmek istemiyorsanız std::shared_ptr veya auto_ptr kullanmak en güzelleri olacaktır. İkisi arasındaki fark şu, auto_ptr kullandığınızda bir pointer’ın sahipliği auto_ptr ye geçer ve sahipliğini devreden pointer null olur. Yeni auto_ptr eğer bir başka auto_ptr tarafıdan sahipliği alınırsa bir önceki auto_ptr yi null eder. Bu böyle devam eder. shared_ptr ise aynı kaynağa birden fazla pointer’ın erişebilmesine olanak verir. Fakat kaynağa erişmeye çalışan hiç bir shared_ptr kalmadığı zaman shared_ptr ölür.
Örnekleyelim:
{ std::auto_ptr<std::string> string1 (new std::string("burada bir string var")); //string1 new.. ile başlayan nesneye point ediyor. std::auto_ptr<std::string> string2 (string1); //string2 şimdi "burada bir string var" string'ine point ediyor ama dikkat string1 şimdi null string1 = string2; // şimdi tekrar string1 point ediyor string2 ise null }
Yukarıda auto_ptr nin nasıl çalıştığını görüyoruz bir de shared_ptr ye bakalım:
{ std::shared_ptr<std::string> string1 (new string("burada yine bir string var")); // string1 "burada yine bir string var" string'ine point ediyor. std::shared_ptr<std::string> string2 (string1); //hem string1 hem de string2 pointerları "burada yine bir string var" stringine point ediyor. string1 = string2; // :) hiç bir değişiklik yok. aynı tas aynı hamam } // string1 ve string2 pointerları destroy edildi ve point ettikleri nesne de otomatik olarak silindi.
4- Amaca uygun olarak shared_ptr veya auto_ptr kullanmak kaynakları doğru kullanmanıza olanak sağlayacaktır.