參考資料:
- 《C++ Primer》第5版
- 《C++ Primer 習題集》第5版
12.1.5 unique_ptr
(P417)
unique
“擁有”它所指向的對象,某個時刻只能有一個 unique_ptr
指向一個給定對象。

當我們定義一個 unique_ptr
時,需要將其綁定到一個 new
返回的指針上,且必須采用直接初始化的形式:
unique_ptr<double> p1; // 空unique_ptr
unique_ptr<int> p2(new int()); // 指向一個值初始化的int
unique_ptr
不支持普通的拷貝和賦值操作:
unique_ptr<string> p1(new string("hello"));
unique_ptr<string> p2(p1); // 錯誤
unique_ptr<string> p3;
p3 = p2; // 錯誤
我們可以通過 release
或 reset
將指針的所有權從一個(非 const
)unique_ptr
轉移給另一個 unique_ptr
:
unique_ptr<string> p1(new string("hello"));
unique_ptr<string> p2(p1.release());
unique_ptr<string> p3;
p3.reset(p2.release());
傳遞unique_ptr
參數和返回unique_ptr
不能拷貝 unique_ptr
的規則有一個例外:我們可以拷貝或賦值一個將要被摧毀的 unique_ptr
:
unique_ptr<int> clone(int p) {unique_ptr<int> ret(new int(p));return ret;
} // 正確
向unique_ptr
傳遞刪除器
unique
默認使用 delete
釋放指向的對象。與 shared_ptr
不同的是,我們需要在構造 unique_ptr
時提供刪除器的類型:
unique_ptr<objT, delT> p(new objT, fcn);
以上一篇筆記中提到的網絡連接類為例:
void f(destination &d /* 其他參數 */) {connection c = connect(&d);unique_ptr<connection, decltype(end_connection)*>p(&c, end_connection);// 當p被銷毀時,調用end_connection
}
12.1.6 weak_ptr
(P420)
weak_ptr
是一種不控制所指向對象生存期的智能指針,指向由一個 shared_ptr
管理的對象。將 weak_ptr
綁定到一個 shared_ptr
不會改變 shared_ptr
的引用計數,一旦最后一個指向對象的 shared_ptr
被銷毀,即使有 weak_ptr
指向對象,對象也還是會被釋放。
當我們創建一個 weak_ptr
時,要用 shared_ptr
來初始化它:
auto p = make_shared<int>();
weak_ptr<int> wp(p);
上述代碼中 wp
不會改變 p
的引用計數。由于 wp
指向的對象可能被釋放掉,我們不能使用 weak_ptr
直接訪問對象,而必須調用 lock
:
if(shared_ptr<int> np = wp.lock()){ // 如果np不為空則條件成立...
}
核查指針類
為了展示 weak_ptr
的用途,我們為 StrBlob
類定義一個伴隨指針類 StrBlobPtr
,類中保存一個 weak_ptr
,指向 StrBlob
的 data
成員。使用 weak_ptr
可以阻止用戶訪問一個不再存在的 vector
。
class StrBlobPtr {
public:StrBlobPtr(): curr(0) {}StrBlobPtr(StrBlob &a, size_t sz = 0) :wptr(a.data), curr(sz){}string &deref() const;StrBlobPtr &incr();
private:shared_ptr<vector<string>> check(size_t, const string &) const;weak_ptr<vector<string>> wptr;size_t curr; // 在數組中的當前位置
};
StrBlobPtr
的 check
成員和 StrBlob
中的同名成員不同,它還要額外檢查指向的 vector
是否存在:
shared_ptr<vector<string>>
StrBlobPtr::check(size_t i, const string &msg)const {auto ret = wptr.lock();if (!ret)throw runtime_error("unbound StrBlobPtr");if (i >= ret->size())throw out_of_range(msg);return ret;
}
指針操作
我們定義 deref
和 incr
用來解引用和遞增 StrBlobPtr
:
string &StrBlobPtr::deref()const {auto p = check(curr, "dereference past end");return (*p)[curr];
}StrBlobPtr &StrBlobPtr::incr() {check(curr, "increment past end of StrBlobPtr");++curr;return *this;
}
由于我們在初始化 StrBlobPtr
時需要用到 StrBlob
中的 data
成員,所以我們要將 StrBlobPtr
聲明成 StrBlob
的友元。
12.2 動態數組(P423)
C++ 和標準庫提供了兩種一次分配一個對象數組的方法。在大多數情況下,我們應該使用容器而非動態數組,使用容器的類可以使用默認版本的拷貝、賦值、析構操作,而使用動態數組的類必須定義自己版本的操作。
new
和數組(P423)
為了讓 new
分配一個對象數組,我們要在類型名后跟一對方括號,在其中指明要分配的對象的數目:
int *pia = new int[get_size()]; // 方括號中必須為整型,但不必為常量
也可以用類型別名來分配數組:
using arrT = int[1024];
int *p = new arrT;
分配一個數組會得到一個元素類型的指針
無論用 new T[]
還是類型別名,我們得到的都是一個指向數組元素類型的指針,而不是一個數組。下面的代碼驗證了這個事實:
int x = 0;
decltype(new int[10]) p1 = &x; // 正確
int arr[10];
decltype(arr) p2 = &x; // 錯誤
動態數組并不是數組類型
初始化動態分配對象的數組
默認情況下,new
分配的對象,不論是單個對象還是數組,都是默認初始化的。要對數組中的元素執行值初始化,可以在大小后跟一對圓括號:
int *pia1 = new int[10]; // 10個默認初始化的int
int *pia2 = new int[10](); // 10個值初始化的int
在新標準中,我們還可以提供初始值列表:
int *pia3 = new int[10] {0, 1, 2, 3};
動態分配一個空數組是合法的
char arr[0]; // 錯誤
char *cp = new char[0]; // 正確
當我們用 new
分配一個大小為 0 的數組時,new
返回一個合法的非空指針。
釋放動態數組
為了釋放動態數組,我們也要在 delete
后跟一對方括號:
delete p; // p必須指向一個動態分配的對象或為空
delete [] pa; // pa必須指向一個動態分配的對象數組或為空
數組中的元素按逆序銷毀。如果我們在 delete
一個數組時忽略了方括號或在 delete
一個對象時使用了方括號,結果是未定義的。
前面提到,當我們使用類型別名來定義數組類型時,在 new
中可以不使用方括號,但是在 delete
時則必須使用方括號:
using arrT = int[1024];
auto p = new arrT;
delete[] p;
此處產生一個疑問,既然前面提到,
new[]
得到的僅僅是一個指針,而并不是一個數組,那么delete[]
是怎么知道需要釋放多少空間的呢?答案見C++中delete是如何獲知需要釋放的內存(數組)大小的? - 知乎 (zhihu.com)
智能指針和動態數組
標準庫提供了一個可以管理 new
分配的數組的 unique_ptr
版本:
unique_ptr<int[]> up(new in[10]);
up.release(); // 自動使用delete[]

