# 第?2?章?智能指針
### 目錄
* [2.1 ?概述](smartpointers.html#smartpointers_general)
* [2.2 RAII](smartpointers.html#smartpointers_raii)
* [2.3 作用域指針](smartpointers.html#smartpointers_scoped_pointer)
* [2.4 作用域數組](smartpointers.html#smartpointers_scoped_array)
* [2.5 共享指針](smartpointers.html#smartpointers_shared_pointer)
* [2.6 共享數組](smartpointers.html#smartpointers_shared_array)
* [2.7 弱指針](smartpointers.html#smartpointers_weak_pointer)
* [2.8 介入式指針](smartpointers.html#smartpointers_intrusive_pointer)
* [2.9 指針容器](smartpointers.html#smartpointers_pointer_container)
* [2.10 練習](smartpointers.html#smartpointers_exercises)
[](http://creativecommons.org/licenses/by-nc-nd/3.0/de/deed.zh) 該書采用 [Creative Commons License](http://creativecommons.org/licenses/by-nc-nd/3.0/de/deed.zh) 授權
## 2.1.??概述
1998年修訂的第一版C++標準只提供了一種智能指針: `std::auto_ptr` 。 它基本上就像是個普通的指針: 通過地址來訪問一個動態分配的對象。 `std::auto_ptr` 之所以被看作是智能指針,是因為它會在析構的時候調用 `delete` 操作符來自動釋放所包含的對象。 當然這要求在初始化的時候,傳給它一個由 `new` 操作符返回的對象的地址。 既然 `std::auto_ptr` 的析構函數會調用 `delete` 操作符,它所包含的對象的內存會確保釋放掉。 這是智能指針的一個優點。
當和異常聯系起來時這就更加重要了:沒有 `std::auto_ptr` 這樣的智能指針,每一個動態分配內存的函數都需要捕捉所有可能的異常,以確保在異常傳遞給函數的調用者之前將內存釋放掉。 Boost C++ 庫 [Smart Pointers](http://www.boost.org/libs/smart_ptr/) 提供了許多可以用在各種場合的智能指針。
## 2.2.?RAII
智能指針的原理基于一個常見的習語叫做 RAII :資源申請即初始化。 智能指針只是這個習語的其中一例——當然是相當重要的一例。 智能指針確保在任何情況下,動態分配的內存都能得到正確釋放,從而將開發人員從這項任務中解放了出來。 這包括程序因為異常而中斷,原本用于釋放內存的代碼被跳過的場景。 用一個動態分配的對象的地址來初始化智能指針,在析構的時候釋放內存,就確保了這一點。 因為析構函數總是會被執行的,這樣所包含的內存也將總是會被釋放。
無論何時,一定得有第二條指令來釋放之前另一條指令所分配的資源時,RAII 都是適用的。 許多的 C++ 應用程序都需要動態管理內存,因而智能指針是一種很重要的 RAII 類型。 不過 RAII 本身是適用于許多其它場景的。
```
#include <windows.h>
class windows_handle
{
public:
windows_handle(HANDLE h)
: handle_(h)
{
}
~windows_handle()
{
CloseHandle(handle_);
}
HANDLE handle() const
{
return handle_;
}
private:
HANDLE handle_;
};
int main()
{
windows_handle h(OpenProcess(PROCESS_SET_INFORMATION, FALSE, GetCurrentProcessId()));
SetPriorityClass(h.handle(), HIGH_PRIORITY_CLASS);
}
```
* [下載源代碼](src/2.2.1/main.cpp)
上面的例子中定義了一個名為 `windows_handle` 的類,它的析構函數調用了 `CloseHandle()` 函數。 這是一個 Windows API 函數,因而這個程序只能在 Windows 上運行。 在 Windows 上,許多資源在使用之前都要求打開。 這暗示著一旦資源不再使用之后就應該關閉。 `windows_handle` 類的機制能確保這一點。
`windows_handle` 類的實例以一個句柄來初始化。 Windows 使用句柄來唯一的標識資源。 比如說,`OpenProcess()` 函數返回一個 `HANDLE` 類型的句柄,通過該句柄可以訪問當前系統中的進程。 在示例代碼中,訪問的是進程自己——換句話說就是應用程序本身。
我們通過這個返回的句柄提升了進程的優先級,這樣它就能從調度器那里獲得更多的 CPU 時間。 這里只是用于演示目的,并沒什么實際的效應。 重要的一點是:通過 `OpenProcess()` 打開的資源不需要顯示的調用 `CloseHandle()` 來關閉。 當然,應用程序終止時資源也會隨之關閉。 然而,在更加復雜的應用程序里, `windows_handle` 類確保當一個資源不再使用時就能正確的關閉。 某個資源一旦離開了它的作用域——上例中 `h` 的作用域在 `main()` 函數的末尾——它的析構函數會被自動的調用,相應的資源也就釋放掉了。
## 2.3.?作用域指針
一個作用域指針獨占一個動態分配的對象。 對應的類名為 `boost::scoped_ptr`,它的定義在 `boost/scoped_ptr.hpp` 中。 不像 `std::auto_ptr`,一個作用域指針不能傳遞它所包含的對象的所有權到另一個作用域指針。 一旦用一個地址來初始化,這個動態分配的對象將在析構階段釋放。
因為一個作用域指針只是簡單保存和獨占一個內存地址,所以 `boost::scoped_ptr` 的實現就要比 `std::auto_ptr` 簡單。 在不需要所有權傳遞的時候應該優先使用 `boost::scoped_ptr` 。 在這些情況下,比起 `std::auto_ptr` 它是一個更好的選擇,因為可以避免不經意間的所有權傳遞。
```
#include <boost/scoped_ptr.hpp>
int main()
{
boost::scoped_ptr<int> i(new int);
*i = 1;
*i.get() = 2;
i.reset(new int);
}
```
* [下載源代碼](src/2.3.1/main.cpp)
一經初始化,智能指針 `boost::scoped_ptr` 所包含的對象,可以通過類似于普通指針的接口來訪問。 這是因為重載了相關的操作符 `operator*()`,`operator->()` 和 `operator bool()` 。 此外,還有 `get()` 和 `reset()` 方法。 前者返回所含對象的地址,后者用一個新的對象來重新初始化智能指針。 在這種情況下,新創建的對象賦值之前會先自動釋放所包含的對象。
`boost::scoped_ptr` 的析構函數中使用 `delete` 操作符來釋放所包含的對象。 這對 `boost::scoped_ptr` 所包含的類型加上了一條重要的限制。 `boost::scoped_ptr` 不能用動態分配的數組來做初始化,因為這需要調用 `delete[]` 來釋放。 在這種情況下,可以使用下面將要介紹的 `boost:scoped_array` 類。
## 2.4.?作用域數組
作用域數組的使用方式與作用域指針相似。 關鍵不同在于,作用域數組的析構函數使用 `delete[]` 操作符來釋放所包含的對象。 因為該操作符只能用于數組對象,所以作用域數組必須通過動態分配的數組來初始化。
對應的作用域數組類名為 `boost::scoped_array`,它的定義在 `boost/scoped_array.hpp` 里。
```
#include <boost/scoped_array.hpp>
int main()
{
boost::scoped_array<int> i(new int[2]);
*i.get() = 1;
i[1] = 2;
i.reset(new int[3]);
}
```
* [下載源代碼](src/2.4.1/main.cpp)
`boost:scoped_array` 類重載了操作符 `operator[]()` 和 `operator bool()`。 可以通過 `operator[]()` 操作符訪問數組中特定的元素,于是 `boost::scoped_array` 類型對象的行為就酷似它所含的數組。
正如 `boost::scoped_ptr` 那樣, `boost:scoped_array` 也提供了 `get()` 和 `reset()` 方法,用來返回和重新初始化所含對象的地址。
## 2.5.?共享指針
這是使用率最高的智能指針,但是 C++ 標準的第一版中缺少這種指針。 它已經作為技術報告1(TR 1)的一部分被添加到標準里了。 如果開發環境支持的話,可以使用 `memory` 中定義的 `std::shared_ptr`。 在 Boost C++ 庫里,這個智能指針命名為 `boost::shared_ptr`,定義在 `boost/shared_ptr.hpp` 里。
智能指針 `boost::shared_ptr` 基本上類似于 `boost::scoped_ptr`。 關鍵不同之處在于 `boost::shared_ptr` 不一定要獨占一個對象。 它可以和其他 `boost::shared_ptr` 類型的智能指針共享所有權。 在這種情況下,當引用對象的最后一個智能指針銷毀后,對象才會被釋放。
因為所有權可以在 `boost::shared_ptr` 之間共享,任何一個共享指針都可以被復制,這跟 `boost::scoped_ptr` 是不同的。 這樣就可以在標準容器里存儲智能指針了——你不能在標準容器中存儲 `std::auto_ptr`,因為它們在拷貝的時候傳遞了所有權。
```
#include <boost/shared_ptr.hpp>
#include <vector>
int main()
{
std::vector<boost::shared_ptr<int> > v;
v.push_back(boost::shared_ptr<int>(new int(1)));
v.push_back(boost::shared_ptr<int>(new int(2)));
}
```
* [下載源代碼](src/2.5.1/main.cpp)
多虧了有 `boost::shared_ptr`,我們才能像上例中展示的那樣,在標準容器中安全的使用動態分配的對象。 因為 `boost::shared_ptr` 能夠共享它所含對象的所有權,所以保存在容器中的拷貝(包括容器在需要時額外創建的拷貝)都是和原件相同的。如前所述,`std::auto_ptr`做不到這一點,所以絕對不應該在容器中保存它們。
類似于 `boost::scoped_ptr`, `boost::shared_ptr` 類重載了以下這些操作符:`operator*()`,`operator->()` 和 `operator bool()`。另外還有 `get()` 和 `reset()` 函數來獲取和重新初始化所包含的對象的地址。
```
#include <boost/shared_ptr.hpp>
int main()
{
boost::shared_ptr<int> i1(new int(1));
boost::shared_ptr<int> i2(i1);
i1.reset(new int(2));
}
```
* [下載源代碼](src/2.5.2/main.cpp)
本例中定義了2個共享指針 `i1` 和 `i2`,它們都引用到同一個 `int` 類型的對象。`i1` 通過 `new` 操作符返回的地址顯示的初始化,`i2` 通過 `i1` 拷貝構造而來。 `i1` 接著調用 `reset()`,它所包含的整數的地址被重新初始化。不過它之前所包含的對象并沒有被釋放,因為 `i2` 仍然引用著它。 智能指針 `boost::shared_ptr` 記錄了有多少個共享指針在引用同一個對象,只有在最后一個共享指針銷毀時才會釋放這個對象。
默認情況下,`boost::shared_ptr` 使用 `delete` 操作符來銷毀所含的對象。 然而,具體通過什么方法來銷毀,是可以指定的,就像下面的例子里所展示的:
```
#include <boost/shared_ptr.hpp>
#include <windows.h>
int main()
{
boost::shared_ptr<void> h(OpenProcess(PROCESS_SET_INFORMATION, FALSE, GetCurrentProcessId()), CloseHandle);
SetPriorityClass(h.get(), HIGH_PRIORITY_CLASS);
}
```
* [下載源代碼](src/2.5.3/main.cpp)
`boost::shared_ptr` 的構造函數的第二個參數是一個普通函數或者函數對象,該參數用來銷毀所含的對象。 在本例中,這個參數是 Windows API 函數 `CloseHandle()`。 當變量 `h` 超出它的作用域時,調用的是這個函數而不是 `delete` 操作符來銷毀所含的對象。 為了避免編譯錯誤,該函數只能帶一個 `HANDLE` 類型的參數, `CloseHandle()` 正好符合要求。
該例和本章稍早講述 RAII 習語時所用的例子的運行是一樣的。 然而,本例沒有單獨定義一個 `windows_handle` 類,而是利用了 `boost::shared_ptr` 的特性,給它的構造函數傳遞一個方法,這個方法會在共享指針超出它的作用域時自動調用。
## 2.6.?共享數組
共享數組的行為類似于共享指針。 關鍵不同在于共享數組在析構時,默認使用 `delete[]` 操作符來釋放所含的對象。 因為這個操作符只能用于數組對象,共享數組必須通過動態分配的數組的地址來初始化。
共享數組對應的類型是 `boost::shared_array`,它的定義在 `boost/shared_array.hpp` 里。
```
#include <boost/shared_array.hpp>
#include <iostream>
int main()
{
boost::shared_array<int> i1(new int[2]);
boost::shared_array<int> i2(i1);
i1[0] = 1;
std::cout << i2[0] << std::endl;
}
```
* [下載源代碼](src/2.6.1/main.cpp)
就像共享指針那樣,所含對象的所有權可以跟其他共享數組來共享。 這個例子中定義了2個變量 `i1` 和 `i2`,它們引用到同一個動態分配的數組。`i1` 通過 `operator[]()` 操作符保存了一個整數1——這個整數可以被 `i2` 引用,比如打印到標準輸出。
和本章中所有的智能指針一樣,`boost::shared_array` 也同樣提供了 `get()` 和 `reset()` 方法。 另外還重載了 `operator bool()`。
## 2.7.?弱指針
到目前為止介紹的各種智能指針都能在不同的場合下獨立使用。 相反,弱指針只有在配合共享指針一起使用時才有意義。 弱指針 `boost::weak_ptr` 的定義在 `boost/weak_ptr.hpp` 里。
```
#include <windows.h>
#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>
#include <iostream>
DWORD WINAPI reset(LPVOID p)
{
boost::shared_ptr<int> *sh = static_cast<boost::shared_ptr<int>*>(p);
sh->reset();
return 0;
}
DWORD WINAPI print(LPVOID p)
{
boost::weak_ptr<int> *w = static_cast<boost::weak_ptr<int>*>(p);
boost::shared_ptr<int> sh = w->lock();
if (sh)
std::cout << *sh << std::endl;
return 0;
}
int main()
{
boost::shared_ptr<int> sh(new int(99));
boost::weak_ptr<int> w(sh);
HANDLE threads[2];
threads[0] = CreateThread(0, 0, reset, &sh, 0, 0);
threads[1] = CreateThread(0, 0, print, &w, 0, 0);
WaitForMultipleObjects(2, threads, TRUE, INFINITE);
}
```
* [下載源代碼](src/2.7.1/main.cpp)
`boost::weak_ptr` 必定總是通過 `boost::shared_ptr` 來初始化的。一旦初始化之后,它基本上只提供一個有用的方法: `lock()`。此方法返回的`boost::shared_ptr` 與用來初始化弱指針的共享指針共享所有權。 如果這個共享指針不含有任何對象,返回的共享指針也將是空的。
當函數需要一個由共享指針所管理的對象,而這個對象的生存期又不依賴于這個函數時,就可以使用弱指針。 只要程序中還有一個共享指針掌管著這個對象,函數就可以使用該對象。 如果共享指針復位了,就算函數里能得到一個共享指針,對象也不存在了。
上例的 `main()` 函數中,通過 Windows API 創建了2個線程。 于是乎,該例只能在 Windows 平臺上編譯運行。
第一個線程函數 `reset()` 的參數是一個共享指針的地址。 第二個線程函數 `print()` 的參數是一個弱指針的地址。 這個弱指針是之前通過共享指針初始化的。
一旦程序啟動之后,`reset()` 和 `print()` 就都開始執行了。 不過執行順序是不確定的。 這就導致了一個潛在的問題:`reset()` 線程在銷毀對象的時候`print()` 線程可能正在訪問它。
通過調用弱指針的 `lock()` 函數可以解決這個問題:如果對象存在,那么 `lock()` 函數返回的共享指針指向這個合法的對象。否則,返回的共享指針被設置為0,這等價于標準的null指針。
弱指針本身對于對象的生存期沒有任何影響。 `lock()` 返回一個共享指針,`print()` 函數就可以安全的訪問對象了。 這就保證了——即使另一個線程要釋放對象——由于我們有返回的共享指針,對象依然存在。
## 2.8.?介入式指針
大體上,介入式指針的工作方式和共享指針完全一樣。 `boost::shared_ptr` 在內部記錄著引用到某個對象的共享指針的數量,可是對介入式指針來說,程序員就得自己來做記錄。 對于框架對象來說這就特別有用,因為它們記錄著自身被引用的次數。
介入式指針 `boost::intrusive_ptr` 定義在 `boost/intrusive_ptr.hpp` 里。
```
#include <boost/intrusive_ptr.hpp>
#include <atlbase.h>
#include <iostream>
void intrusive_ptr_add_ref(IDispatch *p)
{
p->AddRef();
}
void intrusive_ptr_release(IDispatch *p)
{
p->Release();
}
void check_windows_folder()
{
CLSID clsid;
CLSIDFromProgID(CComBSTR("Scripting.FileSystemObject"), &clsid);
void *p;
CoCreateInstance(clsid, 0, CLSCTX_INPROC_SERVER, __uuidof(IDispatch), &p);
boost::intrusive_ptr<IDispatch> disp(static_cast<IDispatch*>(p));
CComDispatchDriver dd(disp.get());
CComVariant arg("C:\\Windows");
CComVariant ret(false);
dd.Invoke1(CComBSTR("FolderExists"), &arg, &ret);
std::cout << (ret.boolVal != 0) << std::endl;
}
void main()
{
CoInitialize(0);
check_windows_folder();
CoUninitialize();
}
```
* [下載源代碼](src/2.8.1/main.cpp)
上面的例子中使用了 COM(組件對象模型)提供的函數,于是乎只能在 Windows 平臺上編譯運行。 COM 對象是使用 `boost::intrusive_ptr` 的絕佳范例,因為 COM 對象需要記錄當前有多少指針引用著它。 通過調用 `AddRef()` 和 `Release()` 函數,內部的引用計數分別增 1 或者減 1。當引用計數為 0 時,COM 對象自動銷毀。
在 `intrusive_ptr_add_ref()` 和 `intrusive_ptr_release()` 內部調用 `AddRef()` 和 `Release()` 這兩個函數,來增加或減少相應 COM 對象的引用計數。 這個例子中用到的 COM 對象名為 'FileSystemObject',在 Windows 上它是默認可用的。通過這個對象可以訪問底層的文件系統,比如檢查一個給定的目錄是否存在。 在上例中,我們檢查 `C:\Windows` 目錄是否存在。 具體它在內部是怎么實現的,跟 `boost::intrusive_ptr` 的功能無關,完全取決于 COM。 關鍵點在于一旦介入式指針 `disp` 離開了它的作用域——`check_windows_folder()` 函數的末尾,函數 `intrusive_ptr_release()` 將會被自動調用。 這將減少 COM 對象 'FileSystemObject' 的內部引用計數到0,于是該對象就銷毀了。
## 2.9.?指針容器
在你見過 Boost C++ 庫的各種智能指針之后,應該能夠編寫安全的代碼,來使用動態分配的對象和數組。多數時候,這些對象要存儲在容器里——如上所述——使用 `boost::shared_ptr` 和 `boost::shared_array` 這就相當簡單了。
```
#include <boost/shared_ptr.hpp>
#include <vector>
int main()
{
std::vector<boost::shared_ptr<int> > v;
v.push_back(boost::shared_ptr<int>(new int(1)));
v.push_back(boost::shared_ptr<int>(new int(2)));
}
```
* [下載源代碼](src/2.9.1/main.cpp)
上面例子中的代碼當然是正確的,智能指針確實可以這樣用,然而因為某些原因,實際情況中并不這么用。 第一,反復聲明 `boost::shared_ptr` 需要更多的輸入。 其次,將 `boost::shared_ptr` 拷進,拷出,或者在容器內部做拷貝,需要頻繁的增加或者減少內部引用計數,這肯定效率不高。 由于這些原因,Boost C++ 庫提供了 [指針容器](http://www.boost.org/libs/ptr_container/) 專門用來管理動態分配的對象。
```
#include <boost/ptr_container/ptr_vector.hpp>
int main()
{
boost::ptr_vector<int> v;
v.push_back(new int(1));
v.push_back(new int(2));
}
```
* [下載源代碼](src/2.9.2/main.cpp)
`boost::ptr_vector` 類的定義在 `boost/ptr_container/ptr_vector.hpp` 里,它跟前一個例子中用 `boost::shared_ptr` 模板參數來初始化的容器具有相同的工作方式。 `boost::ptr_vector` 專門用于動態分配的對象,它使用起來更容易也更高效。 `boost::ptr_vector` 獨占它所包含的對象,因而容器之外的共享指針不能共享所有權,這跟 `std::vector<boost::shared_ptr<int> >` 相反。
除了 `boost::ptr_vector` 之外,專門用于管理動態分配對象的容器還包括:`boost::ptr_deque`, `boost::ptr_list`, `boost::ptr_set`, `boost::ptr_map`, `boost::ptr_unordered_set` 和 `boost::ptr_unordered_map`。這些容器等價于C++標準里提供的那些。最后兩個容器對應于`std::unordered_set` 和 `std::unordered_map`,它們作為技術報告1的一部分加入 C++ 標準。 如果所使用的 C++ 標準實現不支持技術報告1的話,還可以使用 Boost C++ 庫里實現的 `boost::unordered_set` 和 `boost::unordered_map`。
## 2.10.?練習
You can buy [solutions to all exercises](http://en.highscore.de/shop/index.php?p=boost-solution) in this book as a ZIP file.
1. 使用適當的智能指針優化下面的程序:
```
#include <iostream>
#include <cstring>
char *get(const char *s)
{
int size = std::strlen(s);
char *text = new char[size + 1];
std::strncpy(text, s, size + 1);
return text;
}
void print(char *text)
{
std::cout << text << std::endl;
}
int main(int argc, char *argv[])
{
if (argc < 2)
{
std::cerr << argv[0] << " <data>" << std::endl;
return 1;
}
char *text = get(argv[1]);
print(text);
delete[] text;
}
```
* [下載源代碼](src/2.10.1/main.cpp)
2. 優化下面的程序:
```
#include <vector>
template <typename T>
T *create()
{
return new T;
}
int main()
{
std::vector<int*> v;
v.push_back(create<int>());
}
```
* [下載源代碼](src/2.10.2/main.cpp)