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