关于C++拷贝控制
通常來說,對于類內動態分配資源的類需要進行拷貝控制:要在拷貝構造函數、拷貝賦值運算符、析構函數中實現安全高效的操作來管理內存。但是資源管理并不是一個類需要定義自己的拷貝控制成員的唯一原因。C++ Primer 第5版 中給出了一個Message類與Folder類的例子,分別表示電子郵件消息和消息目錄。每個Message可以出現在多個Folder中,但是,任意給定的Message的內容只有一個副本。如果一條Message的內容被改變,我們從任意的Folder中看到的該Message都是改變后的版本。為了記錄Message位于哪些Folder中,每個Message都用一個set保存所在的Folder的指針,同樣的,每個Folder都用一個set保存它包含的Message的指針。二者的設計如下圖所示:
C++ Primer中并沒有給出Folder類的實現。在對Message及Folder類的復現過程中,出現了一個問題,導致了嚴重錯誤。
Message及Folder類的初步設計如下:
Message類:
class Message
{
friend class Folder;
private:
string contents;
set<Folder*> folders; //功能函數:在本消息的folders列表中加入/刪除新文件夾指針f
void addFolder(Folder* f);
void remFolder(Folder* f); //功能函數:在本消息folders列表中的所有Folder中刪除指向此消息的指針
void remove_from_folders(); public:
string getContents();
set<Folder*> getFolders(); //構造函數與拷貝控制
Message(const string& s = " ") :contents(s) {};
~Message(); //接口:將本消息存入給定文件夾f
void save(Folder& f);
//接口:將本消息在給定文件夾中刪除
void remove(Folder& f);
};
Folder類:
class Folder
{
friend class Message;
private:
set<Message*> messages; //功能函數:將給定消息的指針添加到本文件夾的messages中
void addMsg(Message* m);
//功能函數:將給定消息的指針在本文件夾中的messages中刪除
void remMsg(Message* m); public:
set<Message*> getMessages();
};
這兩個類有對稱的功能函數:Message.addFolder(Folder* f)與Folder.addMsg(Message* m),以及Message.remFolder(Folder* f)與Folder.remMsg(Message* m),用來實現Message的保存以及拷貝控制操作等。
所有成員函數的實現如下:
string Message::getContents()
{
return contents;
}
set<Folder*> Message::getFolders()
{
return folders;
} void Message::addFolder(Folder* f)
{
this->folders.insert(f);
}
void Message::remFolder(Folder* f)
{
this->folders.erase(f);
} //接口:將本消息存入給定文件夾f
void Message::save(Folder& f)
{
this->addFolder(&f);
f.addMsg(this);
}
//接口:將本消息在給定文件夾中刪除
void Message::remove(Folder& f)
{
this->remFolder(&f);
f.remMsg(this);
} void Message::remove_from_folders()
{
for (auto f : folders)
{
f->remMsg(this);
}
} Message::~Message()
{
remove_from_folders();
} /*Folder的成員函數*/
//功能函數:將給定消息的指針添加到本文件夾的messages中
void Folder::addMsg(Message* m)
{
messages.insert(m);
}
//功能函數:將給定消息的指針在本文件夾中的messages中刪除
void Folder::remMsg(Message* m)
{
messages.erase(m);
} set<Message*> Folder::getMessages()
{
return messages;
}
在這個實現版本的代碼測試中,出現了這樣一個問題:程序會有運行時錯誤,主函數的返回值不為0。測試代碼如下:
void test()
{
Message m1("Hello,"), m2("World"), m3("!");
Folder f1, f2;
m1.save(f1); m1.save(f2);
m2.save(f2);
m3.save(f2);
m2.remove(f2);
} int main()
{
test();
system("pause");
return 0;
}
運行結果:
經調試排查原因之后,找到了問題所在:試圖對已經被的銷毀對象的指針進行解引用。該bug和“函數返回指向局部變量的指針”所導致的問題類似。我們為Message類定義了析構函數:
Message::~Message()
{
remove_from_folders();
}
這個析構函數的實現與C++ Primer上的實現完全一致。該析構函數意圖在于當一個Message被銷毀時,應該清除它的folders中的所有指向它的指針。這看上去合理,可是在這里卻導致了內存錯誤。原因在于,remove_from_folders()操作會訪問該Message所在的所有Folder的指針,而若這些Folder的銷毀在該Message的銷毀之前進行,則操作會試圖通過指針解引用,來訪問已被銷毀的Folder對象。這會導致嚴重的運行時錯誤。在本例中,局部變量Folder f1的創建在m1之后,將m1加入f1,test()函數結束時,按照局部變量的銷毀順序,會先銷毀后創建的對象f1,于是,m1的析構函數會試圖解引用已被銷毀對象f1的指針。出現這個問題,是因為在實現的時候沒有按照C++ Primer上的設計正確地實現Folder的析構函數。我們按照如下實現Folder的析構函數:
class Folder
{
/*其他Folder的聲明不變*/ /*加入Folder的析構函數,以及一個工具函數,對于將要銷毀的Folder,這個工具函數負責刪除該Folder中所有Message指向它的指針*/
private:
void remove_from_messages();
public:
~Folder();
}; void Folder::remove_from_messages()
{
for (auto m : messages)
m->remFolder(this);
} Folder::~Folder()
{
remove_from_messages();
}
此時,Folder的析構函數在Folder被銷毀時可以正確地刪除所有Message中指向自身的指針,就避免了對已經銷毀的對象進行解引用的操作。反過來,若先定義的是f1,后定義的是m1,在m1先銷毀時,m1的析構函數也可以正確地刪除所有Folder中指向m1的指針。所以,無論Folder先被銷毀,還是Message先被銷毀,都能夠正確地執行析構操作。使用與上面同樣的test()函數進行測試,程序可以正常地退出了:
這個例子也給了我們又一次提醒:在C++中,指針與拷貝控制、內存管理一定要萬分小心謹慎,一點小的差錯也可能導致程序的災難。
總結
- 上一篇: 基于Apache Hudi和Debezi
- 下一篇: 浏览器对HTML5特性检測工具Moder