深度解析C++拷貝構造函數


自2003年開始,斷斷續續用了12年C++,直到這兩年做物聯網嵌入式開發,感覺對C++的掌握僅有10%左右。
習慣了C#開發,C++倒顯得難以下手!今天就一個函數返回問題跟輝月兄弟討論一番,大有所獲,足以解決我們目前80%的問題,感覺對C++的掌握上升到了20%。

背景,現有字節數組ByteArray和字符串String,(不要激動,單片機嵌入式C++很難用起來標准類庫)
我們需要實現函數String& ByteArray::ToHex()
其實這是我們在C#上非常常用的函數,把一個字節數組轉為字符串,然后別的地方使用或者顯示出來。C#原型String ToHex(this Byte[] buf)
這里有一個老大難題:
1,如果ToHex內部棧分配字符串空間,把字節數組填充進去,那么離開ToHex的時候棧回收,對象數據無效
2,如果ToHex內部堆分配空間,字節數組填充,離開ToHex的時候得到指針。但是這樣違背了C/C++誰申請誰釋放的原則,其它小伙伴使用ToHex的時候可能忘了釋放
3,最后只能折中,做成String& ByteArray::ToHex(String& str); 別提多憋屈!最受不了的是,外部分配str的時候,還得考慮數組有多長!這些本來最好由ToHex內部解決的問題。

總之,這個問題就這樣折騰了我12年!

知道今天,跟輝月兄弟聊起這個問題,他也有十多年C++歷史,用得比我要多一些。他有一段常用代碼大概如下:

CString Test()
{
        CString a = "aaaa";
        CString b = "bbbb";
        CString c = a + b;

        return c;
}

按他說法,就這樣子寫了十多年!
我說c不是棧分配嗎?離開的時候會被析構吧,外部怎么可能拿到?他說是哦,從來沒有考慮過這個問題。
我們敏銳的察覺到,C++一定可以實現類似的做法,因為字符串相加就是最常見的例子。

經過一番探討,我們發現關鍵點出在拷貝構造函數上面

測試環境:編譯器Keil MDK 5.14,處理器STM32F407VG

1、進出兩次拷貝
做了一個測試代碼,兩次調用拷貝構造函數

class A
{
public:
        char* str;

    A(char* s)
    {
                str = s;
        debug_printf("A %s 0x%08X\r\n", str, this);
    }
        A(const A &a)
        {
        debug_printf("A.Copy %s 0x%08X => %s 0x%08X\r\n", a.str, &a, str, this);
        }
    ~A()
    {
        debug_printf("~A %s 0x%08X\r\n", str, this);
    }
};

class B : public A
{
public:
    B(char* s) : A(s)
    {
        debug_printf("B %s 0x%08X\r\n", str, this);
    }
        B(const B &b) : A(b.str)
        {
        debug_printf("B.Copy %s 0x%08X => %s 0x%08X\r\n", b.str, &b, str, this);
        }
    ~B()
    {
        debug_printf("~B %s 0x%08X\r\n", str, this);
    }
        B& operator=(const B &b)
        {
        debug_printf("B.Assign %s 0x%08X => %s 0x%08X\r\n", b.str, &b, str, this);
                return *this;
        }
};

B fun(B c)
{
        c.str = "c";
    return c;
}

void CtorTest()
{
        B a("a"), b("b");
        debug_printf("start \r\n");
    b = fun(a);
        debug_printf("end \r\n");
}

執行結果如下:

A a 0x2001FB78
B a 0x2001FB78
A b 0x2001FB74
B b 0x2001FB74
start 
A a 0x2001FB7C
B.Copy a 0x2001FB78 => a 0x2001FB7C
A c 0x2001FB80
B.Copy c 0x2001FB7C => c 0x2001FB80
B.Assign c 0x2001FB80 => b 0x2001FB74
~B c 0x2001FB80
~A c 0x2001FB80
~B c 0x2001FB7C
~A c 0x2001FB7C
end 
~B b 0x2001FB74
~A b 0x2001FB74
~B a 0x2001FB78
~A a 0x2001FB78
  • 進入func的時候,參數進行了一次拷貝,c構造,也就是7C,然后a拷貝給c
  • 離開func的時候,產生了臨時對象80,並把7C拷貝給80
  • func返回值賦值給b,也就是臨時對象80賦值給74
  • 然后才是80和7C的析構。
  • 那么關鍵點就在於這個臨時對象,它的作用域橫跨函數內部和調用者,自然不怕析構回收。
  • 不過奇怪的是,內部參數7C為何在外面析構??



2、進去拷貝出來引用
修改func函數,返回引用,少一次拷貝構造

B& fun(B c)
{
        c.str = "c";
    return c;
}

執行結果如下:

A a 0x2001FB70
B a 0x2001FB70
A b 0x2001FB6C
B b 0x2001FB6C
start 
A a 0x2001FB74
B.Copy a 0x2001FB70 => a 0x2001FB74
B.Assign c 0x2001FB74 => b 0x2001FB6C
~B c 0x2001FB74
~A c 0x2001FB74
end 
~B b 0x2001FB6C
~A b 0x2001FB6C

~A a 0x2001FB70
  • 進去的時候參數來了一次拷貝構造74
  • 出來的時候74直接賦值給6C,也就是b。看樣子,按引用返回直接省去了臨時對象。
  • 但是上面這個代碼編譯會有一個警告,也就是返回本地變量的引用。
  • 賦值以后,內部對象74才被析構
  • 雖然有警告,但是對象還沒有被析構,外面可以使用。按理說每個線程都有自己的棧,不至於那么快被別的線程篡改數據。但是很難說硬件中斷函數會不會用到那一塊內存。
  • 這里有個非常奇怪的現象,沒有見到70的B析構,不知道是不是串口輸出信息太快,丟失了這一部分數據,嘗試了幾次都是如此。


3、引用進去引用出來
修改參數傳入引用,再少一次拷貝構造

B& fun(B& c)
{
        c.str = "c";
    return c;
}

執行結果如下:

A a 0x2001FB88
B a 0x2001FB88
A b 0x2001FB84
B b 0x2001FB84
start 
B.Assign c 0x2001FB88 => b 0x2001FB84
end 
~B b 0x2001FB84
~A b 0x2001FB84
~B c 0x2001FB88
~A c 0x2001FB88
  • 更加徹底,沒有任何拷貝構造函數被執行
  • 並且沒有“返回本地變量引用”的警告


End


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM