自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