C++ Copy Constructor

20 minute read

Soru cevap olarak gidersek konuyu daha iyi anlayabileceğimizi ve anlatabileceğimi umuyorum.


Türkçe olarak nasıl çevirebiliriz?

  • Yoğun olarak Kopyalan Kurucu İşlev olarak çevirildiğini gördüm. Genelde forumlar ve bloglarda kısaltmasını görmeniz mümkün. Kısaltması CC.
  • Öyle durumlar var ki, bir nesne hayata değerini başka bir nesneden alarak başlıyor. Böylesi durumlarda hayata gelen nesne için çağrılan constructor a COPY CONSTRUCTOR deniliyor.

Aşağıdaki örneği inceleyelim:

  • Bu örnekte C++03 ve C++11 kullanımlarına da örnek verelim.
 1// Copy Constructor
 2#include <iostream>
 3
 4using namespace std;
 5
 6class Myclass {
 7public:
 8    Myclass() {
 9        cout << "default constructor" << endl;
10    }
11
12    Myclass(const Myclass &r)
13    {
14        cout << "copy constructor" << endl;
15    }
16
17};
18
19
20int main()
21{
22    Myclass m1;         // default constructor
23    Myclass m2{m1};   // C++11 - copy constructor
24    Myclass m3 = m1;   // C++03 - copy constructor
25    Myclass m4(m1);   // C++03 - copy constructor
26
27
28    return 0;
29}
  • Gördüğünüz gibi m1 oluşturulduktan sonra m2 nesnesini m1 ile oluşturuyorum. Burada copy constructor devreye giriyor.
  • İkinci örneğe baktıktan sonra biraz daha net anlayacağız.
 1// Copy Constructor
 2
 3#include <iostream>
 4
 5using namespace std;
 6
 7class Myclass {
 8public:
 9    Myclass() {
10        cout << "default constructor" << endl;
11    }
12
13    Myclass(const Myclass &r)
14    {
15        cout << "***********************************" << endl;
16        cout << "copy constructor" << endl;
17        cout << "this  = " << this << endl;
18        cout << "&r    = " << &r << endl;
19        cout << "***********************************" << endl;
20    }
21
22};
23
24int main()
25{
26    Myclass m1;
27    Myclass m2{m1};   //C++11
28
29    cout << "&m1  = " << &m1 << endl;
30    cout << "&m2  = " << &m2 << endl;
31
32
33
34    return 0;
35}
36
37/*
38default constructor
39***********************************
40copy constructor
41this  = 0x7ffee3f4c450
42&r    = 0x7ffee3f4c458
43***********************************
44&m1  = 0x7ffee3f4c458
45&m2  = 0x7ffee3f4c450
46*/
  • Burada anlayabilmek için this göstericisi ve adresleri ekrana yazdırdım.

Peki Copy Constructor Yapısı Nasıl?

  • Copy Constructor kullanımını örnekte görmüş olduk. Biraz açıklayalım. Sınıf ismi ile aynı fakat diğer nesneye de ulaşmamız gerekiyor. Nesneye ulaşmam için onu referans yoluyla almam gerekiyor ve nesneyi okuma amacıyla kullanacağım için const olması gerekiyor. Fonksiyonumuzun yapısıda böyle.

Ne zaman Copy Constructor çağırılıyor?

  • Şimdi az önce söylediğimiz gibi bir nesne hayata kendi türünden başka bir nesnenin değerini alarak geldiğinde çağırılıyordu. İşte burada bir soru daha aklımıza geliyor.

Hangi durumlarda Copy Constructor çağırılıyor?

  • 3 tipik senaryo var.
  • Senaryo 1 ~> Açık ilk değer verme, (ilk değer verme çeşitlerini 1. örnekte yazdım)
  • Senaryo 2 -> Bir fonksiyon var, bu sınıfın parametresi bir sınıf türünden. (Dikkat sınıf türünden diyorum, sınıf türünden bir pointer veya referans değil) Bahsettiğim call by value, call by reference değil.