當 unique_str
指向一個數組時,我們不能使用點運算符和箭頭運算符,但我們可以使用下標運算符訪問數組中的元素。
shared_ptr
不支持直接管理動態數組。如果希望使用 shared_ptr
管理動態數組,必須定義自己的刪除器:
shared_ptr<int> sp(new int[10], [](int *p) {delete[] p; });
如果未提供刪除器,shared_ptr
將使用 delete
釋放一個動態數組,這個行為是未定義的。由于 shared_ptr
不支持下標運算符,為了訪問訪問數組中的元素,必須用 get
獲得一個內置指針:
for (size_t i = 0; i != 10; ++i) {*(sp.get() + i) = i;
}
12.2.2 allocator
類(P427)
new
在靈活性上有一些局限,因為它將內存分配和對象構造組合在一起了。當分配一大塊內存時,我們通常希望將內存分配和對象構造分離,而將內存分配和對象構造組合在一起可能造成不必要的浪費:
// 初始化了n個string,但某些string可能永遠用不到
string *const p = new string[n];
此外,沒有默認構造函數的類不能用 new
分配動態數組。
allocator
類
allocator
類定義在頭文件 memory
中,它幫助我們將內存分配和對象構造分離開來。

allocator<string> alloc;
const auto p = alloc.allocate(n); // 分配n個未初始化的string
allocator
分配未構造的內存
auto q = p; // 頂層const被忽略
alloc.construct(q++);
alloc.construct(q++, 10, 'c');
alloc.construct(q++, "hi");
當我們用完對象后,必須對每個元素調用 destroy
銷毀它們:
while(q != p){alloc.destroy(--q); // 釋放真正構造的string
}
調用 deallocate
釋放內存:
alloc.deallocate(p, n);
拷貝和填充未初始化內存的算法
標準庫還為 allocator
類定義了兩個伴隨算法,定義在頭文件 memory
中:

allocator<string> alloc;
vector<string> vs = {"hello", "hi", "him"};
auto p = alloc.allocate(vs.size() * 2);
auto q = uninitialized_copy(vs.begin(), vs.end(), p);
uninitialized_fill_n(q, vs.size(), "world");

allocator<string> alloc;
vector<string> vs = {"hello", "hi", "him"};
auto p = alloc.allocate(vs.size() * 2);
auto q = uninitialized_copy(vs.begin(), vs.end(), p);
uninitialized_fill_n(q, vs.size(), "world");