C++拷貝控制與資源管理
參考《C++ Primer》P452 拷貝控制與資源管理
當一個類需要自定義析構函數,那麼它幾乎肯定也需要定義拷貝構造函數和拷貝複製運算符。 如下例子:
class HasPtr {
public:
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)),i(0) {}
~HasPtr() {delete ps;}
//此時需要定義一個拷貝構造函數和一個拷貝賦值運算符
};
如果我們沒有定義拷貝構造函數和拷貝賦值運算符,那麼會調用類的合成拷貝構造函數和合成拷貝賦值運算符,這些函數簡單拷貝指針成員,意味着多個HasPtr對象可能指向相同的內存。
HasPtr f(HasPtr hp)
{
HasPtr ret = hp; //拷貝給定的HasPtr
//處理ret
return ret; //ret和hp被銷燬
}
當f返回時,hp和ret都被銷燬,在這兩個對象上都會調用HasPtr的析構函數,析構函數會調用delete ret和hp的指針成員,但這兩個指針指向相同的內存塊,則導致了指針被delete兩次的錯誤。
定義行爲像值的類
爲了提供類值的行爲,需要:
- 定義一個拷貝構造函數,完成string的拷貝,而不是指針拷貝;
- 定義一個析構函數來釋放string;
- 定義一個拷貝複製運算符來釋放對象當前的string,並從右側運算對象拷貝string;
clas HasPtr {
public:
HasPtr(const std::string &s=std::string()):
ps(new std::string(s)),i(0) {}
HasPtr(const HasPtr &p):
ps(new std::string(*p.ps)),i(p.i) {} //拷貝string的值,而不是ps指針
HasPtr& operator=(const HasPtr&);
~HasPtr() {delete ps;}
private:
std::string *ps;
int i;
};
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
auto newp = string(*rhs.ps); //首先將右側運算對象拷貝到局部臨時變量中。
delete ps; //再刪除左側原始成員
ps = newp; //賦予新的成員值
i = rhs.i;
return *this; //返回對象本身
}
需要注意的是:
如果將一個對象賦予它自身,賦值運算符必須保證正確。
大多數賦值運算符組合了析構函數和拷貝構造函數的工作。
定義行爲像指針的類
相比值行爲類,指針行爲類需要拷貝的是指針,會出現多個對象指向同一內存塊的行爲,則我們需要加入引用計數,計數器需要放置在動態內存中,當創建一個新的對象時,我們分配一個新的計數器,當拷貝或賦值對象時,我們拷貝指向計數器的指針。
class HasPtr{
public:
HasPtr(const string &s=string()):
ps(new string(s)),i(0),use(new size_t(1)){}
HasPtr(const HasPtr &p): //拷貝指針,計數器加1
ps(p.ps),i(p.i),use(p.use){++ *use}
HasPtr& operator=(const HasPtr &);
~HasPtr();
private:
string *ps;
int i;
size_t *use;
};
HasPtr::~Hasptr() {
if(-- *use==0) { //計數器減1,如果引用計數變爲0,則釋放string和計數器內存
delete ps;
delete use;
}
}
HasPtr& HasPtr::operator=(const HasPtr &rhs) {
++ *rhs.user; //右值對象計數器加1
if(--*use==0) { //左值對象計數器減1,如果計數器爲0,則釋放內存
delete ps;
delete use;
}
ps = rhs.ps;
i = rhs.i;
use = rhs.use;
return *this;
}