top of page

C++ Dilinde Sınıf ve Özel Üye Fonksiyonları(Special Member Functions)

  • Writer: Yusuf Hançar
    Yusuf Hançar
  • Jun 11, 2023
  • 19 min read

Önceki yazılarımızda sınıf kavramına giriş yaparak ve gelişim süreci ile mimarisini değerlendirmiştik. Sınıf içerisinde ihtiyaçlarımıza göre tasarımımızı yaparken kullandığımız dilin sağladığı, sınıfın davranışını ve yaşam döngüsünü yöneten özel fonksiyonlara değineceğiz. C++ derleyicilerinin otomatik olarak oluşturabildiği ve kullanıcının da gerekli gördüğü durumlarda tanımlamasına imkan verilen fonksiyonlardır.

Sınıfların davranışlarını kontrol eder derken bunun kaynakların yönetimi ve performansı iyileştirmeye katkı sağlaması, dile eklenmesinin gerekliliğidir. Bunu dikkatli ve uygun şekilde yaptığımızda beklenen davranışı çıktı olarak verecektir. Tabi modern C++ öncesi ve sonrası diye devrim niteliğinde olan bir ayrım denilen gelişim süreci de bu özel üye fonksiyonların da değişimini etkilemiştir. İleride detaylandıracağımız kopyalama ve taşıma işlemleri önemli işlevlerdendir.

Bu özel üye fonksiyonları önce maddeler halinde yazarak tanımlamalarını yaparak kodlarla pekiştirmeye başlayalım :
  1. Sınıfın kurucu işlevi (constructor)

  2. Sınıfın yıkıcı işlevi (destructor)

  3. Sınıfın kopyalayan kurucu işlevi (copy constructor)

  4. Sınıfın kopyalayan atama işlevi (copy assignment)

  5. Sınıfın taşıyan kurucu işlevi (move constructor)

  6. Sınıfın taşıyan atama işlevi (move assignment)

Yukarıda 6 adet olan derleyicinin belirli koşullar sağlandığında programcı için yazmasının özel yaptığı üye fonksiyonları örnek bir sınıf içerisinde formatlarını inceleyelim.
class SmartCode {
public :
    SmartCode() = default;                   // default constructor
    ~SmartCode() = default;                  // default destructor

    SmartCode(const SmartCode&);             // copy constructor
    SmartCode(SmartCode&&);                  // move constructor(c++11)

    SmartCode& operator=(const SmartCode &); // copy assignment
    SmartCode& operator=(SmartCode &&);      // move assignment(c++11)
};
*****************************
AÇIKLAMA: 6 adet special member functions...
*****************************
"Bu özel üye fonksiyonların dilin kurallarına göre derleyici tarafından oluşturulmasına implicitly declared denir."

1. Sınıfın kurucu işlevi (constructor)

  • Bir sınıf nesnesinin hayata gelmesi için o sınıf nesnesini hayata getirecek fonksiyonun çalışması gerekmektedir. Geri dönüş değeri(return value) kavramı yoktur. (but has a return value!!!) Bu kurucu işlev const üye fonksiyon, global fonksiyon ya da static üye fonksiyon olamaz. Önceki yazımızda function overloading konusuna değinmiştik, kurucu işlevler de overload edilebilmektedir. Bu kurucu işlev sınıf içerisinde de inline olarak tanımlanabilmektedir.

  • Bu kurucu işlev parametre değişkenine sahip değilse ve bir ya da birden fazla parametresi olup tüm parametreleri default argument ile tanımlandıysa "default constructor" olarak değerlendirilmektedir.


class SmartCode {
public :
    SmartCode();
    SmartCode (int val = 23);
    SmartCode (int val = 23, double dval = 5.5);
};

int main()
{
    bool is_def_ctor = std::is_default_constructible<SmartCode>::value;
    
   if (is_def_ctor)
   {
       cout << "SmartCode sınıfı default contructor'a sahiptir" << endl;
   }
   else
   {
       cout << "SmartCode sınıfı default contructor'a sahip değil" << endl;
   }
}
*****************************
AÇIKLAMA: Sınıf içindeki tüm kurucu işlevler default constructor olarak değerlendirilir. 
"std::is_default_constructible" type trait özelliği ile bunu test edebiliriz.
*****************************
class SmartCode {
public :
};

int main()
{
    SmartCode df;    
}
*****************************
AÇIKLAMA: derleyici default constructor yazar.
*****************************
class SmartCode {
public :
    SmartCode(int);
};

int main() 
{ 
    SmartCode df;   
}
*****************************
AÇIKLAMA: Sınıfın varsayılan kurucu(default constructor) işlevi olmadığı ve derleyici de yazmadığı için hata verecektir.
*****************************
CEVAP: syntax error
*****************************
Eğer programcı tarafından hiç kurucu işlev(constructor) yazılmaz ise derleyici tarafından "non-static, public, inline" default contructor yazılacaktır.
class SmartCode {
public :
    SmartCode (){}
};
*****************************
AÇIKLAMA: derleyici sınıf içerisindeki gibi non-static, public, inline default constructor yazar.
*****************************

2. Sınıfın yıkıcı işlevi (destructor)

  • Sınıfın yıkıcı işlevinin çalışması için kurucu işlev(constructor) kodunun tamamlanması gerekir. Yıkıcı işlev(destructor) koduna girdiği an nesnenin hayatı bitmiş sayılır. Constructor sonuna kadar gelindiğinde nesne hayata gelmiş sayılır. Bildirilmezse programcı tarafından "implicitly declared" olarak yazılacaktır. (non-static, public, inline) Destructor sınıfın non-static member fonksiyonudur. Geri dönüş değeri kavramı yoktur. Overload edilemez yani sınıfın tek bir yıkıcı işlevi olabilir. Parametresi olamaz. Const olarak, static member function olarak tanımlanamaz. Destructor nokta(".") ya da ok("->") operatörlerinin sağına ismi yazılarak çağırılabilir. Destructor ismiyle çağırılması sadece özel bir durumda kullanılır.

class SmartCode {
public :
    ~SmartCode();
};
*****************************
AÇIKLAMA: destructor
*****************************
class SmartCode { 
public : 
    ~SmartCode();      
};