Senaryo 2 için bir örnek verelim.

 1// Copy Constructor
 2#include <iostream>
 3
 4using namespace std;
 5
 6class Myclass {
 7public:
 8    Myclass() {
 9        cout << "default constructor" << endl;
10    }
11
12    Myclass(const Myclass &r)
13    {
14        cout << "***********************************" << endl;
15        cout << "copy constructor" << endl;
16        cout << "this = " << this << endl;
17        cout << "&r = " << &r << endl;
18        cout << "***********************************" << endl;
19    }
20
21};
22
23
24void gfunc(Myclass m)
25{
26    cout << "gfunc cagrildi" << endl;
27    cout << "&m = " << &m << endl;
28    cout << "---------------------------------------------" << endl;
29
30}
31
32
33int main()
34{
35    Myclass m1;
36
37    cout << "&m1 = " << &m1 << endl;
38    gfunc(m1);
39
40
41    return 0;
42}
43
44/*
45default constructor
46&m1 = 0x7ffeeeeb0468
47***********************************
48copy constructor
49this = 0x7ffeeeeb0460
50&r = 0x7ffeeeeb0468
51***********************************
52gfunc cagrildi
53&m = 0x7ffeeeeb0460
54---------------------------------------------
55*/
  • Senaryo 3 -> Biraz daha zor bir senaryo. Çünkü ortada bir nesne görünmüyor :) Ama makina koduna yada assembl koduna bakılacak olursa orada görülecektir. Görünmemesinin sebebi bu isimlendirilmiş bir nesne değil. Bu durumda hayata gelen nesne fonksiyonun geri dönüş değerini tutacak olan geçici nesnedir. O zaman this burada geçici nesne. Fonksiyonlar bir türe geri döndüğünde (int yada int & demek istemiyorum) çağırılan fonksiyonun, çağıran fonksiyona değer iletmesi için bizim görmediğimiz bir nesne hayata geliyor.

Peki bu nesne değerini nereden alıyor?

  • return expression dan alıyor.

Bu geçici nesneye değer atama olarak mı gidiyor yoksa ilk değer olarak mı veriliyor?

  • ilk değer olarak veriliyor.

Senaryo 3 için bir örnek verelim.

 1#include <iostream>
 2
 3using namespace std;
 4
 5class Myclass {
 6public:
 7    Myclass() {
 8        cout << "default constructor" << endl;
 9    }
10
11    ~Myclass() {
12        cout << "destructor" << endl;
13    }
14
15    Myclass(const Myclass &r)
16    {
17        cout << "***********************************" << endl;
18        cout << "copy constructor" << endl;
19        cout << "***********************************" << endl;
20    }
21
22};
23
24
25Myclass g;
26
27Myclass gfunc()
28{
29    return g;
30}
31
32
33int main()
34{
35    cout << "main basliyor" << endl;
36    gfunc();
37    cout << "main bitiyor" << endl;
38
39    return 0;
40}
41
42/*
43default constructor
44main basliyor
45***********************************
46copy constructor
47***********************************
48destructor
49main bitiyor
50destructor
51*/
  • Burada default const. global nesne için çağırıldı. Global olduğu için main den önce çağırıldı.

Peki neden copy constructor çağırıldı, nereden çıktı?

  • Çünkü ortada herhangi bir nesne yok. Anlatmak istediğim nokta burası. Bu örnekte bir destructor yazdık. Anlatmak istediğim konu biraz daha anlaşılıyor hale geliyor. Yani bu örnekte 2 kez destructor çağırılıyor. 2. destructor sizin görmediğiniz nesnenin..

