C++ 深复制与浅复制 RVO问题
前言
內(nèi)容主要是深淺復(fù)制、復(fù)制構(gòu)造函數(shù)以及賦值運(yùn)算符的問(wèn)題。
先從一段簡(jiǎn)單的代碼開(kāi)始:
#include <iostream> #include <string.h> using namespace std;class Student {private:char *name;int number;public:Student(char *na, int n);~Student();setnumber(int n);setname(char *na);void Print(); };Student::Student(char *na, int n) {name = new char[strlen(na)+1];strcpy(name, na);number = n;cout << "Student:" << (int)name << " Number:" << number <<endl; }Student::~Student() {cout << "~Student:" << (int)name << " Number:" << number <<endl;delete name; }Student::setnumber(int n) {number = n; }Student::setname(char *na) {strcpy(name, na); }void Student::Print() {cout << "student:" << name << " " << "Number:" << number <<endl; }int main() {Student s1((char*)"Tom", 123);Student s2(s1);//復(fù)制對(duì)象cout <<endl;s2.setname((char*)"Bob");s2.setnumber(456);s1.Print();s2.Print();cout <<endl;return 0; }細(xì)細(xì)分析一下上述代碼中的 "TOM" 為什么會(huì)變成 "Bob"??,可以發(fā)現(xiàn)當(dāng)用s1去初始化s2時(shí),s1的name的地址也復(fù)制給了s2。那么自然而然,當(dāng)修改s2的name時(shí),s1的name也跟著變化。這對(duì)Student對(duì)象的使用造成了麻煩,而且還會(huì)帶來(lái)別的問(wèn)題。
這里先給出解決辦法:
Student::Student(const Student &st) {int len = strlen(st.name);name = new char[len + 1];strcpy(name, st.name); }至于為什么這么做,我們需要接著看下面的內(nèi)容。
?
上面的程序只是在拋磚引玉,接下來(lái)的內(nèi)容是《C++ Primer Plus》中給出的程序代碼。通過(guò)下面的代碼,能讓我們發(fā)現(xiàn)很多潛在的問(wèn)題。
#include <iostream> #include <cstring> #include <string.h> using namespace std;class StringBad { private:char *str;int len;static int num_strings; public:StringBad(const char *s);StringBad();~StringBad();friend std::ostream & operator << (std::ostream & os, const StringBad & st); };int StringBad::num_strings = 0;StringBad::StringBad(const char *s) {len = strlen(s);str = new char[len + 1];strcpy(str, s);num_strings++;cout << num_strings << ": \"" << str << "\" object created\n"; }StringBad::StringBad() {len = 4;str = new char[4];strcpy(str, "C++");num_strings++;cout << num_strings << ": \"" << str << "\" default object created\n"; }StringBad::~StringBad() {cout << "\"" << str << "\" object deleted, ";--num_strings;cout << num_strings << " left\n";delete []str; }ostream & operator << (ostream & os, const StringBad &st) {os << st.str;return os; }void callme1(StringBad &); void callme2(StringBad);int main() {cout << "String an inner block.\n";StringBad headline1("asd");StringBad headline2("qwe");StringBad sports("zxc123");cout << "headline1: " << headline1 <<endl;cout << "headline2: " << headline2 <<endl;cout << "sports: " << sports <<endl;callme1(headline1);cout << "headline1: " << headline1 <<endl;callme2(headline2);cout << "headline2: " << headline2 <<endl;cout << "Initialize one object to another:\n";StringBad sailor = sports;cout << "sailor: " << sailor <<endl;cout << "Assign one object to another:\n";StringBad knot;knot = headline1;cout << "knot: " << knot <<endl;cout << "Exiting the block.\n";return 0; }void callme1(StringBad &rsb) {cout << "String passed by reference:\n";cout << " \"" << rsb << "\"\n"; }void callme2(StringBad sb) {cout << "String passed by value:\n";cout << " \"" << sb << "\"\n"; }這里還要說(shuō)明的一點(diǎn)是:我用CodeBlocks運(yùn)行上述代碼,沒(méi)有達(dá)到書(shū)本分析的預(yù)期結(jié)果。具體情況是臨時(shí)對(duì)象調(diào)用了析構(gòu)函數(shù)釋放相應(yīng)內(nèi)存后,還可以獲取內(nèi)存中的值。查閱資料后發(fā)現(xiàn)原因是:RVO(return value optimization),被C++進(jìn)行值返回的優(yōu)化。有的軟件可以關(guān)閉RVO,因?yàn)槲也惶宄﨏odeBlocks如何關(guān)閉它。所以我將代碼放到了Linux系統(tǒng)下運(yùn)行。將RVO優(yōu)化關(guān)閉,可以對(duì)g++增加選項(xiàng)-fno-elide-constructors,重新編繹之后,執(zhí)行結(jié)果如下:
關(guān)閉RVO后,一下子就發(fā)現(xiàn)問(wèn)題了。
這里有一個(gè)問(wèn)題是因?yàn)楫?dāng)時(shí)忘寫(xiě)knot = headline1;這一條賦值語(yǔ)句 導(dǎo)致"Exiting the block.下面出現(xiàn)的是 "C++" object deleted, 2 left而不是"asd" object deleted, 2 left程序第一個(gè)問(wèn)題是輸出中出現(xiàn)的各種非標(biāo)準(zhǔn)字符隨系統(tǒng)而異,另一個(gè)問(wèn)題是對(duì)象計(jì)數(shù)為負(fù)。程序開(kāi)始時(shí)還是正常的,但逐漸變得異常,最終導(dǎo)致了災(zāi)難性結(jié)果。可以看出到headline1傳遞給callme1()函數(shù),并在調(diào)用后重新顯示headline1。
這一塊代碼運(yùn)行都是正常的:
String passed by reference:"asd" headline1: asd但隨后程序?qū)eadline2傳遞給了callme2,出現(xiàn)了一個(gè)嚴(yán)重的問(wèn)題:
String passed by value:"qwe" "qwe" object deleted, 2 left headline2:首先,將headline2作為函數(shù)參數(shù)來(lái)傳遞給函數(shù),導(dǎo)致析構(gòu)函數(shù)被調(diào)用。其次,雖然按值傳遞可以防止原始參數(shù)被修改,但實(shí)際上函數(shù)已使原字符串無(wú)法識(shí)別,導(dǎo)致顯示一些非標(biāo)準(zhǔn)字符(顯示的內(nèi)容取決于內(nèi)存中所包含的內(nèi)容)。
在為每一個(gè)創(chuàng)建的對(duì)象自動(dòng)調(diào)用析構(gòu)函數(shù)時(shí),情況更糟糕:
上面的計(jì)數(shù)異常是一條線索,因?yàn)槊總€(gè)對(duì)象被構(gòu)造和析構(gòu)一次,因此調(diào)用構(gòu)造函數(shù)的次數(shù)應(yīng)當(dāng)與析構(gòu)函數(shù)的調(diào)用次數(shù)相同。對(duì)象計(jì)數(shù)遞減的次數(shù)比遞增的次數(shù)多2,這表明使用了不將num_string遞增的構(gòu)造函數(shù)創(chuàng)建了兩個(gè)對(duì)象。此時(shí)使用的不是默認(rèn)的構(gòu)造函數(shù),也不是參數(shù)為const char *的構(gòu)造函數(shù),而是復(fù)制構(gòu)造函數(shù)。
?
復(fù)制構(gòu)造函數(shù)用于將一個(gè)對(duì)象復(fù)制到新創(chuàng)建的對(duì)象中。也就是說(shuō)它用于初始化過(guò)程中,而不是常規(guī)的賦值過(guò)程中。
類的復(fù)制構(gòu)造函數(shù)原型通常如下:
Class_name(const Class_name &);它接受一個(gè)指向類對(duì)象的常量引用作為參數(shù)。例如:
class StringBad { private:char *str;int len;static int num_strings; public:StringBad(const char *s);StringBad();StringBad(const StringBad &); //復(fù)制構(gòu)造函數(shù)~StringBad();friend std::ostream & operator << (std::ostream & os, const StringBad & st); };新建一個(gè)對(duì)象并將其初始化為同類現(xiàn)有的對(duì)象時(shí),復(fù)制構(gòu)造函數(shù)都將被調(diào)用。假設(shè)motto是一個(gè)StringBad對(duì)象,則下面4種聲明將調(diào)用復(fù)制構(gòu)造函數(shù):
StringBad ditto(motto); StringBad metoo = mott; StringBad also = StringBad (motto); StringBad * pStringBad = new StringBad(motto);其中中間的2種聲明可能會(huì)使用復(fù)制構(gòu)造函數(shù)直接創(chuàng)造metoo和also,也可能使用復(fù)制構(gòu)造函數(shù)生成一個(gè)臨時(shí)對(duì)象,然后將臨時(shí)對(duì)象的內(nèi)容賦值給metoo和also,這取決于具體實(shí)現(xiàn)。每當(dāng)程序生成了對(duì)象副本時(shí),編譯器都將使用復(fù)制構(gòu)造函數(shù)。具體地說(shuō),當(dāng)函數(shù)按值傳遞對(duì)象或函數(shù)返回對(duì)象時(shí),都將使用復(fù)制構(gòu)造函數(shù)。
調(diào)用復(fù)制構(gòu)造函數(shù)總結(jié):
(1)用對(duì)象去初始化另一個(gè)對(duì)象。
(2)函數(shù)的參數(shù)是類對(duì)象(值傳遞)。
(3)返回值是類對(duì)象。
?
上述代碼存在的問(wèn)題
(1)默認(rèn)的復(fù)制構(gòu)造函數(shù)不說(shuō)明其行為(逐個(gè)賦值非靜態(tài)成員,只是復(fù)制成員的值,成員復(fù)制也稱為淺復(fù)制),因?yàn)樗恢赋鰟?chuàng)建過(guò)程,也不增加計(jì)數(shù)器num_strings的值。但析構(gòu)函數(shù)更新了計(jì)數(shù),并且在任何對(duì)象過(guò)期時(shí)都將被調(diào)用,而不管對(duì)象是如何被創(chuàng)建的。這就導(dǎo)致了程序無(wú)法準(zhǔn)確地記錄對(duì)象的個(gè)數(shù)。
(2)就像開(kāi)頭的小程序一樣,程序復(fù)制的不是字符串,而是一個(gè)指向字符串的指針。也就是說(shuō),將sailor初始化為sports后,會(huì)有兩個(gè)指向同一個(gè)字符串的指針。當(dāng)析構(gòu)函數(shù)被調(diào)用時(shí),str指針?biāo)赶虻膬?nèi)存將被釋放。此時(shí),另一個(gè)對(duì)象再用str指針去訪問(wèn)這塊區(qū)域,必然導(dǎo)致不確定的、可能有害的后果。
(3)最后一個(gè)問(wèn)題是,試圖釋放內(nèi)存兩次可能導(dǎo)致程序異常終止(不同系統(tǒng)提供的信息不同)。
?
定義一個(gè)顯式復(fù)制構(gòu)造函數(shù)以解決問(wèn)題
解決類設(shè)計(jì)中的這種問(wèn)題的方法是進(jìn)行深度復(fù)制(deep copy)。也就是說(shuō),復(fù)制構(gòu)造函數(shù)應(yīng)當(dāng)復(fù)制字符串并將副本的地址賦值給str成員,而不是僅僅復(fù)制字符串地址。
StringBad::StringBad(const StringBad & st) {num_strings++;len = st.len;str = new char[len + 1];strcpy(str, st.str);cout << num_strings << ": \"" << str << "\" object created\n"; }這時(shí)程序可以打印出headline2的值、字符串不亂碼以及計(jì)數(shù)恢復(fù)正常。但是最后一條的字符串不應(yīng)該為空,這又是一個(gè)問(wèn)題。
?
StringBad的其他問(wèn)題:賦值運(yùn)算符
(1)賦值運(yùn)算符的功能以及何時(shí)使用它
StringBad headline1(“asd”); ... StringBad knot; knot = headline1; 初始化對(duì)象時(shí),不一定會(huì)使用賦值運(yùn)算符 StringBad metoo = knot; ??//使用復(fù)制構(gòu)造函數(shù)與復(fù)制構(gòu)造函數(shù)相似,賦值運(yùn)算符的隱式實(shí)現(xiàn)也對(duì)成員進(jìn)行逐個(gè)復(fù)制。如果成員本身就是類對(duì)象,則程序?qū)⑹褂脼檫@個(gè)類定義的賦值運(yùn)算符來(lái)復(fù)制該成員,但靜態(tài)數(shù)據(jù)成員不受影響。
?
(2)賦值的問(wèn)題
上述程序中將headline1賦值給了knot:
knot = headline1;為knot調(diào)用析構(gòu)函數(shù)時(shí),knot.str所指向的區(qū)域?qū)⒈会尫拧D敲淳蜁?huì)出現(xiàn)和隱式復(fù)制構(gòu)造函數(shù)一樣的結(jié)果:數(shù)據(jù)受損。以及試圖刪除已經(jīng)刪除的數(shù)據(jù)導(dǎo)致的結(jié)果是不確定的,因此可能會(huì)改變內(nèi)存中的內(nèi)容,導(dǎo)致程序異常終止。
?
(3)解決賦值問(wèn)題
對(duì)于由與默認(rèn)賦值運(yùn)算符不合適而導(dǎo)致的問(wèn)題,解決辦法是提供賦值運(yùn)算符(進(jìn)行深度復(fù)制)定義。
StringBad & StringBad::operator = (const StringBad & st) {if (this == &st)return *this;delete []str;len = st.len;str = new char[len + 1];strcpy(str, st.str);return *this; }代碼首先檢查自我復(fù)制,這是通過(guò)查看賦值運(yùn)算符右邊的地址是否與接受對(duì)象的地址相同來(lái)完成的。如果相同,程序返回*this,然后結(jié)束。如果地址不同,函數(shù)將釋放str指向的內(nèi)存,這是因?yàn)樯院髮岩粋€(gè)新字符串的地址賦給str。賦值操作并不創(chuàng)建新的對(duì)象,因此不需要調(diào)整靜態(tài)數(shù)據(jù)成員num_strings的值。通過(guò)修改程序,上述代碼存在的3個(gè)問(wèn)題都得到了解決。最終的運(yùn)行結(jié)果如下:
總結(jié):深復(fù)制開(kāi)辟新的空間,而淺復(fù)制沒(méi)有。
總結(jié)
以上是生活随笔為你收集整理的C++ 深复制与浅复制 RVO问题的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: C/C++数组指针和指针数组
- 下一篇: C/C++运算符