int main() 
{
    SmartCode* ptr;
    
    ptr->~SmartCode();   
} 
*****************************
AÇIKLAMA: destructor çağrısı ok operatörünün sağında ismi ile çağırılır.
*****************************
class SmartCode { 
public :
    SmartCode()
    {
        cout << "SmartCode constructor this : " << this << endl;
    }

    ~SmartCode() 
    { 
        cout << "SmartCode destructor this : " << this << endl;
    }
};

SmartCode gl_val;       

int main()  
{ 
    cout << "main basladı" << endl;
    cout << "main sonlandı" << endl;
}
*****************************
AÇIKLAMA: gl_val sınıfın global nesnesi main öncesi çağrılır ve buna ait kurucu işlev çağırılır. Sınıfın non-static fonksiyonları oldukları için this nesneleri vardır.
*****************************
CEVAP: SmartCode constructor this 
       main basladı 
       main sonlandı 
       SmartCode destructor this
*****************************

Static yerel değişkenler tanımlandıkları blok içerisindeyken kurucu işlev çağırılır, main sonunda ise yıkıcı işlevleri çağırılır.
class SmartCode { 
public : 
    SmartCode() 
    { 
        cout << "SmartCode constructor this : " << this << endl;         
    }

    ~SmartCode() 
    { 
        cout << "SmartCode destructor this : " << this << endl;            
    } 
};

void gl_code()
{
    static int counter = 0;

    cout << "gl_code() işlevine yapılan " << ++counter << ". çağrı" <<  endl;

    static SmartCode val;      
}

int main()  
{ 
    cout << "main basladı" << endl;
    
    for (int idx{0}; idx < 3; ++idx)
    {
        gl_code();
    }
    
    cout << "main sonlandı" << endl; 
}
*****************************
AÇIKLAMA: val nesnesi static olduğu için her defasında yeniden oluşturulmayacaktır. main sona erdikten sonra yıkıcı işlevi çağırılmaktadır.
*****************************
CEVAP: main basladı 
       gl_code() işlevine yapılan 1.çağrı
       SmartCode constructor this
       gl_code() işlevine yapılan 2.çağrı
       gl_code() işlevine yapılan 3.çağrı
       main sonlandı 
       SmartCode destructor this
*****************************

Static yerel değişken bir fonksiyonun ana bloğunda olmak zorunda değildir.!!!
class SmartCode { 
public : 
    SmartCode() 
    { 
        cout << "SmartCode constructor this : " << this << endl;         
    }

    ~SmartCode() 
    { 
        cout << "SmartCode destructor this : " << this << endl;            
    } 
};

int main()  
{ 
    cout << "main basladı" << endl;
    
    for (int idx{0}; idx < 3; ++idx)
    {        
       cout << "döngünün " << idx + 1 << ". turu" << endl;
        static SmartCode val;
    }
    
    cout << "main sonlandı" << endl; 
}
*****************************
AÇIKLAMA: 
*****************************
CEVAP: main basladı 
       döngünün 1. turu
       SmartCode constructor this
       döngünün 2. turu
       döngünün 3. turu
       main sonlandı 
       SmartCode destructor this
*****************************
Otomatik ömürlü nesneler için tanımlandıkları yerde kurucu işlev çağırılır, blok sonuna gelince yıkıcı işlev çağırılır.
class SmartCode { 
public : 
    SmartCode() 
    { 
        cout << "SmartCode constructor this : " << this << endl;         
    }

    ~SmartCode() 
    { 
        cout << "SmartCode destructor this : " << this << endl;            
    } 
};

int main()
{
    cout << "main basladı" << endl;
    {
        SmartCode val;      // otomatik ömürlü sınıf nesnesi
        cout << "main devam ediyor - 1" << endl;  
    }

    // burada val nesnesinin hayatı biter, yani destructor çağırılır.
    
    cout << "main devam ediyor - 2" << endl;
}  
*****************************
AÇIKLAMA: 
*****************************
CEVAP: main basladı 
       SmartCode constructor this
       main devam ediyor - 1 
       SmartCode destructor this
       main devam ediyor - 2
*****************************
class SmartCode { 
public : 
    SmartCode() 
    { 
        cout << "SmartCode constructor this : " << this << endl;         
    }

    ~SmartCode() 
    { 
        cout << "SmartCode destructor this : " << this << endl;            
    } 
};

int main() 
{ 
    cout << "main basladı" << endl; 

    if (1)
    {
        SmartCode val;

        if (1)
        {
            SmartCode val1;
        }
    }
     
    cout << "main devam ediyor - 1" << endl; 
}
*****************************
AÇIKLAMA: 
*****************************
CEVAP: main basladı 
       SmartCode constructor this
       SmartCode constructor this
       SmartCode destructor this
       SmartCode destructor this
       main devam ediyor - 1
*****************************
Elemanları sınıf türünden dizi örneği :
class SmartCode { 
public : 
    SmartCode() 
    { 
        cout << "SmartCode constructor this : " << this << endl;         
    }

    ~SmartCode() 
    { 
        cout << "SmartCode destructor this : " << this << endl;            
    } 
};

int main()
{
    SmartCode mval[2];        
}
*****************************
AÇIKLAMA: mval nesnesi hayata gelince tüm elemanları hayata gelir ve 2 kez kurucu işlev, 2 defa yıkıcı işlev çağırılır.
*****************************
CEVAP: SmartCode constructor this
       SmartCode constructor this
       SmartCode destructor this
       SmartCode destructor this
*****************************
class SmartCode {
public : 
    SmartCode() 
    { 
        int val;
        cout << val++ << " "; 
    } 
};

int main() 
{ 
    SmartCode mval[10];  
}
*****************************
AÇIKLAMA: yine dizinin her bir elemanı için kurucu işlev çağırılır
*****************************
CEVAP: 0 1 2 3 4 5 6 7 8 9
*****************************
Dinamik ömürlü nesneler -eğer new operatörü ile oluşturulmuş ise- yıkıcı işlev çağrısı için(kaynakların geri verilmesi için) kullanıcının delete etmesi gerekir.
class SmartCode { 
public : 
    SmartCode() 
    { 
        cout << "SmartCode constructor this : " << this << endl;         
    }

    ~SmartCode() 
    { 
        cout << "SmartCode destructor this : " << this << endl;            
    } 
};