Copy Constructor biz yazmazsak derleyici bizim için bir CC yazacak mı?

  • Evet compiler bizim için bir CC yazacak.
  • Derleyicimizin yazdığı CC non-static inline public üye fonksiyonudur. Tıpkı constructor ve destructor da olduğu gibi.
  • CC bizim tarafımızdan yazılması gereken durumlar var. Durumu basite indirgeyerek şöyle anlatabilirim. Eğer derleyicinin yazdığı destructor ile yetinemiyorsak yani kaynakların geri iadesi için destructor yazıyorsak kesinlikle copy constructor ı biz yazmalıyız. C++11 öncesinde destructor siz yazarsanız CC yazmamanız bir sentaks hatası değil. C++11 de bunu depracated ilan ettiler.
  • Yani kısaca şöyle açıklayabiliriz, destructor var ise demek ki kaynaklar geri verme işi var. O zaman kaynakları release etme gibi bir tema varsa sınıf nesneleri birbirine kopyalandığında biz pointerları yada referansları kopyalamış oluyoruz. Buda bağımsızlık ilkesini tamamen bozar. Bağımsızlığı kendin elde edeceksin. Nasıl olucak bu iş? CC kendin yazarak.
  • Konuyu açıklayan birkaç örnek yazmaya çalışalım. Şöyle bir senaryo oluşturuyorum.
  • Bir tane int türden öğe olsun, ismin uzunluğunu bulsun(m_len).İsmi dinamik bellek alanında tutalım. Constructor m_len e p pointerinin gösterdiği ismin uzunluğunu hesaplasın, m_p pointerinin m_len+1 karakterlik bir heap te bellek alanını edinsin. Bir de kopyalama işlemi var. Bu örnekte CC derleyiciye bırakıyorum. Derleyici ne görürse karşılıklı birbirine kopyalıyor. Bunlara “memberwise copy” yada “shallow copy” deniyor. (Deep copy bunların tersi) Deep copy ile independency yapacağız.
 1// Compiler Default Copy Constructor
 2
 3#include <iostream>
 4#include <cstring>
 5
 6using namespace std;
 7
 8class Name {
 9    int m_len;
10    char *m_p;
11public:
12    Name(const char *p)
13    {
14        m_len = strlen(p);
15        m_p = (char *)malloc(m_len + 1);
16        ////
17        strcpy(m_p, p);
18    }
19
20    ~Name()
21    {
22        free(m_p);
23
24    }
25    void display()const
26    {
27        // ismi ekrana yazdiralim
28        cout << "(" << m_p << ")" << endl;
29    }
30    ////
31};
32
33
34
35int main()
36{
37    Name x{"Kerem Vatandas"};
38    x.display();
39    if (1) {
40        Name y {x}; // CC cagirildi
41        y.display();
42        getchar(); // burada scope bitti, y nesnesinin destructor'i cagiralacak
43    }
44    // kaynak geri verildi ama x nesnesi bunun farkinda degil
45    x.display();
46
47    return 0;
48}
  • Örnekte gördüğümüz gibi bu senaryoda derleyicinin yazdığı CC işimize yaramıyor. Run time hatası alırız. Şimdi CC kendimiz yazıp bağımsızlık oluşturacağım. Şurayı çok iyi anlamamız gerekiyor, biz bir CC yazarsak derleyici bu koda kesinlikle müdahale etmez. Normal örnekte CC yazma nedenim pointer yani aslında m_len in doğrudan kopyalanmasının benim için bir sakıncası yok. Daha karmaşık bir örnekte olabilir di. 20 tane veri elemanı var diyelim ki, bir tanesi için aslında müdahale etmem gerekiyor. Ama 20 elemandan 19 karşıklıklı kopyalanması biz yapacağız.
 1// CC Kendimiz Yazalım
 2
 3#include <iostream>
 4#include <cstring>
 5
 6using namespace std;
 7
 8class Name {
 9    int m_len;
10    char *m_p;
11public:
12    Name(const char *p)
13    {
14        m_len = strlen(p);
15        m_p = (char *)malloc(m_len + 1);
16        ////
17        strcpy(m_p, p);
18    }
19
20    Name(const Name &r)
21    {
22        m_len = r.m_len;
23        m_p = (char *)malloc(m_len + 1);
24        ///
25        strcpy(m_p, r.m_p);
26    }
27
28    ~Name()
29    {
30        free(m_p);
31
32    }
33    void display()const
34    {
35        cout << "(" << m_p << ")" << endl;
36    }
37    ////
38};
39
40
41
42int main()
43{
44    Name x{"Kerem Vatandas"};
45    x.display();
46    if (1) {
47        Name y {x};
48        y.display();
49        getchar();
50    }
51
52    x.display();
53
54    return 0;
55}
  • CC maliyetli bir yapı. Bir nesneye başka bir nesne ile değer vermemiz. Call by value çağrısı, MyClass sınıfına geri dönen fonksiyon vs bunlar ciddi maliyetler. Senaryoyu biraz abartacak olursak, name olmasında matris olsun(matrislerde dev gibi olsun :D ). Hadi diğer kaynakları bıraktık, heapteki kaynak o kadar büyük olur ki, her CC belki onbinlerce bytelık bellek alanının birbirine kopyalanması anlamına gelir. Ama diğer taraftan da bunu yapmak zorundayım. Çünkü runtime hatası olması daha mı iyi? … Yani CC bazı durumlarda maliyet artabilir. Özel bir senaryo uyduralım, kopyalama yaptığım x nesnesinin hayatının kesinlikle biteceğini biliyorsam ben kopyalama yerine o adresi devralırım. (Move semantiği -> r_value ref.)

