C++ 类的行为 | 行为像值的类、行为像指针的类、swap函数处理自赋值
文章目錄
- 概念
- 行為像值的類
- 行為像指針的類
- 概念
- 引用計數(shù)
- 動態(tài)內(nèi)存實(shí)現(xiàn)計數(shù)器
- 類的swap
- 概念
- swap實(shí)現(xiàn)自賦值
概念
行為像值的類和行為像指針的類這兩種說法其實(shí)蠻拗口的,這也算是 《C++Primer》 翻譯的缺點(diǎn)之一吧。。。
其實(shí)兩者的意思分別是:
- 行為像值的類: 每個類的對象都有自己的實(shí)現(xiàn)
- 行為像指針的類: 所有類的對象共享類的資源(類似于 shared_ptr 智能指針,每有一個對象持有該資源則引用計數(shù)+1,每有一個對象釋放該資源則引用計數(shù)-1,引用計數(shù)為0時釋放內(nèi)存)
本篇博客的內(nèi)容跟 類 和 智能指針 兩篇博客有關(guān)。不了解的同學(xué)可以先看看這兩篇博客。
行為像值的類
對于類管理的資源,每個對象都應(yīng)該有一份自己的拷貝(實(shí)現(xiàn))。如下面的 string類型的指針 ,使用拷貝構(gòu)造函數(shù) or 賦值運(yùn)算符時,每個對象拷貝的都是 指針成員ps 指向的 string 而非 ps本身 。換言之,每個對象 都有一個ps 而不是 給ps加引用計數(shù)。
class A {int i = 0;string* ps; public:A(const string &s = string()): ps(new string(s)), i(0) {}A(const A &a): ps(new string(*a.ps)), i(a.i) {}A& operator=(const A&);~A() { delete ps; } };A& A::operator=(const A& a) {string* newps = new string(*a.ps); // 將a.ps指向的值拷貝到局部臨時對象newps中delete ps; // 銷毀ps指向的內(nèi)存,避免舊內(nèi)存泄漏ps = newps; i = a.i;return *this; // 返回此對象的引用 }為什么不能像下面這樣實(shí)現(xiàn)賦值運(yùn)算符呢?
A& A::operator=(const A& a) {delete ps; // 銷毀ps指向的內(nèi)存,避免內(nèi)存泄漏ps = new string(*(a.ps)); i = a.i;return *this; // 返回此對象的引用 }這是因?yàn)槿绻?a 和 *this 是 同一個對象,delete ps 會釋放 *this 和 a 指向的 string。接下來,當(dāng)我們在 new表達(dá)式 中試圖拷貝*(a.ps)時,就會訪問一個指向無效內(nèi)存的指針(即空懸指針),其行為和結(jié)果是未定義的。
因此,第一種實(shí)現(xiàn)方法可以確保銷毀 *this 的現(xiàn)有成員操作是絕對安全的,不會產(chǎn)生空懸指針。
行為像指針的類
概念
對于行為類似指針的類,使用拷貝構(gòu)造函數(shù) or 賦值運(yùn)算符時,每個對象拷貝的都是 ps本身 而非 指針成員ps 指向的 string。換言之,每有一個對象都是 給指向string的ps加引用計數(shù)。
因此,析構(gòu)函數(shù)不能粗暴地釋放 ps 指向的 string ,只有當(dāng)最后一個指向 string 的 A類對象 銷毀時,才可以釋放 string 。我們會發(fā)現(xiàn)這個特性很符合 shared_ptr 的功能,因此我們可以使用 shared_ptr 來管理 像指針的類 中的資源。
但是,有時我們需要程序員直接管理資源,因此就要用到 引用計數(shù)(reference count) 了。
引用計數(shù)
工作方式:
- 每個構(gòu)造函數(shù)(拷貝構(gòu)造函數(shù)除外)都要創(chuàng)建一個引用計數(shù),用來記錄有多少對象與正在創(chuàng)建的對象共享狀態(tài)。當(dāng)我們創(chuàng)建一個對象時,只有一個對象共享狀態(tài),因此將計數(shù)器初始化為1。
- 拷貝構(gòu)造函數(shù)不分配新的計數(shù)器,而是拷貝給定對象的數(shù)據(jù)成員,包括計數(shù)器。拷貝構(gòu)造函數(shù)遞增共享的計數(shù)器,指出給定對象的狀態(tài)又被一個新用戶所共享。
- 析構(gòu)函數(shù)遞減計數(shù)器,指出共享狀態(tài)的用戶少了一個。如果計數(shù)器變?yōu)?,則析構(gòu)函數(shù)釋放狀態(tài)。
- 拷貝賦值運(yùn)算符遞增右側(cè)運(yùn)算對象的計數(shù)器,遞減左側(cè)運(yùn)算對象的計數(shù)器。如果左側(cè)運(yùn)算對象的計數(shù)器變?yōu)?,意味著它的共享狀態(tài)沒有用戶了,拷貝賦值運(yùn)算符就必須銷毀狀態(tài)。
唯一的難題是確定在哪里存放引用計數(shù)。計數(shù)器不能直接作為 A對象 的成員。舉個例子:
A a1("cmy"); A a2(a1); // a2和a1指向相同的string A a3(a2); // a1、a2、a3都指向相同的string如果計數(shù)器保存在每個對象中,創(chuàng)建 a2 時可以遞增 a1 的計數(shù)器并拷貝到 a2 中。可創(chuàng)建 a3 時,誠然可以更新 a1 的計數(shù)器,但怎么找到 a2 并將它的計數(shù)器更新呢?
那么怎么處理計數(shù)器呢?
動態(tài)內(nèi)存實(shí)現(xiàn)計數(shù)器
class A {int i = 0;string *ps;size_t *use; // 記錄有多少個對象共享*ps的成員 public:A(const string &s = string()): ps(new string(s)), i(0), use(new size_t(1)) {}A(const A &a): ps(new string(*a.ps)), i(a.i), use(a.use) { ++*use; }A& operator=(const A&);~A() {} }; A::~A(){if(--*use == 0){ // 引用計數(shù)變?yōu)?delete ps; // 釋放string內(nèi)存delete use; // 釋放計數(shù)器內(nèi)存} } A& A::operator=(const A& a) {++*(a.use); // 之所以將計數(shù)器自增操作放這么前// 是為了防止自賦值時計數(shù)器自減導(dǎo)致ps、use直接被釋放if(--(*use) == 0){delete ps;delete use;}ps = a.ps;i = a.i;use = a.use;return *this; // 返回此對象的引用 }類的swap
概念
我們在設(shè)計類的 swap 時,雖然邏輯上是這樣:
A tmp = a1; a1 = a2; a2 = tmp;但如果真的這樣實(shí)現(xiàn)的話,還需要創(chuàng)建一個新的對象 tmp,效率是很低的,造成了內(nèi)存空間的浪費(fèi)。因此我們實(shí)際上希望的是這樣的邏輯實(shí)現(xiàn):
string *tmp = a1.ps; a1.ps = a2.ps; a2.ps = tmp;創(chuàng)建一個 string類型 總比創(chuàng)建一個 A類對象 要省內(nèi)存。具體實(shí)現(xiàn):
class A {friend void swap(A&, A&); }; inline void swap(A& a1, A& a2){using std::swap;swap(a1.ps, a2.ps);swap(a1.i, a2.i); }swap實(shí)現(xiàn)自賦值
使用拷貝和交換的賦值運(yùn)算符:
A& A::operator=(A a){ // 傳值,使用拷貝構(gòu)造函數(shù)通過實(shí)參(右側(cè)運(yùn)算對象)拷貝生成臨時量aswap(*this, a); // a現(xiàn)在指向*this曾使用的內(nèi)存return *this; // a的作用域結(jié)束,被銷毀,delete了a中的ps }上面重載的賦值運(yùn)算符參數(shù)并不是一個引用,也就是說 a 是右側(cè)運(yùn)算對象的一個副本。
在函數(shù)體中,swap 交換了 a 和 *this 中的數(shù)據(jù)成員。*this 的 ps 指向右側(cè)運(yùn)算對象中 string 的一個副本;*this 原來的 ps 存入 a 中。但函數(shù)體執(zhí)行完,a 作為局部變量被銷毀,delete了 a 中的 ps,即 釋放掉了左側(cè)運(yùn)算對象(*this)中原來的內(nèi)存。
這個技術(shù)的有趣之處是它自動處理了自賦值情況且天然就是異常安全的。
總結(jié)
以上是生活随笔為你收集整理的C++ 类的行为 | 行为像值的类、行为像指针的类、swap函数处理自赋值的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux mint安装步骤,Linux
- 下一篇: 为什么我们仍然坚持用C++做游戏服务器