int main() 
{
    cout << "main basladı " << endl;

    auto ptr = new SmartCode;
   
    cout << "main sonlandı." << endl;
}
*****************************
AÇIKLAMA: yıkıcı işlev çağırılmadı. ptr delete edilmelidir.
*****************************
CEVAP: main basladı 
       SmartCode constructor this
       main sonlandı 
*****************************
Bir pointer değişken ya da bir reference isim oluşturulması durumunda yani; hayatta olan bir nesneyi bir referansa bağlarsak ya da hayatta olan bir nesnenin adresini bir pointer değişkende saklarsak kurucu işlev çağrısı olmayacaktır.
class SmartCode { 
public : 
    SmartCode() 
    { 
        cout << "SmartCode constructor this : " << this << endl;         
    }

    ~SmartCode() 
    { 
        cout << "SmartCode destructor this : " << this << endl;            
    } 
};

int main()
{
    SmartCode val;
    SmartCode* ptr{ &val };
}
/* ya da */
int main()
{
    SmartCode val;
    SmartCode& ref{ val };
}
*****************************
AÇIKLAMA: pointer değişken için yeni kurucu işlev çağrısı olmadı.!!!
*****************************
CEVAP: SmartCode constructor this
       SmartCode destructor this
****************************
Bir fonksiyona geçirilen parametrenin sınıf türünden olması ile sınıf türünden reference olması durumunda kurucu işlev çağrısı farklılık göstermektedir.
class SmartCode { 
public : 
    SmartCode() 
    { 
        cout << "SmartCode constructor this : " << this << endl;         
    }

    ~SmartCode() 
    { 
        cout << "SmartCode destructor this : " << this << endl;            
    } 
};

void gl_func(SmartCode &ref)
{
}

int main() 
{ 
    SmartCode val; 
    
    for (int idx{0}; idx < 3; ++idx)
    {
        gl_func(val);                       
    }
}
*****************************
AÇIKLAMA: fonksiyon parametresi reference olduğunda val nesnesinin bir kopyası oluşturulmaz. Bunun yerine val nesnesinin referansı fonksiyona iletilir. Her döngüde aynı sınıf nesnesi reference olarak kullanıldığı için birer defa kurucu ve yıkıcı işlev çağırılır
*****************************
CEVAP: SmartCode constructor this
       SmartCode destructor this
****************************
class SmartCode { 
public : 
    SmartCode() 
    { 
        cout << "SmartCode constructor this : " << this << endl;         
    }

    ~SmartCode() 
    { 
        cout << "SmartCode destructor this : " << this << endl;            
    } 
};

void gl_func(SmartCode ref)
{
}

int main() 
{ 
    SmartCode val; 
    
    for (int idx{0}; idx < 3; ++idx)
    {
        gl_func(val);                       
    }
}
*****************************
AÇIKLAMA: fonksiyon parametresi reference olmazsa her seferinde val nesnesinin bir kopyası oluşturulur ve bu kopya fonksiyona iletilir. Bu durumda her döngüdeki fonksiyon çağrısında bi sınıf nesnesi oluşturulur. Burada sınıfın default kurucu işlevi değil kopyalayan kurucu işlevi çağırılır
*****************************
CEVAP: SmartCode constructor this
       SmartCode destructor this
       SmartCode destructor this
       SmartCode destructor this
       SmartCode destructor this
****************************

Default kurucu işlev kullanılarak aşağıdaki çağrılar ile nesne hayata getirilebilir.
class SmartCode {
public : 
    SmartCode() 
    { 
        cout << "SmartCode constructor this : " << this << endl;         
    }

    ~SmartCode() 
    { 
        cout << "SmartCode destructor this : " << this << endl;            
    } 
};

int main()
{
    SmartCode val;  
// sınıf türünden bir nesne default init edilirse öğeler çöp değerde olur. val, çöp değerle hayata gelir

    SmartCode tmp{};      
// value initialize (bu şekilde çöp değerle hayata gelmez. ilk değeri biz vermezsek zero init edilirler)

    SmartCode arr[3];        
// 3 defa ctor ve 3 defa dtor çağırılır.

    SmartCode arr_tmp[3]{}     
// 3 defa ctor ve 3 defa dtor çağırılır.
}

Sınıfa parametreli bir kurucu işlev yazarsak eğer derleyici default kurucu işlev yazmayacaktır.
class SmartCode { 
public : 
    SmartCode(int val) 
    { 
        cout << "SmartCode copy constructor this : " << this << endl;     
    } 

    ~SmartCode()  
    { 
        cout << "SmartCode destructor this : " << this << endl;     
    } 
};

int main() 
{ 
    SmartCode val;       // syntax error. derleyici def ctor yazmaz.

    SmartCode m1(5);     // direct initialize

    SmartCode m2{6};     // uniform initialize(brace direct)(c++11)

/* NOT: uniform init. kullanılan her yerde narrowing conversion error olur */

    SmartCode m3 = 7;    // copy initialize
}
class SmartCode { 
public : 
    SmartCode(int val) 
    { 
        cout << "SmartCode copy constructor this : " << this << endl;     
    } 

    ~SmartCode()  
    { 
        cout << "SmartCode destructor this : " << this << endl;     
    } 
};

int main() 
{  
    SmartCode val = 5;  
}
AÇIKLAMA: val değişkeni için copy initialize
*****************************
CEVAP: SmartCode copy constructor this
       SmartCode destructor this
       SmartCode destructor this
****************************

Explicit Constructor : Bir sınıfın tek parametreli kurucu işlevinin otomatik çağırılmasını engellemek için kullanılır. Yani implicit dönüşümlere izin verilmez. Bu durumda copy initialization yapılırsa syntax error olacaktır.
class SmartCode {  
public : 
    explicit SmartCode(int x)  
    { 
        cout << "SmartCode copy constructor this : " << this << endl; 
    } 

    ~SmartCode()  
    { 
        cout << "SmartCode destructor this : " << this << endl;    
    } 
};

int main() 
{  
    SmartCode val = 5;
}
AÇIKLAMA: val değişkeni için copy initialize
*****************************
CEVAP: syntax error
****************************

Sınıfların kurucu işlevlerinin overload edilebildiğini söylemiştik.
class SmartCode {  
public : 
    SmartCode(int val)   
    { 
        cout << "SmartCode int constructor this : " << this << endl;  
    } 