Atama Operator Fonksiyonu

 1// Copy Assignment Operator
 2#include <iostream>
 3#include <cstring>
 4
 5using namespace std;
 6
 7class Name {
 8    int m_len;
 9    char *m_p;
10public:
11    Name(const char *p)
12    {
13        m_len = strlen(p);
14        m_p = (char *)malloc(m_len + 1);
15        ////
16        strcpy(m_p, p);
17    }
18
19    Name(const Name &r)
20    {
21        m_len = r.m_len;
22        m_p = (char *)malloc(m_len + 1);
23        ///
24        strcpy(m_p, r.m_p);
25    }
26
27    Name &operator=(const Name &r)
28    {
29        if (this == &r)
30            return *this;
31
32        free(m_p);
33
34        m_len = r.m_len;
35        m_p = (char *)malloc(m_len + 1);
36        ///
37        strcpy(m_p, r.m_p);
38
39        return *this;
40    }
41
42    ~Name()
43    {
44        free(m_p);
45
46    }
47    void display()const
48    {
49        cout << "(" << m_p << ")" << endl;
50    }
51    ////
52};
53
54
55int main()
56{
57    Name x{"Kerem Vatandas"};
58
59    x = x;
60
61
62    x.display();
63
64
65    return 0;
66}
 1// Copy Assignment Operator
 2#include <iostream>
 3#include <cstring>
 4
 5using namespace std;
 6
 7class Name {
 8    int m_len;
 9    char *m_p;
10    void releaseResources()
11    {
12        free(m_p);
13    }
14    Name &deepCopy(const Name &r)
15    {
16        m_len = r.m_len;
17        m_p = (char *)malloc(m_len + 1);
18        ///
19        strcpy(m_p, r.m_p);
20        return *this;
21    }
22
23public:
24    Name(const char *p)
25    {
26        m_len = strlen(p);
27        m_p = (char *)malloc(m_len + 1);
28        ////
29        strcpy(m_p, p);
30    }
31
32    Name(const Name &r)
33    {
34        deepCopy(r);
35    }
36
37    Name &operator=(const Name &r)
38    {
39        if (this == &r)
40            return *this;
41
42        releaseResources();
43
44        return deepCopy(r);
45    }
46
47    ~Name()
48    {
49        releaseResources();
50    }
51    void display()const
52    {
53        cout << "(" << m_p << ")" << endl;
54    }
55    ////
56};
57
58
59
60int main()
61{
62    Name x{"Kerem Vatandas"};
63
64    x = x;
65
66    x.display();
67
68
69    return 0;
70}

Atama Operator Fonksiyonu ile CC ortak parçası “deep copy”.

  • Destructor ile Atama Operatör Fonksiyonunun ortak noktası her ikisi de kaynakları iade ediyor.
  • Buradan şu ortaya çıkıyor, eğer bir sınıf için destructor yazmışsak CC yazmalıyız aynı zamanda atama operator fonksiyonunu da yazmalıyız.
  • C++ en meşhur terimlerinden biri “Big Three(Büyük üçlü)”.

Neye büyük üçlü deniyor?

  • destructor, copy constructor, copy assignment operator oluşturduğu üçlüye. Birimiz varsak diğerlerimizde olucak diyor :)

Kaynak: Rule of Three

  • Kaynakta yazıyor artık C++11 ile bu arttır. Rule of Five
1destructor
2copy constructor
3move constructor
4copy assignment operator
5move assignment operator
  • Move semantiğini ve Perfect Forwarding gerçekleştirmek için C++11 standartlarında sağ taraf referansı diye bir araç eklendi. Eskiden referans referanstı. Şimdi önce ki referans dediklerimiz şimdi sol taraf referansı olarak anılmaya başlandı. (Lvalue reference)
  • iki && işareti ile belirtilen referansa rvalue(sağ taraf) referansı deniyor.

Kaynak: Lvalue, Rvalue ..

  • Bir sınıfın constructor parametresi sağ taraf değeri olabilir.
  • Sağ taraf referansı parametreli olan taşıma semantiğini yapıcak.
  • Sol taraf referansı parametreli olan kopyalama semantiğini yapıcak.
 1// Rvalue & Lvalue
 2
 3class Name {
 4    int m_len;
 5    char *m_p;
 6
 7public:
 8    Name(const char *p)
 9    {
10        m_len = strlen(p);
11        m_p = (char *)malloc(m_len + 1);
12        ////
13        strcpy(m_p, p);
14    }
15
16    Name(Name &&r)
17    {
18        m_len = r.m_len;
19        mp = r.m_p;
20        r.m_p = nullptr;
21
22    }
23
24    Name(const Name &r)
25    {
26        m_len = r.m_len;
27        m_p = (char *)malloc(m_len + 1);
28        ///
29        strcpy(m_p, r.m_p);
30    }
31
32    Name &operator=(Name &&r)
33    {
34        if (this == &r)
35            return *this;
36        m_len = r.m_len;
37        m_p = r.m_p;
38        r.m_p = nullptr;
39    }
40
41    Name &operator=(const Name &r)
42    {
43        if (this == &r)
44            return *this;
45
46        free(m_p);
47
48        m_len = r.m_len;
49        m_p = (char *)malloc(m_len + 1);
50        ///
51        strcpy(m_p, r.m_p);
52
53        return *this;
54    }
  • Evet burada hangisinin çağırıldığını derleyeci belirleyecek, function overloading kurallarına göre.
  • Konu da ek olarak değindiğimiz move semantiği, rvalue .. gibi konuları ayrı bir başlık altında anlatmaya çalışacağım.