Merhaba sevgili yazılımperver dostlarım. Bugün C++ geliştiricilerin vakıf olduğu “rule of three” ve C++ 11 ile birlikte artık “rule of five” mevzusuna bakıyor olacağız. Bunu yaparken de, C++ constructor, copy constructor, assignment operator gibi temel kavramlara da eğiliyor olacağız. Kurallara geçmeden önce, bu temel kavramları ve nasıl kullanıldıklarını hatırlayalım isterseniz.
Temel Sınıf Oluşturma/Atama Operasyonları
Yapıcılar aslında, sınıf ile aynı ismi tanıyan (derleyici diğer fonksiyonlardan bu sayede onları ayırır), dönüş değeri olmayan özel üye fonksiyonlardır ve sınıf içerisindeki verileri geçerli değerler ile doldurmak (ya da benzeri) amacı ile kullanılırlar. Üç tip yapıcı bulunur. Bunlar: Varsayılan, parametrik ve kopya yapıcılardır. Varsayılan yapıcı, herhangi bir parametre almayan ve eğer programcı tanımlamaz ise derleyici tarafından otomatik olarak üretilen bir yapıcıdır. Temel değerler 0/false ile doldurulur.
Parametrik yapıcılar ise varsayılanlardan farklı olarak çeşitli girdiler alabilmektedir. Bunlar da ilgili sınıfın oluşturulmasında ve verilerin ilklendirilmesinde kullanılırlar.
Son olarak, kopya yapıcı ise isminden de anlaşılacağı gibi, daha önce aynı tipte oluşturulmuş bir nesne ile yeni nesnenin ilklendirilmesi için kullanılan yapıcıdır. Bu yapıcı için nesneye referans tipinde argüman geçirilir.
Aşağıda bütün bunları gösteren basit bir örnek bulabilirsiniz:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
#include <iostream> using namespace std; class Sinif { public: // Varsayilan yapici Sinif(){ cout << "Varsayilan yapici\n"; } // Parametre alan yapici Sinif(int deneme){ cout << "Parametrik yapici\n"; } // Kopya yapici Sinif(const Sinif& Parametre){ cout << "Kopya yapici\n"; } }; // Bu durumda da kopya yapici cagrilir void kopyaAlanFonks(Sinif girdi){ } // Bu durumda da kopya yapici cagrilir Sinif returnKopyaFonks(){ Sinif a; return a; } int main() { // Varsayilan yapicilar Sinif ornek1; Sinif* ornek2{new Sinif}; Sinif ornekArray[4]; // Parametrik yapicilarin cagrildigi durumlar Sinif ornek3{1}; Sinif* ornek4{new Sinif(2)}; Sinif ornek5Array[4]={Sinif(1), Sinif(2), Sinif(3), Sinif(4)}; // explicit koymadigimiz icin dolayli yoldan parametrik yapici calisir kopyaAlanFonks(3); // Kopya yapicilar Sinif ornek6 = ornek1; Sinif ornek7(ornek1); Sinif ornek8{ornek1}; Sinif* ornek9{new Sinif(ornek1)}; kopyaAlanFonks(ornek1); // Asagidaki ornek varsayilan olarak kopya yapiciyi cagirmiyor // -fno-elide-constructors ile bunu aktiflestirebilirsiniz. Detaylar icin https://www.wikiwand.com/en/Return_value_optimization Sinif ornek10 = returnKopyaFonks(); return 0; } |
Bu örnekte gördüğünüz üzere bir sınıf için birden fazla yapıcı tanımlayabilirsiniz. Yapılacar mevzusunu kapatmadan, onlar ile ilgili bir kaç hususa daha değinelim:
- Yapıcıları tanımlamazsanız, derleyiciler varsayılan bir taneyi sizin için tanımlar (tabi özellikle tanımlamamasını demezseniz),
- Yapıcılar statik ya da virtual ön eki almaz,
- Yapıcılar için olabildiğince ilklendirme işlemleri yapıp, “exception” üretebilecek durumlardan kaçınmalısınız.
Şimdi kurallara bakabiliriz.
Üç Kuralı
Üç kuralı aslında C++ 11 öncesi kullanım için geçerli, sonrası için beş kuralına da bakıyor olacağız. Üç kural temelde şunu der:
Eğer bir C++ sınıfı, yıkıcı (“destructor”) ya da kopya yapıcı da kopyalama operatöründen (operator =()) birini tanımlarsa, diğerlerini de tanımlamalıdır.
Yapıcılara baktık, yıkıcı da temelde ilgili sınıf kapsam dışına çıktığında ya da özellikle silindiğinde çağrılan yine özel bir fonksiyondur. Atama operatörü ise, zaten oluşturulmuş bir nesnenin içeriğinin diğerine kopyalanması amacı ile kullanılır.
Bunun arkasında yatan temel motivasyon da, eğer programcı bunlardan birini tanımlama ihtiyacı duymuşsa, muhtemelen (özel durumlar hariç) otomatik olarak tanımlanacak diğer yapıcılar ve yıkıcı ihtiyacını karşılamayacaktır. Çünkü, derleyici tarafından tanımlanan yapıcılar ve kopyalama operatörü, basitçe verileri kopyalar (“shallow copy”), işaretçilerin veya benzeri yapıların ihtiyaç duyacağı kopyalama işlemlerini yapmaz.
Şimdi bunun sıkıntı yarattığı bir duruma göz atalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
#include <iostream> using namespace std; class Sinif { private: int mSize; int* mVals; public: // Yikici tanimli ~Sinif(); // Parametrik yapici da Sinif( int s, int* v ); // Kopya yapici ya da atama operatoru tanimli degil. }; // Yikici yonettigimiz bellegi siliyoruz Sinif::~Sinif() { cout << "yikici cagriliyor\n"; delete mVals; mVals = nullptr; } // Parametrik yapici icerisinde "deep copy" yapiyoruz. Bunda sikinti yok Sinif::Sinif( int s, int* v ) { cout << "parametrik yapici cagriliyor\n"; mSize = s; mVals = new int[ mSize ]; std::copy( v, v + mSize, mVals ); } int main() { int mVals[ 4 ] = { 11, 22, 33, 44 }; // Parametrik yapicimiz cagriliyor Sinif a1( 4, mVals ); // Iste bu noktada, derleyici tarafindan olusturulan kopya yapici cagriliyor // O da sadece degerleri kopyaladigi icin işaretciler aslinda ayni yeri gösteriyor Sinif a2( a1 ); // Burada da silme işlemi iki kere aynı adres icin cagriliyo return 0; } |
Yukarıdaki örnekte, her ne kadar parametrik yapıcı ile dinamik bellekler kopyalansa da, kopya yapıcı tanımlanmadığı için kopyalama işlemi sadece adreslerin kopyalanması ile kalıyor. Bu da ilgili nesneler kapsam dışına çıktığında yıkıcının zaten silinmiş bir belleği iki kere silmeye kalkmasına yol açıyor. Yukarıdaki kodu çalıştırdığınızda aşağıdaki gibi bir çıktı görmeniz normal:
1 2 3 4 |
parametrik yapici cagriliyor yikici cagriliyor yikici cagriliyor free(): double free detected in tcache 2 |
Benzer şekilde kopya yapıcının olup, atama operatörünün olmadığını düşünün. Bu durumda da, yine atama ile kopyalanan nesnenin değerleri basitçe kopyalanacak ve işaretçiler yanı yeri gösteriyor olacaklar.
Şimdi gelelim beş kuralına.
Beş Kuralı
Beş kuralı temelde, üç kuralı ile aynı olması yanında, taşıma yaklaşımı ile gelen yeni taşıma yapıcısı ve atama operatörünü de göz önüne alır ve kuralı şu şekilde günceller:
Eğer bir C++ sınıfı, yıkıcı (“destructor”) ya da kopya yapıcı da kopyalama operatöründen (operator =()) ya da taşıma yapıcısı ya da taşıma atama operatöründen birini tanımlarsa, diğerlerini de tanımlamalıdır.
taşıma operatör ya da yapıcılarının tanımlanmaması her ne kadar hata olmasa da özellikle bellek yönetimi gerektiren sınıflarda bunların tanımlanması daha verimli bir çözüm sağlar. Bu bağlamda yukarıdaki sınıf için aşağıdaki iki operatörü tanımlamalıyız:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// Taşıma yapıcısı Sinif::Sinif(Sinif&& other) { this->mSize = other.mSize; this->mVals = other.mVals; other.mVals = nullptr; } // Taşıma atama operatörü Sinif& Sinif::operator=(Sinif&& rhs) { // kendi kendine atama kontrolü if (this != &rhs) { this->mSize = rhs.mSize; // Eski veriyi silelim if (this->mVals) { delete[] this->mVals; } this->mVals = rhs.mVals; rhs.mVals = nullptr; } return *this; } |
Bu arada taşıma yapıcısı ve atama operatörü ve diğer taşıma semantiğine ilişkin detaylar için aşağıdaki yazıma göz atabilirsiniz.
Modern C++ (5) : Taşıma Semantikleri
Bir sonraki yazımda görüşmek dileğiyle, kendinize iyi bakın.