    SmartCode(double dval)   
    { 
        cout << "SmartCode double constructor this : " << this << endl; 
    }

    SmartCode(int val, int val)   
    { 
        cout << "SmartCode int, int constructor this : " << this << endl;      
    }
};

int main() 
{  
    SmartCode m1;            // syntax error çünkü default constructor yok
    SmartCode m2{ 1.2 };     // double çağırıldı.
    SmartCode m3{ 20 };      // int çağırıldı.
    SmartCode m4{ 4.4f };    // promotion double çağırıldı.
    SmartCode m5{ 15u };     // ambiguity 
}
*****************************
AÇIKLAMA:
*****************************
CEVAP: 
****************************

Constructor Initializer List : Bir sınıfın kurucu işlevinin en önemli işi non-static veri elemanlarına ilk değer vermektir. Member initializer list(MIL) ile aynıdır. Ancak bu durum std::initializer_list sınıfı ile karıştırılmamalıdır.!!!
class SmartCode {
private :
    int val, val1;
    double dval;

public :
    SmartCode();
};

/* smartcode.cpp */
SmartCode::SmartCode()
{
    // sınıf nesnesinin non-static üye elemanları zaten hayata gelmiştir.
    val = 5;    
}

int main()
{
    return 0;
}
*****************************
AÇIKLAMA: val değerine 5 atanması durumunda hayata getirmiş olunmaz çünkü zaten kurucu işlev yani constructor initializer list ile ilk değer verilmiştir. Burada yapılan atama işlemidir.
*****************************
CEVAP: 
****************************
class SmartCode {
private :
    int& val;

public :
    SmartCode();
};
/* smartcode.cpp */
SmartCode::SmartCode()
{
    val = 5;    
}

int main()
{
    return 0;
}
*****************************
AÇIKLAMA: Sınıfın private erişim belirtecindeki val değişkeni reference olduğu için ve ilk değer verilmediği için hata alınacaktır. Yapılan atama işlemi de assignment olduğu için hatalıdır.
*****************************
CEVAP: syntax error
****************************
class SmartCode {
private :
    const int val;

public :
    SmartCode();
};
/* smartcode.cpp */
SmartCode::SmartCode()
{
    val = 5;    
}

int main()
{
    return 0;
}
*****************************
AÇIKLAMA: Sınıfın private erişim belirtecindeki val değişkeni const olduğundan ve  ilk değer verilmediği için hata alınacaktır. Yapılan atama işlemi de assignment olduğu için hatalıdır.
*****************************
CEVAP: syntax error
****************************

Constructor initialzer list ile sınıfın tüm non-static veri elemanlarına ilk değer vermek zorunlu değildir ancak verilmemesi geçersiz durumlara yol açabilir.
class SmartCode { 
private :
    int val, val1;  

public : 
    SmartCode(); 

    void show()const 
    { 
        cout << val << " " << val1 << endl; 
    } 
};

/* smartcode.cpp */
SmartCode::SmartCode() : val{5}     
{                               
    val1 = 6;          
}

int main() 
{ 
    SmartCode sc; 
    sc.show(); 
}
*****************************
AÇIKLAMA: Bu durumda val1 default initialize edilir ancak garbage value ile hayata gelir.
*****************************
CEVAP: undefined behaviour
****************************
class SmartCode { 
private :
    int arr[5];  

public : 
    SmartCode(); 
    void show()const 
    {
        for (auto list : arr)
        { 
            cout << list << endl;
        } 
    } 
};

/* smartcode.cpp */
SmartCode::SmartCode() : arr{ 4, 8, 12 }     
{                     
}

int main() 
{ 
    SmartCode sc; 
    sc.show(); 
}
*****************************
AÇIKLAMA: ilk index verilen parametrelerle başlatılır kalanlar sıfır ile hayata gelir.
*****************************
CEVAP: 4 8 12 0 0
****************************
class SmartCode { 
private :
    int arr[5];  

public : 
    SmartCode(); 
    
    void show()const 
    {
        for (auto list : arr)
        { 
            cout << list << endl;
        } 
    } 
};

int f1() { return 1; }
int f2() { return 2; }
int f3() { return 3; }

/* smartcode.cpp */
SmartCode::SmartCode() : mx{ f1(), f(2), f(3) }  
{                        
} 

int main() 
{ 
    SmartCode sc; 
    sc.show(); 
}
*****************************
AÇIKLAMA: ilk index verilen parametrelerle başlatılır kalanlar sıfır ile hayata gelir.
*****************************
CEVAP: 1 2 3 0 0
****************************
C++ dilinde sınıf tanımı içinde pointer kullanmak, genel olarak riskli bir yaklaşım olarak kabul edilir. Bunun birkaç nedeni vardır:
  1. Memory Management: Pointerlar, bellekte dinamik olarak ayrılan alanları işaret ederler. Bu durum, bellek yönetimi sorumluluğunu sınıfa aktarır ve doğru bir şekilde yönetilmezse bellek sızıntılarına veya bellek hatalarına yol açabilir.

  2. Memory Leak: Pointerlar, bellekte ayrılan alanları manuel olarak serbest bırakmanızı gerektirir. Eğer pointerın işaret ettiği bellek alanı serbest bırakılmazsa, bellek sızıntısı oluşabilir ve programın çalışma zamanında daha fazla bellek tüketmesine neden olabilir.

  3. Null Pointer Check: Pointerlar null değerini alabilir ve null kontrolü yapmanız gerekebilir. Sınıf içindeki işaretçinin null kontrolünü unutmak, uygulamanın beklenmeyen hatalara yol açabileceği bir durumdur.

Sınıf içinde pointer kullanmaktansa modern C++ ile güvenli bir bellek yönetimi sağlayan "smart pointers" kullanabiliriz. STL de bulunan std::shared_ptr, std::unique_ptr veya std::weak_ptr gibi akıllı işaretçiler, bellek yönetimini otomatikleştirir ve bellek sızıntılarından kaçınmanızı sağlar.


Kurucu işlev eğer hiçbir şey yapmasa dahi ana bloğunu tanımlamak zorundayız.!!!

class SmartCode {
private :
    int val = 5;     // derleyici ilk değeri verecek şekilde ctor yazar.
public :
    SmartCode();          
};
*****************************
AÇIKLAMA: kurucu işlevi biz yazmazsak derleyici SmartCode () : val{5} {} şeklinde yazar. Öğeler hep aynı değerle hayata geliyorsa ve dinamik olarak bir nesneye bağlı değilse bu şekilde kullanılabilir.
*****************************

Hangi durumlarda bir sınıf nesnesi hayata başka bir sınıf nesnesinin değerinden kopyalama yapılarak gelir :

1- Açık ilk değer verme durumunda :

SmartCode m1;
SmartCode m2(m1);   // m2 m1 değeri olan sınıf nesnesi ile hayata getirildi.
SmartCode m3 = m1;
SmartCode m4{ m1 };
*****************************
AÇIKLAMA: Yukarıdaki çağrıların hepsinde copy constructor çağırılarak nesneler hayata getirilmektedir. Kopyalanan ile kopya alan nesneler tamamen birbirinden bağımsızdır. Yani m2 ya da m1 nesnelerinden herhangi birini değiştirirsek diğeri değerini koruyacaktır.
*****************************

2- Bir fonksiyonun parametre değişkeninin sınıf türünden olması durumunda :

void gl_func(SmartCode mval);       // global fonksiyon (call by value)
gl_func(mval);                
*****************************
AÇIKLAMA: mval nesnesi sınıf nesnesi ise copy constructor çağırılır.
*****************************

3- Bir fonksiyonun geri dönüş değerinin sınıf türünden olması durumunda :

SmartCode func()
{
    return val;    
}                
*****************************
AÇIKLAMA: Derleyici bu fonksiyonun geri dönüş değerini tutabilmek için geçici nesne oluşturacak ve bu nesne değerini return ifadesinde kullanılan val nesnesinden alacaktır. Sınıf türünden pointer ya da reference olmamak koşulu ile.
*****************************

Derleyicinin yazdığı senaryoya bakalım :
class SmartCode {
public :
    SmartCode(const SmartCode &);   
};

int main()
{
    SmartCode m1;
    SmartCode m2{ m1 };
}               
*****************************
AÇIKLAMA: bu durumda derleyici default constructor yazmaz.
*****************************
class SmartCode { 
public : 
    SmartCode() = default; 
}; 

int main () 
{ 
    SmartCode m1; 
    SmartCode m2{ m1 };  
}              
*****************************
AÇIKLAMA: bu durumda derleyici copy constructor yazar.(implicitly declared)
*****************************
class SmartCode {
private :
    T tx;
    U ux;

public :       
};             
*****************************
AÇIKLAMA: bu durumda derleyici copy constructor yazar.(implicitly declared) SmartCode(const SmartCode& rx) : tx{ rx.tx }, ux{ rx.ux } {} şeklinde yazar.
*****************************
class SmartCode {
private :
    int mx, my; 

public : 
    SmartCode() : mx{ 10 }, my{ 20 } 
    { 
        cout << "default constructor this : " << this << endl; 
    }

    ~SmartCode()  
    { 
        cout << "destructor this : " << this << endl;  
    }

    void show()const
    {
        cout << mx << " " << my << " / "; 
    }

    void set(int val1, int val2)
    {
        mx = val1;
        my = val2;
    }
};

int main() 
{ 
    SmartCode m1;
    m1.show(); 
    SmartCode m2{ m1 };
    m2.show(); 

    m1.set(-1, -2);
    m2.show();        // m2 m1 den bağımsızdır
}           
*****************************
AÇIKLAMA: 
*****************************
CEVAP:                                                              default constructor this                                               10 20                                                                10 20                                                                  10 20                                                             destructor this                                                  destructor this                      
****************************

Copy constructor yazmak neden gereklidir ?
  1. İdeali her zaman derleyiciye bırakmaktır. (Rule of zero : özel üye fonksiyonların hepsini derleyicinin yazması)

  2. Eğer sınıf veri elemanı olarak pointer ya da reference tutuyorsa (yani bir handle) bu durumda hemen her zaman sınıf nesnesi hayatta olduğu sürece bir kaynak kullanacak demektir. O kaynakta sınıfın bir veri elemanı olan pointer ya da referansa bağlanmış demektir. Yani RAII(kaynak edinimi ilk değer verme yöntemiyle olur) sınıfları mevzu bahistir. Pointerların birbirine kopyalanması yani aynı nesneyi göstermeleri demektir.


#define _CRT_SECURE_NO_WARNINGS                 // strcpy için

#include<cstring>
#include<iostream>                                                                                             using namespace std;

class SmartCode {
private :
    char* mp;       // heap alanında bir bellek bloğunun adresini tutar
    size_t mlen;

public :
    SmartCode (const char *ptr) : mlen{ std::strlen(ptr) }
    {
        mp = static_cast<char*>(std::malloc(mlen + 1));
        
        if (!mp)
        {
            std::cerr << "bellek yetersiz "  << endl;
            std::exit(EXIT_FAILURE);
        }

        std::strcpy(mp, ptr);
    }

    ~SmartCode()
    {
        free(mp);
    }

    void show()const
    {
        cout << mp << endl;
    }

    size_t length()const
    {
        return mlen;
    }
};

int main()
{
    SmartCode val{ "Smart Code" };
    val.show();                   
    cout << val.length() << endl;

    if (1) 
    {
        SmartCode dang = val;  
        dang.show();    
    }                     
                       
    val.show();        
}            
*****************************
AÇIKLAMA: dang oluşturulurken için copy constructor çağırılır.  dang.show() çağrısında destructor çağırılır. dang nesnesini gösteren pointer dangling pointer olacaktır. Bu yüzden bu nesneyi kullanmak run time hatasına neden olur. val.show() çağrısında RTE alınır. Burada derleyinin yazdığı copy constructor elemanları karşılıklı kopyalar. Bunlar pointer ise pointerlar birbirine kopyalanacaktır.
*****************************

Bu run-time hatasını çözmek için SmartCode sınıf nesnelerini kopyalamaya kapatabiliriz. Bu durumda compile-time hatası olacaktır. Bunu önlemek için delete edebiliriz.
SmartCode(const SmartCode&) = delete; 

Ya da kendimiz copy constructor yazabiliriz.
SmartCode (const SmartCode) : mlen(other.mlen)
{
    mp = static_cast<char*>(std::malloc(mlen + 1)); 
         
    if (!mp) 
    { 
        std::cerr << "bellek yetersiz "  << endl; 
        std::exit(EXIT_FAILURE); 
    }
        
    std::strcpy(mp, other.mp);
}            
*****************************
AÇIKLAMA: artık dang nesnesinin kendi kaynağı geri verilir.(deep copy) Pointer kopyalamak yerine kaynak kopyalanır.
*****************************
#include <cstring>
#include <iostream>

class SmartCode {
private:
    char* mp;       // heap alanında bir bellek bloğunun adresini tutar
    size_t mlen;

public:
    SmartCode(const char* ptr) : mlen{ std::strlen(ptr) }
    {
        mp = new char[mlen + 1];

        if (!mp)
        {
            std::cerr << "bellek yetersiz" << std::endl;
            std::exit(EXIT_FAILURE);
        }

        std::strcpy(mp, ptr);
    }

    SmartCode(const SmartCode& other) : mlen{ other.mlen }
    {
        mp = new char[mlen + 1];

        if (!mp)
        {
            std::cerr << "bellek yetersiz" << std::endl;
            std::exit(EXIT_FAILURE);
        }

        std::strcpy(mp, other.mp);
    }

    SmartCode& operator=(const SmartCode& other)
    {
        if (this == &other)
            return *this;

        delete[] mp;

        mlen = other.mlen;
        mp = new char[mlen + 1];

        if (!mp)
        {
            std::cerr << "bellek yetersiz" << std::endl;
            std::exit(EXIT_FAILURE);
        }

        std::strcpy(mp, other.mp);

        return *this;
    }

    ~SmartCode()
    {
        delete[] mp;
    }

    void show() const
    {
        std::cout << mp << std::endl;
    }

    size_t length() const
    {
        return mlen;
    }
};

int main()
{
    SmartCode val{ "Smart Code" };
    val.show();
    std::cout << val.length() << std::endl;

    if (1)
    {
        SmartCode dang = val;
        dang.show();
    }

    val.show();

    return 0;
}          
*****************************
AÇIKLAMA: kopyalama işlemleri için copy constructor ve copy assignment yazıldı. Bellek yönetimi içinde new[] ve delete[] kullanıldı.
*****************************

COPY ASSIGNMENT
  • Copy assignment, bir sınıfın bir başka sınıf nesnesine atanmasıdır ve atama operatörü (operator=) ile gerçekleştirilir.

  • Mevcut bir nesnenin değerlerinin, başka bir nesneye atanmasını sağlar. Bu operatör, bir sınıf nesnesini başka bir sınıf nesnesine atarken, nesnelerin özelliklerini kopyalar ve kaynak nesneyle aynı duruma getirir.

class SmartCode {
private :
    T mx;
    U ux;

public :
    SmartCode &operator=(const SmartCode& rx)     // derleyici yazarsa
    {
        mx = rx.mx;
        ux = rx.ux;
        return *this;
    }
};        
*****************************
AÇIKLAMA: 
*****************************
Big3 (c++11 öncesi) yani modern c++ ile Big5; bir sınıfa destructor yazmamız gerekiyorsa her zaman geri verilecek bir kaynak var demektir ve copy constructor, copy assignment, move constructor, move assignment yazılmalıdır.

Bir nesnenin kendisine atanmasına self assignment denir.
#define _CRT_SECURE_NO_WARNINGS                 // strcpy için

#include <cstring>
#include <iostream>

using namespace std;

class SmartCode { 
private :
    char* mp;                
    size_t mlen;

public : 
    SmartCode (const char *ptr) : mlen{ std::strlen(ptr) } 
    { 
        mp = static_cast<char*>(std::malloc(mlen + 1)); 
         
        if (!mp) 
        { 
            std::cerr << "bellek yetersiz "  << endl; 
            std::exit(EXIT_FAILURE); 
        }

        std::strcpy(mp, ptr); 
    }

    SmartCode& operator=(const SmartCode& r)      // r ile *this aynı 
    {        
        mlen = r.mlen;      // dangling pointerı da dereference etmiş olduk.

        free(mp); 
      
        mp = static_cast<char*>(std::malloc(mlen + 1)); 
         
        if (!mp) 
        { 
            std::cerr << "bellek yetersiz "  << endl; 
            std::exit(EXIT_FAILURE); 
        } 

        std::strcpy(mp, r.mp); 
        return *this; 
    }

    ~SmartCode() 
    { 
        free(mp); 
    }

    void show()const 
    { 
        cout << mp << endl; 
    }

    size_t length()const
    { 
        return mlen; 
    } 
};

int main()
{
    SmartCode n1{ "CppCodes" };
    n1 = n1;
    n1.show();           
}        
*****************************
AÇIKLAMA: copy assignment operatörünün içerisinde bir atama yapmadan önce free() ile kaynak geri verildiği için hatalıdır. Aynı nesneye atanma durumunda belleği serbest bırakıp yeniden aynı bellek bloğuna yazma işlemi yapıldığından run-time da hata meydana gelir. Hatayı gidermek için atama işleminden önce bu free() işlemini yapmamak gerekir.
***************************** 
Çözüm olarak yukarıda bahsettiğimiz self assignment kontrol edilmelidir.
SmartCode& operator=(const SmartCode &r)  
{                                                               
    if (this == &r)
    {    
        return *this;  // self assignment kontrol edildi. 
    }

    mlen = r.mlen;

    free(mp); 
  
    mp = static_cast<char*>(std::malloc(mlen + 1)); 
     
    if (!mp) 
    { 
        std::cerr << "bellek yetersiz "  << endl; 
        std::exit(EXIT_FAILURE); 
    } 

    std::strcpy(mp, r.mp); 
    return *this; 
}

Bir sınıfa copy constructor ya da copy assignment yazarsak tüm öğelerden biz sorumlu oluruz.
class SmartCode {
private :
    T* up;
    U ux, uy;

public :  
};        
*****************************
AÇIKLAMA: pointer değişkeni olduğu için copy constructor yazmammız gerekir. Yani tüm öğelerden biz sorumlu oluruz. U türünden nesneler için yazmazsak çöp değerle hayata gelirler.
***************************** 

Copy constructor ve copy assignment fonksiyonlarının kodlarını teke indirme :
#define _CRT_SECURE_NO_WARNINGS

#include <cstring>
#include <iostream>

class SmartCode {
private:
    char* mp;
    size_t mlen;

    SmartCode& deep_copy(const SmartCode& r)
    {
        mlen = r.mlen;

        mp = static_cast<char*>(std::malloc(mlen + 1));

        if (!mp)
        {
            std::cerr << "bellek yetersiz " << std::endl;
            std::exit(EXIT_FAILURE);
        }

        std::strcpy(mp, r.mp);

        return *this;
    }

    void release_resources()
    {
        std::free(mp);
    }

public:
    SmartCode(const char* ptr) : mlen{ std::strlen(ptr) }
    {
        mp = static_cast<char*>(std::malloc(mlen + 1));

        if (!mp)
        {
            std::cerr << "bellek yetersiz " << std::endl;
            std::exit(EXIT_FAILURE);
        }

        std::strcpy(mp, ptr);
    }

    SmartCode(const SmartCode& other)
    {
        deep_copy(other);
    }

    SmartCode& operator=(const SmartCode& r)
    {
        if (this == &r)
        {
            return *this;
        }

        release_resources();

        return deep_copy(r);
    }

    SmartCode(SmartCode&&) = delete; // Move semantiğini devre dışı bıraktık

    SmartCode& operator=(SmartCode&&) = delete; // Move semantiği deleted

    ~SmartCode()
    {
        release_resources();
    }

    void show() const
    {
        std::cout << mp << std::endl;
    }

    size_t length() const
    {
        return mlen;
    }
};

int main()
{
    SmartCode sc{ "SmartCodeCpp" };
    sc = sc;
    sc.show();
}       
*****************************
AÇIKLAMA: Bu kod ile copy assignment test edilmektedir. deep_copy() ile karakter dizisi kopyası alınır ve bellek tahsisi yapılır. Aynı nesnenin kendisine atanması durumunda move semantik kullanımı engellenmesi için delete edilmiştir.
***************************** 

MOVE CONSTRUCTOR
class SmartCode {
private :
    T tx;
    U ux;

public :
    SmartCode(SmartCode&& rx) : tx(move(rx.tx)), ux(move(rx.ux)){}   
};     
*****************************
AÇIKLAMA: burada move constructor başka bir R value değerinden kaynak alarak yeni bir sınıf nesnesi oluşturur. Yine bu kurucu işlev move semantiği kullanarak tx ve ux memberlarını aktarır. Aynı zamanda bu move constructor kaynak alma veya aktarma işlemini gerçekleştirir. Bu sayede kaynakların yeniden tahsis edilmesi ya da kopyalanmasının yerine mevcut kaynakların verimli şekilde yeniden kullanılması sağlanır. tx ve ux nesnelerini std::move işlevi ile rx.tx ve tx.ux değerlerine taşır. std::move sağ taraf değerini sol taraf değerine dönüştürür.
***************************** 
SmartCode val;
SmartCode val1;

int main()
{
    T val = val1;   
}
*****************************
AÇIKLAMA: Bu sınıfın copy ve move kurucu işlevleri varsa eğer bu işlem sonucu copy constructor çağırılır. Çünkü ya L value expression.
***************************** 
Şimdi örneklerle hangi durumlarda hangi özel üye fonksiyonların çağırılacağını gösterelim :
#include <iostream>

using namespace std;

class SmartCode {
public:
    SmartCode()
    {
        std::cout << "Default constructor" << endl;
    }
    
    SmartCode(const SmartCode& other)
    {
        std::cout << "Copy constructor" << endl;
    }
    
    SmartCode(SmartCode&& other) 
    {
        std::cout << "Move constructor" << endl;
    }
    
    SmartCode& operator=(const SmartCode& other) 
    {
        std::cout << "Copy assignment" << endl;
        
        return *this;
    }
    
    SmartCode& operator=(SmartCode&& other) 
    {
        std::cout << "Move assignment" << endl;
        
        return *this;
    }
};

SmartCode val;
SmartCode val1;

int main() 
{
    val = val1;
    
    return 0;
}
*****************************
CEVAP: 
Default constructor
Default constructor 
copy assignment
***************************** 
int main() 
{
    val = std::move(val1);
    
    return 0;
}
*****************************
AÇIKLAMA: move bir tür dönüştürme operatörüdür.  Burada taşıma işleminin                        val = static_cast<T&&>(val1); şeklinde bir iş yapmaktadır.
*****************************
CEVAP: 
Default constructor
Default constructor 
move assignment
***************************** 
int main() 
{
    SmartCode val(val1);
    
    return 0;
}
*****************************
CEVAP: 
Default constructor
Default constructor 
copy constructor 
***************************** 
int main() 
{
    SmartCode val(std::move(val1));
    
    return 0;
}
*****************************
CEVAP: 
Default constructor
Default constructor 
move constructor 
***************************** 

Örnekler üzerinden copy ve move kurucu işlevleri ve value category etkisiyle olan ilişkisini inceleyelim :
using namespace std;

class SmartCode {
    //...
};

void first_code(const SmartCode &)                           
{
    cout << "const L-value ref. overload"  << endl; 
}

void first_code(SmartCode &&)  
{ 
    cout << "R-value ref. overload" << endl; 
}

void second_code (SmartCode&& rval)
{
    first_code(rval);             
}

int main() 
{
    second_code(SmartCode {});    
}
*****************************
AÇIKLAMA: mainde çağırılan seconde_code() işlevi çağrısında SmartCode{} şeklinde oluşturulan geçici nesne parametre olarak geçiliyor. Bu fonksiyon parametresi sağ taraf kategorisinde ancak bağlı olduğu category L-value reference(isimlendirilmiş her nesne L-value), bu yüzden çıktı aşağıdaki gibidir.
*****************************
CEVAP: const L-value ref.overload
*****************************
using namespace std;

class SmartCode {
    //...
};

void first_code(const SmartCode &)                           
{
    cout << "const L-value ref. overload\n";
}

void first_code(SmartCode &&)  
{ 
    cout << "R-value ref. overload\n"; 
}

void second_code (SmartCode&& rval)
{
    first_code(std::move(rval));  
}

int main() 
{
    second_code(SmartCode {});      
}
*****************************
AÇIKLAMA: bu çağrıda geçilen nesne sağ tarafa değerine std::move ile dönüştürüldüğü için çıktı aşağıdaki gibidir
*****************************
CEVAP: R-value ref.overload
*****************************

C++ dilinde bir fonksiyonun parametresi sağ taraf reference(SmartCode&&) ise ve bu fonksiyon template değilse; böyle fonksiyonların amacı taşıma semantiğini implemente etmektir.
template <typename T>
void smart_code (T&& rval)
{
    //...
}
*****************************
AÇIKLAMA: Bu şekilde bir çağrıda parametrelere R-value değil forwarding reference denir. Amaç perfect forwarding implemente etmektir. Bu konu ayrı bie yazıda detaylandırılacaktır.
*****************************
CEVAP: 
*****************************

C++ standartlarına göre bir üye fonksiyonu tanımlarken derleyici, otomatik oluşturulması beklenen özel üye fonksiyonlar için (copy constructor, copy assignment, move constructor, move assignment) fonksiyonun tanımında hata tespit ederse o fonksiyonu delete eder. Derleme zamanında tespit edilerek hatalı çağrıların önüne geçilir.
struct SmartCode {
    SmartCode() = default;  // Default constructor, default

    SmartCode(const SmartCode&) {  // Copy constructor, özel tanımlandı
        // ...
    }

    SmartCode& operator=(const SmartCode&) = delete;  // Copy assignment

    SmartCode(SmartCode&&) = default;// Move constructor, default tanımlandı

    SmartCode& operator=(SmartCode&&) = default;  // Move assignment 

    ~SmartCode() = default;  // Destructor, default
};
*****************************
AÇIKLAMA: 
*****************************
CEVAP: 
*****************************

Derleyici default constructor yazarsa eğer tüm öğeleri default initialize eder.
class SmartCode {
private :
    const int val;    
public :
};

int main() 
{
    SmartCode sc;         
}
*****************************
AÇIKLAMA: val nesnesi default initialize edilir ve çöp değer ile hayata gelir. Ancak const nesnelere(referanslara da aynı şekilde!!) de ilk değer vermek zorunlu olduğu için zaten hata alınır. 
*****************************
CEVAP: Fonksiyon derleyici tarafından delete edildiği için sc nesnesinin oluşturulması syntax error. (implicitly declared)
*****************************
class SmartCode {
private :
    SmartCode(int val);    
public :
};

int main() 
{
    SmartCode sc;         
}
*****************************
AÇIKLAMA: Default constructor yoktur
*****************************
CEVAP: syntax error
*****************************

ree

Yukarıdaki tabloda özel üye fonksiyonların declaration ve delete durumları gösterilmektedir.

Örneğin bir sınıfı taşımaya açık kopyalamaya kapalı hale getirmek için move fonksiyonlar yazılırsa derleyici otomatik olarak copy özel üye fonksiyonları (constructor ve assignment) delete edecektir.
class SmartCode {
public :
    SmartCode (SmartCode &&);
    SmartCode &operator=(SmartCode &&);
};
*****************************
AÇIKLAMA: kopyalamaya kapalı hale getirilmiştir.
*****************************
CEVAP: 
*****************************
class SmartCode {
public :
    SmartCode (const SmartCode &) = delete;
    SmartCode &operator=(const SmartCode &) = delete;
    
    SmartCode (SmartCode &&);
    SmartCode &operator=(SmartCode &&);
};
*****************************
AÇIKLAMA: kopyalamaya kapalı hale getirilmiştir. Programcı tarafında copy members delete edilmiştir.
*****************************
CEVAP: 
*****************************
C++ dilinde std::unique_ptr sınıfı default olarak "taşınabilir ancak kopyalanması syntax error" olan bir yapıdır.
#include <memory> 

using namespace std; 

int main () 
{ 
    unique_ptr<int> uptr{ new int{} }; 
}
*****************************
AÇIKLAMA: val değişkenine uptr kopyalanabilmesi için gereken copy constructor delete edildiği için syntax hatasıdır.
*****************************
CEVAP: auto val{ std::move(uptr) }; şeklinde yazılarak hata giderilebilir.
*****************************

C++ dilinde std::shared_ptr, std::vector, std::list gibi container elemanları için yukarıdakinin tersi durum söz konusudur.
#include <iostream>
#include <vector>

int main() 
{
    std::vector<int> s_vec = { 1, 2, 3, 4, 5 }; 

    // s_vec taşınarak d_vec container'a aktarıldı.
    std::vector<int> d_vec = std::move(s_vec);  

    std::cout << "s_vec size: " << s_vec.size() << std::endl;
    std::cout << "d_vec size: " << d_vec.size() << std::endl;

    return 0;
}
*****************************
AÇIKLAMA:
*****************************
CEVAP: s_vec size : 0
      d_vec size : 5
*****************************
#include <iostream>
#include <vector>

int main() 
{
    std::vector<int> s_vec = { 1, 2, 3, 4, 5 }; 

    std::vector<int> d_vec = s_vec;  

    std::cout << "s_vec size: " << s_vec.size() << std::endl;
    std::cout << "d_vec size: " << d_vec.size() << std::endl;

    return 0;
}
*****************************
AÇIKLAMA:
*****************************
CEVAP: s_vec size : 5
       d_vec size : 5
*****************************
Bu yazıda C++ dilinde tanımlanan özel üye fonksiyonların kullanım senaryoları özetlenmeye çalışılmıştır. Kodun verimli ve derleyici optimizasyonuna en uygun olacak şekilde yazılması için iyi anlaşılması gereken en temel konulardandır.
 
 
 

Recent Posts

See All
C++ Dilinde [[nodiscard]] attribute

C++’ta [[attribute]] (öznitelik) mekanizması, kod hakkında ek bilgi sağlayarak derleyicinin uyarılar vermesine, belirli optimizasyonlar...

 
 
 
C++ Dilinde constinit

C++20'de tanıtılan önemli bir özellik `constinit`'dir; bu özellik, statik depolama süresine sahip değişkenlerin derleme zamanında...

 
 
 

Comments


bottom of page