動態記憶體與智能指標
- 1.動態記憶體與智能指標
- 2.shared_ptr類
- 2.1.make_shared函式
- 2.2.shared_ptr的拷貝和賦值
- 2.3.shared_ptr自動銷毀所管理的物件
- 2.4. shared_ptr會自動釋放相關聯的記憶體
- 2.5.使用了動態生存期的資源的類
- 2.6.定義StrBlob類
- 2.7. StrBlob建構式
- 2.8.元素訪問成員函式
- 2.9.StrBlob的拷貝,賦值和銷毀
- 2.9.5StrBlob類的測驗
- 3.直接管理記憶體
- 3.1.使用new動態分配和初始化物件
- 3.2.動態分配的const物件
- 3.3.釋放動態記憶體
- 3.4.指標指和delete
- 3.5.動態物件的生存期直到被釋放時為止
- 3.6.小心:動態記憶體的管理非常容易出錯
- 3.7.delete之后重置指標值,這只是提供了有限的保護
- 4.shared_ptr和new結合使用
- 4.1.不要混合使用普通指標和智能指標
- 4.2.不要使用get初始化另一個智能指標或為智能指標賦值
- 4.3.shared_ptr指標操作:reset
- 4.4.shared_ptr的洗掉器
- 5.智能指標和例外
- 5.1注意:智能指標陷阱
- 6.unique_ptr
- 6.1.傳遞unique_ptr引數和回傳
- 6.2.unique_ptr的洗掉器
- 7.weak_ptr
- 7.1.核查指標類
- 7.2.指標操作
- 7.3.StrBlobPtr類的測驗
- 7.4.小插曲:改進StrBlobPtr的建構式
本篇參考于《C++Primer》
三種記憶體區別:
- 靜態存盤區
主要存放static靜態變數、全域變數、常量,這些資料記憶體在編譯的時候就已經為他們分配好了記憶體,生命周期是整個程式從運行到結束, - 堆疊區
存放區域變數,在執行函式的時候(包括main這樣的函式),函式內的區域變數的存盤單元會在堆疊上創建,函式執行完自動釋放,生命周期是從該函式的開始執行到結束,線性結構, - 堆區(存盤動態分配的物件)
程式員自己申請的任意大小的記憶體,一直存在直到被釋放,鏈表結構,
1.動態記憶體與智能指標
在C++中,動態記憶體的管理是通過一對運算子來完成的,
- new,在動態記憶體中為物件分配空間并回傳一個指向該物件的指標,我們可以選擇對物件進行初始化;
- delete,接受一個動態物件的指標,銷毀該物件,并釋放與之關聯的記憶體,
動態記憶體的使用很容易出問題,因為確保在正確的時間釋放記憶體是極其困難的,有時我們會忘記釋放記憶體,在這種情況下就會產生記憶體泄漏;有時在尚有指標參考記憶體的情況下我們就釋放了它,在這種情況下就會產生參考非法記憶體的指標,
為了更容易(同時也更安全)地使用動態記憶體,新的標準庫提供了兩種智能指標(smart pointer)型別來管理動態物件,智能指標的行為類似常規指標,重要的區別是它負責自動釋放所指向的物件,新標準庫提供的這兩種智能指標的區別在于管理底層指標的方式:
- shared_ptr允許多個指標指向同一個物件;
- unique_ptr則“獨占”所指向的物件,
- 標準庫還定義了一個名為weak_ptr的伴隨類,它是一種弱參考,指向shared_ptr所管理的物件,
這三種型別都定義在memory頭檔案中,
2.shared_ptr類
類似vector,智能指標也是模板,與vector一樣,我們在尖括號內給出型別,之后是所定義的這種智能指標的名字:
shared_ptr<string>p1;//shared_ptr,可以指向string
shared_ptr<list<int>>p2;//shared_ptr,可以指向int的list
默認初始化的智能指標中保存著一個空指標,后面我們將介紹初始化智能指標的其他方法,
智能指標的使用方式與普通指標類似,解參考一個智能指標回傳它指向的物件,如果在一個條件判斷中使用智能指標,效果就是檢測它是否為空,
下面列出shared_ptr和unique_ptr所支持的操作,

圖片來源于《C++Primer》
2.1.make_shared函式
最安全的分配和使用動態記憶體的方法是呼叫一個名為make_shared的標準庫函式,此函式在動態記憶體中分配一個物件并初始化它,回傳指向此物件的shared_ptr,與智能指標一樣,make_shared也定義在頭檔案memory中,
當要用make_shared時,必須指定想要創建的物件的型別,定義方式與模板類相同,在函式名之后跟一個尖括號,在其中給出型別:
//p3指向一個值為42的int的shared_ptr
shared_ptr<int>p3 = make_shared<int>(42);
//p4指向一個值為“9999999999”的string
shared_ptr<string>p4 = make_shared<string>(10,'9');
//p5指向一個值初始化的int,即值為0
shared_ptr<int>p5 = make_shared<int>();
我們通常用auto定義一個物件來保存make_shared的結果,這種方式比較簡單:
//p6指向一個動態分配的空vector<string>
auto p6 = make_shared<vector<string>>();
2.2.shared_ptr的拷貝和賦值
當進行拷貝或賦值操作時,每個shared_ptr都會記錄有多少個其他shared_ptr指向相同的物件:
auto p = make_shared<int>(42);//p指向的物件只有p一個參考者
auto q(p);//p和q指向相同物件,此物件有兩個參考者
我們可以認為每個shared_ptr都有一個關聯的計數器,通常稱其為參考計數,無論何時我們拷貝一個shared_ptr,計數器都會遞增,例如,當用一個shared_ptr初始化另一個shared_ptr,或將它作為引數傳遞給一個函式以及作為函式的回傳值時,它所關聯的計數器就會遞增,當我們給shared_ptr賦予一個新值或是shared_ptr被銷毀(例如一個區域的shared_ptr離開其作用域時,計數器就會遞減,
一旦一個shared_ptr的計數器變為0,它就會自動釋放自己所管理的物件:
auto r = make_shared<int>(42);//r指向的int只有一個參考者
r = q;//給r賦值,令它指向另一個地址
//遞增q指向的物件的參考計數
//遞減r原來指向的物件的參考計數
//r原來指向的物件已沒有參考者,會自動釋放
此例中我們分配了一個int,將其指標保存在r中,接下來,我們將一個新值賦予r,在此情況下,r是唯一指向此int的shared_ptr,在把q賦給r的程序中,此int被自動釋放,
2.3.shared_ptr自動銷毀所管理的物件
當指向一個物件的最后一個shared_ptr被銷毀時,shared_ptr類會自動銷毀此物件,它是通過另一個特殊的成員函式——解構式完成銷毀作業的,
shared_ptr的解構式會遞減它所指向的物件的參考計數,如果參考計數變為0,shared_ptr的解構式就會銷毀物件,并釋放它占用的記憶體,
2.4. shared_ptr會自動釋放相關聯的記憶體
當動態物件不再被使用時,shared_ptr類會自動釋放動態物件,這一特性使得動態記憶體的使用變得非常容易,
2.5.使用了動態生存期的資源的類
程式使用動態記憶體出于以下三種原因之一:
1.程式不知道自己需要使用多少物件
2.程式不知道所需物件的準確型別
3.程式需要在多個物件間共享資料
容器類是出于第一種原因而使用動態記憶體的典型例子,這里我們拿vector容器舉例,每個vector“擁有”其自己的元素,當我們拷貝一個vector時,原vector和副本vector中的元素是相互分離的:
vector<string>v1;//空vector
{//新作用域
vector<string>v2 = {"a","an","the"};
v1 = v2;//從v2拷貝元素到v1中
}//v2被銷毀,其中的元素也被銷毀
//v1有三個元素,是原來v2中元素的拷貝
由一個vector分配的元素只有當這個vector存在時才存在,當一個vector被銷毀時,這個vector中的元素也都被銷毀,
出于第二種原因而使用動態記憶體的情況我們在這里不介紹,
我們現在來定義一個名為Blob的類,保存一組元素,與容器不同,我們希望Blob物件的不同拷貝之間共享相同的元素,即,當我們拷貝一個Blob時,原Blob物件及其拷貝應該參考相同的底層元素,
一般而言,如果兩個物件共享底層的資料,當某個物件被銷毀時,我們不能單方面地銷毀底層資料:
Blob<string>b1;//空Blob
{//新作用域
Blob<string>b2 = {"a","an","the"};
b1 = b2;//b1和b2共享相同的元素
}//b2被銷毀了,但b2中的元素不能被銷毀
//b1指向最初由b2創建的元素
在此例中,b1和b2共享相同的元素,當b2離開作用域時,這些元素必須保留,因為b1仍然在使用它們,
note:
使用動態記憶體的一個常見原因是允許多個物件共享相同的狀態,
2.6.定義StrBlob類
現在讓我們來撰寫在多個物件間共享資料的類,在這里我們不用模板來實作(因為我還不會模板),因此,現在我們先定義一個管理string的類,此版本命名為StrBlob,
實作一個新的集合型別的最簡單方法是使用某個標準庫容器來管理元素,采用這種方法,我們可以借助標準庫型別來管理元素所使用的記憶體空間,在本例中,我們將使用vector來保存元素,
但是,我們不能在一個Blob物件內直接保存vector,因為一個物件的成員在物件銷毀時也會被銷毀,例如,假定b1和b2是兩個Blob物件,共享相同的vector,如果此vector保存在其中一個Blob中——例如b2中,那么當b2離開作用域時,此vector也將被銷毀,也就是說其中的元素都將不復存在,為了保證vector中的元素繼續存在,我們將vector保存在動態記憶體中,
為了實作我們所希望的資料共享,我們為每個StrBlob設定一個shared_ptr來管理動態分配的vector,此shared_ptr的成員將記錄有多少個StrBlob共享相同的vector,并在vector的最后一個使用者被銷毀時釋放vector,
我們還需要確定這個類應該提供什么操作,當前,我們將實作一個vector操作的小的子集,我們會修改訪問元素的操作(如front和back):在我們的類中,如果用戶試圖訪問不存在的元素,這些操作會拋出一個例外,
我們的類有一個默認建構式和一個建構式,接受單一的initializer_list型別引數,此建構式可以接受一個初始化器的花括號串列,
代碼如下:
class StrBlob{
public:
typedef std::vector<std::string>::size_type size_type;
StrBlob();
StrBlob(std::initializer_list<std::string>il);
size_type size()const{return data->size();}
bool empty()const {return data->empty();}
void push_back(const std::string &t){data->push_back(t);}
void pop_back();
std::string&front();
std::string&back();
private:
std::shared_ptr<std::vector<std::string>>data;
void check(size_type i,const std::string &msg)const;
};
在此類中,我們實作了size、empty和push_back成員,這些成員通過指向底層vector的data成員來完成它們的作業,例如,對一個StrBlob物件呼叫size()會呼叫data->size(),依此類推,
2.7. StrBlob建構式
兩個建構式都使用初始化串列來初始化其data成員,令它指向一個動態分配的vector,默認建構式分配一個空vector:
StrBlob::StrBlob():data(make_shared<vector<string>>()){}
StrBlob::StrBlob(initializer_list<string>il):data(make_shared<vector<string>>(il)){}
接受一個initializer_list的建構式將其引數傳遞給對應的vector建構式,此建構式通過拷貝串列中的值來初始化vector的元素,
2.8.元素訪問成員函式
pop_back、front和back操作訪問vector中的元素,這些操作在試圖訪問元素之前必須檢查元素是否存在,由于這些成員函式需要做相同的檢查操作,我們為StrBlob定義了一個名為check的private工具函式,它檢查一個給定索引是否在合法范圍內,除了索引,check還接受一個string引數,它會將此引數傳遞給例外處理程式,這個string描述了錯誤內容:
pop_back和元素訪問成員函式首先呼叫check,如果check成功,這些成員函式繼續利用底層vector的操作來完成自己的作業:
void StrBlob::check(size_type i,const string &msg) const
{
if (i >= data->size())
throw out_of_range(msg);
}
pop_back和元素訪問成員函式首先呼叫check,如果check成功,這些成員函式繼續利用底層vector的操作來完成自己的作業:
string &StrBlob::front()
{
check(0,"front on empty StrBlob");
return data->front();
}
string &StrBlob::back()
{
check(0,"back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back()
{
check(0,"pop_back on empty StrBlob");
data->pop_back();
}
front和back應該對const進行多載
2.9.StrBlob的拷貝,賦值和銷毀
StrBlob使用默認版本的拷貝、賦值和銷毀成員函式來對此型別的物件進行這些操作,默認情況下,這些操作拷貝、賦值和銷毀類的資料成員,我們的StrBlob類只有一個資料成員,它是shared_ptr型別,因此,當我們拷貝、賦值或銷毀一個StrBlob物件時,它的shared_ptr成員會被拷貝、賦值或銷毀,
如前所見,拷貝一個shared_ptr會遞增其參考計數;將一個shared_ptr賦予另一個shared_ptr會遞增賦值號右側shared_ptr的參考計數,而遞減左側shared_ptr的參考計數,如果一個shared_ptr的參考計數變為0,它所指向的物件會被自動銷毀,因此,對于由StrBlob建構式分配的vector,當最后一個指向它的StrBlob物件被銷毀時,它會隨之被自動銷毀,
2.9.5StrBlob類的測驗
完整代碼如下:
#ifndef MY_STRBLOB_H
#define MY_STRBLOB_H
#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
#include <stdexcept>
using namespace std;
class StrBlob {
public:
typedef vector<string>::size_type size_type;
StrBlob();
StrBlob(initializer_list<string>il);
size_type size()const {
return data->size();
}
bool empty()const {
return data->empty();
}
void push_back(const string &t) {
data->push_back(t);
}
void pop_back();
string &front();
const string &front()const;
string &back();
const string &back()const;
private:
shared_ptr<std::vector<std::string>>data;
void check(size_type i, const std::string &msg)const;
};
StrBlob::StrBlob(): data(make_shared<vector<string>>()) {
}
StrBlob::StrBlob(initializer_list<string>il): data(make_shared<vector<string>>(il)) {}
void StrBlob::check(size_type i, const string &msg)const {
if (i >= data->size())
throw out_of_range(msg);
}
string &StrBlob::front() {
check(0, "front on empty StrBlob");
return data->front();
}
const string &StrBlob::front()const {
check(0, "front on empty StrBlob");
return data->front();
}
string &StrBlob::back() {
check(0, "back on empty StrBlob");
return data->back();
}
const string &StrBlob::back()const {
check(0, "back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back() {
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
#endif
測驗代碼如下:
#include <iostream>
using namespace std;
#include "my_StrBlob.h"
int main() {
StrBlob b1;
StrBlob b2 = {"a", "an", "the"};
b1 = b2;
b2.push_back("about");
cout << b2.size() << endl;
cout << b1.size() << endl;
cout << b1.front() << " " << b1.back() << endl;
const StrBlob b3 = b1;
cout << b3.front() << " " << b3.back() << endl;
return 0;
}
測驗結果:

普通vector代碼如下:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<string>b1;
vector<string> b2 = {"a", "an", "the"};
b1 = b2;
b2.push_back("about");
cout << b2.size() << endl;
cout << b1.size() << endl;
cout << b1.front() << " " << b1.back() << endl;
const vector<string> b3 = b1;
cout << b3.front() << " " << b3.back() << endl;
return 0;
}
測驗結果:

3.直接管理記憶體
C++定義了new分配記憶體,delete釋放new分配的記憶體,
相對于智能指標,使用這兩個運算子管理記憶體非常容易出錯
而且,自己直接管理記憶體的類與使用智能指標的類不同,它們不能依賴類物件拷貝、賦值和銷毀操作的任何默認定義,因此,使用智能指標的程式更容易撰寫和除錯,
3.1.使用new動態分配和初始化物件
在自由空間分配的記憶體是無名的,因此new無法為其分配的物件命名,而是回傳一個指向該物件的指標:
int *pi = new int;//pi指向一個動態分配的,未初始化的無名物件
此new運算式在自由空間構造一個int型物件,并回傳指向該物件的指標,
默認情況下,動態分配的物件是默認初始化的,這意味著內置型別或組合型別的物件的值將是未定義的,而型別別物件將用默認建構式進行初始化:
string *ps = new string;//初始化空string
int *pi = new int;//pi指向一個未初始化的int
初始化方式(1.使用圓括號,2.使用串列初始化,3.在型別名之后跟一對空括號進行值初始化):
int *pi = new int(1024);//pi指向的物件的值為1024
string *ps = new string(10,'9');//*ps為"9999999999"
vector<int>*pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};
//vector有10個元素,值依次從0到9
string *ps1 = new string;//默認初始化為空string
string *ps = new string();//值初始化為空string
int *pi1 = new int;//默認初始化;*pi1的值未定義
int *pi2 = new int();//值初始化為0,*pi2為0
如果我們用auto來初始化,由于編譯器要用初始化器的型別來推斷要分配的型別,只有當括號中僅有單一初始化器時才可以使用auto:
auto p1 = new auto(obj);//p指向一個與obj型別相同的物件
//該物件用obj進行初始化
auto p2 = new auto{a,b,c};//錯誤;括號中只能有單個初始化器
p1的型別是一個指標,指向從obj自動推斷出的型別,若obj是一個int,那么p1就是int*;若obj是一個string,那么p1是一個string*;依此類推,新分配的物件用obj的值進行初始化,
3.2.動態分配的const物件
用new分配const物件是合法的:
const int *pci = new const int(1024);
//分配并初始化一個const int
const string *pcs = new const string;
//分配并默認初始化一個const的空string
類似其他任何const物件,一個動態分配的const物件必須進行初始化,對于一個定義了默認建構式的型別別,其const動態物件可以隱式初始化,而其他型別的物件就必須顯式初始化,由于分配的物件是const的,new回傳的指標是一個指向const的指標,
3.3.釋放動態記憶體
為了防止記憶體耗盡(在這里不介紹),在動態記憶體使用完畢后,必須將其歸還給系統,我們通過delete運算式來將動態記憶體歸還給系統,delete運算式接受一個指標,指向我們想要釋放的物件:
delete p;//p必須指向一個動態分配的物件或是一個空指標
note:
delete p; 之后,p的值(指向的地址不變),但不能再使用p處理該地址的內容,也不能再delete p,因為delete之后,p不指向nullptr,
通常需要delete之后置指標為空:
delete p;
p = nullptr;
與new型別類似,delete運算式也執行兩個動作:銷毀給定的指標指向的物件;釋放對應的記憶體,
3.4.指標指和delete
我們傳遞給delete的指標必須指向動態分配的記憶體,或者是一個空指標,釋放一塊并非new分配的記憶體,或者將相同的指標值釋放多次,又或者釋放一個區域變數等,其行為是未定義的,
舉個例子:
如果我們delete了區域變數,那么當區域變數離開了它的作用域,系統要銷毀這個區域變數!可這個區域變數已經被我們銷毀了,就相當于將相同的指標值釋放多次,(如果錯誤,還請指正)
雖然一個const物件的值不能被改變,但它本身是可以被銷毀的,
const int *pci = new const int(1024);
delete pci;
3.5.動態物件的生存期直到被釋放時為止
注意:呼叫者必須記得釋放記憶體
讓我們看下面這段代碼:
#include <iostream>
using namespace std;
int *fff(int a) //回傳一個指標,指向一個動態分配的物件
{
return new int(a);
}
void use_fff(int a) {
int *b = fff(a);//呼叫者必須記得釋放此記憶體
cout << *b << endl;
}//b離開了它的作用域,但它所指向的記憶體沒有被釋放!!!
int main() {
use_fff(42);
return 0;
}
注意:
由內置指標(而不是智能指標)管理的動態記憶體在被顯式釋放前一直都會存在,
#include <iostream>
using namespace std;
int *fff(int a) { //回傳一個指標,指向一個動態分配的物件
return new int(a);
}
void use_fff(int a) {
int *b = fff(a);//呼叫者必須記得釋放此記憶體
cout << *b << endl;
delete b;//現在記得釋放記憶體,我們已經不需要它了
}
int main() {
use_fff(42);
return 0;
}
當然,如果在這段代碼中,我們不釋放記憶體,那么目的就是,我們的系統中的其他代碼要使用use_fff所分配的物件,我們就應該修改此函式,讓它回傳一個指標,指向它分配的記憶體:
#include <iostream>
using namespace std;
int *fff(int a) { //回傳一個指標,指向一個動態分配的物件
return new int(a);
}
int *use_fff(int a) {
int *b = fff(a);//呼叫者必須記得釋放此記憶體
cout << *b << endl;
return b;
}
int main() {
int *c = use_fff(42);
cout << *c << endl;
return 0;
}
3.6.小心:動態記憶體的管理非常容易出錯
- 忘記delete記憶體
- 使用已釋放掉的記憶體
- 同一塊記憶體釋放兩次
堅持只使用智能指標,就可以避免所有這些問題,對于一塊記憶體,只有在沒有任何智能指標指向它的情況下,智能指標才會自動釋放它,
3.7.delete之后重置指標值,這只是提供了有限的保護
當我們delete一個指標后,指標值就變為無效了,雖然指標無效,但是很多機器上指標(已經釋放了的)仍然保存著動態記憶體的地址,在delete之后,指標就變成了空懸指標
空懸指標:指向一塊曾經保存資料物件但現在已經無效的記憶體的指標,
未初始化指標的所有缺點空懸指標也都有,有一種方法可以避免空懸指標的問題:在指標即將要離開其作用域之前釋放掉它所關聯的記憶體,這樣,在指標關聯的記憶體被釋放掉之后,就沒有機會繼續使用指標了,如果我們需要保留指標,可以在delete之后將nullptr賦予指標,這樣就清楚地指出指標不指向任何物件,
但這也僅僅提供了有限的保護,
比如下面這個例子:
int *p(new int(42));//p指向動態記憶體
auto q = p;//p和q指向相同記憶體
delete p;//p和q均變為無效
p = nullptr;//指出p不再系結到任何物件
本例中p和q指向相同的動態分配的物件,我們delete此記憶體,然后將p置為nullptr,指出它不再指向任何物件,但是,重置p對q沒有任何作用,在我們釋放p所指向的(同時也是q所指向的!)記憶體時,q也變為無效了,在實際系統中,查找指向相同記憶體的所有指標是例外困難的,(在這里,我們很難發現q已經無效了)
4.shared_ptr和new結合使用
如果我們不初始化一個智能指標,它就會被初始化為一個空指標,如下面代碼所示,我們還可以用new回傳的指標來初始化智能指標:
shared_ptr<double>p1;//shared_ptr可以指向一個double
shared_ptr<int>p2(new int(42));//p2指向一個值為42的int
接受指標引數的智能指標建構式是explicit的,因此,我們不能將一個內置指標隱式轉換為一個智能指標,必須使用直接初始化形式來初始化一個智能指標:
shared_ptr<int>p1 = new int(1024);//錯誤,必須使用直接初始化
shared_ptr<int>p2(new int(1024));//正確,直接初始化
shared_ptr<int>clone(int p)
{
return new int(p);
//錯誤;隱式轉換為shared_ptr<int>;
}
shared_ptr<int>clone(int p)
{
return shared_ptr<int>(new int(p));
//正確;顯式地用int*創建shared_ptr<int>;
}
默認情況下,
- 一個用來初始化智能指標的普通指標必須指向動態記憶體,因為智能指標默認使用delete釋放它所關聯的物件,
- 我們可以將智能指標系結到一個指向其他型別的資源的指標上,但是為了這樣做,必須提供自己的操作來替代delete,


4.1.不要混合使用普通指標和智能指標
shared_ptr可以協調物件的析構,但這僅限于其自身的拷貝(也是shared_ptr)之間,這也是為什么我們推薦使用make_shared而不是new的原因,這樣,我們就能在分配物件的同時就將shared_ptr與之系結,從而避免了無意中將同一塊記憶體系結到多個獨立創建的shared_ptr上,
現在讓我們看下面這段代碼:
//在函式呼叫時ptr被創建并初始化
void process(shared_ptr<int>ptr)
{
//使用ptr
};//ptr離開作用域,被銷毀
process的引數是傳值方式傳遞,因此實參會被拷貝到ptr,創建一個shared_ptr會遞增其參考計數,拷貝一個shared_ptr也會遞增其參考計數,因此在process運行程序中,其參考計數至少為2,等ptr離開作用域,ptr的參考計數會遞減,但不會變為0,因此,當區域變數ptr被銷毀時,ptr指向的記憶體不會被釋放,
使用process函式的正確方法是傳遞給它一個shared_ptr:
shared_ptr<int>p(new int(42));//參考計數為1
process(p);//拷貝p會遞增參考計數,參考計數為2
int i = *p;//正確,參考計數為1
現在讓我們混合使用普通指標和智能指標,看會發生什么???
雖然我們不能傳遞給process一個內置指標,但可以傳遞給它一個(臨時的)shared_ptr,這個shared_ptr是用一個內置指標顯式構造的,但是,這樣做很可能會導致錯誤:
int *x(new int(1024));//危險,x是一個普通指標,不是一個智能指標
process(x);//錯誤,不能將int*轉換為一個shared_ptr<int>
process(shared_ptr<int>(x));//合法的,但記憶體會被釋放
int j = *x;//未定義,x是一個空懸指標
在上面的呼叫中,我們將一個臨時shared_ptr傳遞給process,當這個呼叫所在的運算式結束時,這個臨時物件就被銷毀了,銷毀這個臨時變數會遞減參考計數,此時參考計數就變為0了,因此,當臨時物件被銷毀時,它所指向的記憶體會被釋放,
但x繼續指向(已經釋放的)記憶體,從而變成一個空懸指標,如果試圖使用x的值,其行為是未定義的,
當將一個shared_ptr系結到一個普通指標時,我們就將記憶體的管理責任交給了這個shared_ptr,一旦這樣做了,我們就不應該再使用內置指標來訪問shared_ptr所指向的記憶體了,
注意:
使用一個內置指標來訪問一個智能指標所負責的物件是很危險的,因為我們無法知道物件何時會被銷毀,
4.2.不要使用get初始化另一個智能指標或為智能指標賦值
智能指標型別定義了一個名為get的函式(參見表12.1),它回傳一個內置指標,指向智能指標管理的物件,
當我們需要向不能使用智能指標的代碼傳遞一個內置指標時,我們才會用get,
使用get回傳的指標的代碼不能delete此指標,
雖然編譯器不會給出錯誤資訊,但將另一個智能指標也系結到get回傳的指標上是錯誤的:
shared_ptr<int>p(new int(42));//參考計數為1
int *q = p.get();//正確,但使用q時要注意,不要讓它管理的指標被釋放
{//新程式塊
//未定義:兩個獨立的shared_ptr指向相同的記憶體
shared_ptr<int>q;
}//程式塊結束,q被銷毀,它指向的記憶體被釋放
int foo = *p;//未定義:p指向的記憶體已經被釋放了
在本例中,p和q指向相同的記憶體,由于它們是相互獨立創建的,因此各自的參考計數都是1,當q所在的程式塊結束時,q被銷毀,這會導致q指向的記憶體被釋放,從而p變成一個空懸指標,意味著當我們試圖使用p時,將發生未定義的行為,而且,當p被銷毀時,這塊記憶體會被第二次delete,
注意:
get用來將指標的訪問權限傳遞給代碼,你只有在確定代碼不會delete指標的情況下,才能使用get,特別是,永遠不要用get初始化另一個智能指標或者為另一個智能指標賦值,
4.3.shared_ptr指標操作:reset
上面的代碼,指標p指向的記憶體已經被釋放了,不過我們可以用reset來將一個新的指標賦予一個shared_ptr:
p = new int(1024);//錯誤,不能將一個指標賦予shared_ptr
p.reset(new int(1024));//正確,p指向一個新物件
與賦值類似,reset會更新參考計數,如果需要的話,會釋放p指向的物件,**reset成員經常與unique一起使用,來控制多個shared_ptr共享的物件,**在改變底層物件之前,我們檢查自己是否是當前物件僅有的用戶,如果不是,在改變之前要制作一份新的拷貝:
if (!p.unique())
p.reset(new string(*p));//我們不是唯一用戶;分配新的拷貝
*p+=newVal;//現在我們知道自己是唯一的用戶,可以改變物件的值
4.4.shared_ptr的洗掉器
本文暫時未介紹!!!
5.智能指標和例外
使用智能指標,即使程式塊過早結束或發生例外,智能指標類也能確保在記憶體不再需要時將其釋放:
void f()
{
shared_ptr<int>sp(new int(42));//分配一個物件
//發生例外,且未被f捕獲
}//在函式結束時shared_ptr自動釋放記憶體
函式的退出有兩種可能,正常處理結束或者發生了例外,無論哪種情況,區域物件都會被銷毀,在上面的程式中,sp是一個shared_ptr,因此sp銷毀時會檢查參考計數,在此例中,sp是指向這塊記憶體的唯一指標,因此記憶體會被釋放掉,
與之相對的,當發生例外時,我們直接管理的記憶體是不會自動釋放的,如果使用內置指標管理記憶體,且在new之后在對應的delete之前發生了例外,則記憶體不會被釋放:
void f
{
int *ip = new int(42);//動態分配一個新物件
//發生例外,且在f中未被捕獲
delete ip//在退出之前手動釋放記憶體
}
如果在new和delete之間發生例外,且例外未在f中被捕獲,則記憶體就永遠不會被釋放了,在函式f之外沒有指標指向這塊記憶體,因此就無法釋放它了,
5.1注意:智能指標陷阱
智能指標可以提供對動態分配的記憶體安全而又方便的管理,但這建立在正確使用的前提下,為了正確使用智能指標,我們必須堅持一些基本規范:
- · 不使用相同的內置指標值初始化(或reset)多個智能指標,
- · 不delete get()回傳的指標,
- · 不使用get()初始化或reset另一個智能指標,
- · 如果你使用get()回傳的指標,記住當最后一個對應的智能指標銷毀后,你的指標就變為無效了,
- ·如果你使用智能指標管理的資源不是new分配的記憶體,記住傳遞給它一個洗掉器(本文暫時沒有介紹該內容)
6.unique_ptr
一個unique_ptr“擁有”它所指向的物件,與shared_ptr不同,某個時刻只能有一個unique_ptr指向一個給定物件,當unique_ptr被銷毀時,它所指向的物件也被銷毀,表12.4列出了unique_ptr特有的操作,與shared_ptr相同的操作列在表12.1)中,

與shared_ptr不同,沒有類似make_shared的標準庫函式回傳一個unique_ptr,當我們定義一個unique_ptr時,需要將其系結到一個new回傳的指標上,類似shared_ptr,初始化unique_ptr必須采用直接初始化形式:
unique_ptr<double>p1;//可以指向一個double的unique_ptr
unique_ptr<int>p2(new int(42));//p2指向一個值為42的int
由于一個unique_ptr擁有它指向的物件,因此unique_ptr不支持普通的拷貝或賦值操作:
unique_ptr<string>p1(new string("Stegosaurus"));
unique_ptr<string>p2(p1);//錯誤,unique_ptr不支持拷貝
unique_ptr<string>p3;
p3 = p2;//錯誤,unique不支持賦值
雖然我們不能拷貝或賦值unique_ptr,但可以通過呼叫release或reset將指標的所有權從一個(非const)unique_ptr轉移給另一個unique:
//將所有權從p1(指向string Stegosaurus)轉移給p2
unique_ptr<string>p2(p1.release());//release將p1置為空
unique_ptr<string>p3(new string("Trex"));
//將所有權從p3轉移給p2
p2.reset(p3.release());//reset釋放了p2原來指向的記憶體
- release成員回傳unique_ptr當前保存的指標并將其置為空,
- reset成員接受一個可選的指標引數,令unique_ptr重新指向給定的指標,如果unique_ptr不為空,它原來指向的物件被釋放,
unique_ptr<string>p2(p1.release());
因此,p2被初始化為p1原來保存的指標,而p1被置為空,
p2.reset(p3.release());
因此,對p2呼叫reset釋放了用"Stegosaurus"初始化的string所使用的記憶體,將p3對指標的所有權轉移給p2,并將p3置為空,
呼叫release會切斷unique_ptr和它原來管理的物件間的聯系,**release回傳的指標通常被用來初始化另一個智能指標或給另一個智能指標賦值,**在本例中,管理記憶體的責任簡單地從一個智能指標轉移給另一個,但是,如果我們不用另一個智能指標來保存release回傳的指標,我們的程式就要負責資源的釋放:
p2.release();//錯誤,p2不會釋放記憶體,而且我們丟失了指標
auto p = p2.release();//正確,但我們必須記得delete(p)
6.1.傳遞unique_ptr引數和回傳
unique_ptr不能拷貝unique_ptr的規則有一個例外:我們可以拷貝或賦值一個將要被銷毀的unique_ptr,最常見的例子是從函式回傳一個unique_ptr:
unique_ptr<int>clone(int p)
{
//正確,從int*創建一個unique_ptr<int>
return unique_ptr<int>(new int(p));
}
還可以回傳一個區域物件的拷貝:
unique_ptr<int>clone(int p)
{
unique_ptr<int>ret(new int (p));
//...
return ret;
}
對于兩段代碼,編譯器都知道要回傳的物件將要被銷毀,在此情況下,編譯器執行一種特殊的“拷貝”(本文無介紹)
6.2.unique_ptr的洗掉器
本文暫時不介紹!!!
7.weak_ptr
weak_ptr(見表12.5)是一種不控制所指向物件生存期的智能指標,它指向由一個shared_ptr管理的物件,將一個weak_ptr系結到一個shared_ptr不會改變shared_ptr的參考計數,一旦最后一個指向物件的shared_ptr被銷毀,物件就會被釋放,即使有weak_ptr指向物件,物件也還是會被釋放,因此,weak_ptr的名字抓住了這種智能指標“弱”共享物件的特點,
當我們創建一個weak_ptr時,要用一個shared_ptr來初始化它:
auto p = make_shared<int>(42);
weak_ptr<int>wp(p);//wp弱共享p;p的參考計數未改變
本例中wp和p指向相同的物件,由于是弱共享,創建wp不會改變p的參考計數;wp指向的物件可能被釋放掉,
由于物件可能不存在,我們不能使用weak_ptr直接訪問物件,而必須呼叫lock, 此函式檢查weak_ptr指向的物件是否仍存在,如果存在,lock回傳一個指向共享物件的shared_ptr,與任何其他shared_ptr類似,只要此shared_ptr存在,它所指向的底層物件也就會一直存在,例如:
auto p = make_shared<int>(42);
weak_ptr<int>wp(p);//wp弱共享p;p的參考計數未改變
if(shared_ptr<int>np = wp.lock())//如果np不為空則條件成立
{
//在if中,np與p共享物件
}
在這段代碼中,只有當lock呼叫回傳true時我們才會進入if陳述句體,在if中,使用np訪問共享物件是安全的,
7.1.核查指標類
作為weak_ptr用途的一個展示,我們將為StrBlob類定義一個伴隨指標類,我們的指標類將命名為StrBlobPtr,會保存一個weak_ptr,指向StrBlob的data成員,這是初始化時提供給它的,通過使用weak_ptr,不會影響一個給定的StrBlob所指向的vector的生存期,但是,可以阻止用戶訪問一個不再存在的vector的企圖,
StrBlobPtr會有兩個資料成員:wptr,或者為空,或者指向一個StrBlob中的vector;curr,保存當前物件所表示的元素的下標,類似它的伴隨類StrBlob,我們的指標類也有一個check成員來檢查解參考StrBlobPtr是否安全:
//對于訪問一個不存在元素的嘗試,StrBlobPtr拋出一個例外
class StrBlobPtr
{
public:
StrBlobPtr():curr(0){}
StrBlobPtr(StrBlob &a,size_t sz = 0):wptr(a.data),curr(sz){}
std::string &deref()const;
StrBlobPtr&incr();//前綴遞增
private:
//若檢查成功,check回傳一個指向vector的shared_ptr
std::shared_ptr<std::vector<std::string>>
check(std::size_t,const std::string&)const;
//保存一個weak_ptr,意味著底層vector可能會被銷毀
std::weak_ptr<std::vector<std::string>>wptr;
std::size_t curr;//在陣列中的當前位置
};
默認建構式生成一個空的StrBlobPtr,其建構式初始化串列將curr顯式初始化為0,并將wptr隱式初始化為一個空weak_ptr,第二個建構式接受一個StrBlob參考和一個可選的索引值,此建構式初始化wptr,令其指向給定StrBlob物件的shared_ptr中的vector,并將curr初始化為sz的值,我們使用了默認引數,表示默認情況下將curr初始化為第一個元素的下標,我們將會看到,StrBlob的end成員將會用到引數sz,
值得注意的是,我們不能將StrBlobPtr系結到一個const StrBlob物件,這個限制是由于建構式接受一個非const StrBlob物件的參考而導致的,
StrBlobPtr的check成員與StrBlob中的同名成員不同,它還要檢查指標指向的vector是否還存在:
std::shared_ptr<std::vector<std::string>>
StrBlobPtr::check(std::size_t i,const std::string &msg) const
{
auto ret = wptr.lock();//vector還存在嗎?
if (!ret)
throw std::runtime_error("unbound StrBlobPtr");
if (i >= ret->size())
throw std::out_of_range(msg);
return ret; //否則,回傳指向vector的shared_ptr
}
由于一個weak_ptr不參與其對應的shared_ptr的參考計數,StrBlobPtr指向的vector可能已經被釋放了,如果vector已銷毀,lock將回傳一個空指標,在本例中,任何vector的參考都會失敗,于是拋出一個例外,否則,check會檢查給定索引,如果索引值合法,check回傳從lock獲得的shared_ptr,
7.2.指標操作
在這里我們不用多載運算子的方法,我們定義名為deref和incr的函式,分別用來解參考和遞增StrBlobPtr,
deref成員呼叫check,檢查使用vector是否安全以及curr是否在合法范圍內:
std::string&StrBlobPtr::deref()const
{
auto p = check(curr,"dereference past end");
return (*p)[curr];//(*p)是物件所指向的vector
}
如果check成功,p就是一個shared_ptr,指向StrBlobPtr所指向的vector,運算式(*p)[curr]解參考shared_ptr來獲得vector,然后使用下標運算子提取并回傳curr位置上的元素,
//incr成員也呼叫check
//前綴遞增;回傳遞增后的物件的參考
StrBlobPtr&StrBlobPtr::incr()
{
//如果curr已經指向容器的尾后位置,就不能遞增它
check(curr,"increment past end of StrBlobPtr");
++curr;//推進當前位置
return *this;
}
當然,為了訪問data成員,我們的指標類必須宣告為StrBlob的friend(參見7.3.4節,第250頁),我們還要為StrBlob類定義begin和end操作,回傳一個指向它自身的StrBlobPtr:
//對于StrBlob中的友元宣告來說,此前置宣告是必要的
class StrBlobPtr;
class StrBlob
{
friend class StrBlobPtr;
//回傳指向首元素和后元素的StrBlobPtr
StrBlobPtr begin(){return StrBlobPtr (*this);}
StrBlobPtr end()
{
auto ret = StrBlobPtr(*this,data->size());
return ret;
}
};
7.3.StrBlobPtr類的測驗
完整代碼如下:
#ifndef MY_STRBLOB_H
#define MY_STRBLOB_H
#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
#include <stdexcept>
using namespace std;
//提前宣告,StrBlob中的友類宣告所需
class StrBlobPtr;
class StrBlob {
friend class StrBlobPtr;
public:
typedef vector<string>::size_type size_type;
StrBlob();
StrBlob(initializer_list<string>il);
size_type size()const {
return data->size();
}
bool empty()const {
return data->empty();
}
//添加和洗掉元素
void push_back(const string &t) {
data->push_back(t);
}
void pop_back();
//元素訪問
string &front();
const string &front()const;
string &back();
const string &back()const;
//提供給StrBlobPtr的介面
StrBlobPtr begin();//這是宣告,定義StrBlobPtr后才能定義這兩個函式
StrBlobPtr end();
private:
shared_ptr<std::vector<std::string>>data;
//如果data[i]不合法,拋出一個例外
void check(size_type i, const std::string &msg)const;
};
inline StrBlob::StrBlob(): data(make_shared<vector<string>>()) {}
StrBlob::StrBlob(initializer_list<string>il): data(make_shared<vector<string>>(il)) {}
inline void StrBlob::check(size_type i, const string &msg) const {
if (i >= data->size())
throw out_of_range(msg);
}
inline string&StrBlob::front()
{
//如果vector為空,check會拋出一個例外
check(0,"front on empty StrBlob");
return data->front();
}
//const版本front
inline const string &StrBlob::front()const {
check(0, "front on empty StrBlob");
return data->front();
}
inline string &StrBlob::back() {
check(0, "back on empty StrBlob");
return data->back();
}
//const 版本 back
inline const string &StrBlob::back()const {
check(0, "back on empty StrBlob");
return data->back();
}
inline void StrBlob::pop_back() {
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
//當試圖訪問一個不存在的元素時,StrBlobPtr拋出一個例外
class StrBlobPtr {
friend bool eq(const StrBlobPtr &, const StrBlobPtr &);
public:
StrBlobPtr(): curr(0) {}
StrBlobPtr(StrBlob &a, size_t sz = 0): wptr(a.data), curr(sz) {}
string &deref()const;
StrBlobPtr &incr();//前綴遞增
StrBlobPtr &decr();//前綴遞減
private:
//若檢查成功,check回傳一個指向vector的shared_ptr
shared_ptr<vector<string>>
check(size_t, const string &)const;
//保存一個weak_ptr,意味著底層vector可能會被銷毀
weak_ptr<vector<string>>wptr;
size_t curr;//在陣列中的當前位置
};
inline
shared_ptr<vector<string>>
StrBlobPtr::check(size_t i, const string &msg)const {
auto ret = wptr.lock();//vector還存在嗎?
if (!ret)
throw runtime_error("unbound StrBlobPtr");
if (i >= ret->size())
throw out_of_range(msg);
return ret;
}
inline string &StrBlobPtr::deref()const {
auto p = check(curr, "dereference past end");
return (*p)[curr];
}
//前綴遞增:回傳遞增后的物件的參考
inline StrBlobPtr &StrBlobPtr::incr() {
//如果curr已經指向容器的尾后位置,就不能遞增它
check(curr, "increment past end of StrBlobPtr");
++curr;//推進當前位置
return *this;
}
//前綴遞減:回傳遞減后的物件的參考
inline StrBlobPtr &StrBlobPtr::decr() {
//如果curr已經為0,遞減它就會產生一個非法下標
--curr; //遞減當前位置
check(-1, "decrement past begin of StrBlobPtr");
return *this;
}
//StrBlob的begin和end成員的定義
inline StrBlobPtr StrBlob::begin() {
return StrBlobPtr(*this);
}
inline StrBlobPtr StrBlob::end() {
auto ret = StrBlobPtr(*this, data->size());
return ret;
}
//StrBlobPtr的比較操作
inline
bool eq(const StrBlobPtr &lhs, const StrBlobPtr &rhs) {
auto l = lhs.wptr.lock(), r = rhs.wptr.lock();
//若底層的vector是同一個
if (l == r)
//則兩個指標都是空,或者指向相同的元素時,它們相等
return (!r || lhs.curr == rhs.curr);
else
return false;//若指向不同vector,則不可能相等
}
inline
bool neq(const StrBlobPtr &lhs, const StrBlobPtr &rhs) {
return !eq(lhs, rhs);
}
#endif
主程式代碼:
#include <iostream>
using namespace std;
#include "my_StrBlob.h"
int main(int argc, char **argv) {
StrBlob b1;
{
StrBlob b2 = {"a", "an", "the"};
b1 = b2;
b2.push_back("about");
cout << b2.size() << endl;
}
cout << b1.size() << endl;
cout << b1.front() << " " << b1.back() << endl;
const StrBlob b3 = b1;
cout << b3.front() << " "<<b3.back() << endl;
for (auto it = b1.begin(); neq(it, b1.end()); it.incr())
cout << it.deref() << endl;
return 0;
}
測驗結果:

這次我們用檔案讀入的方式測驗:

代碼如下:
#include <iostream>
#include <fstream>
using namespace std;
#include "my_StrBlob.h"
int main(int argc, char **argv) {
ifstream in(argv[1]);
if (!in) {
cout << "無法打開輸入檔案" << endl;
return -1;
}
StrBlob b;
string s;
while (getline(in, s))
b.push_back(s);
for (auto it = b.begin(); neq(it, b.end()); it.incr())
cout << it.deref() << endl;
system("pause");
return 0;
}

7.4.小插曲:改進StrBlobPtr的建構式
我們不能將StrBlobPtr系結到一個const StrBlob物件,這個限制是由于建構式接受一個非const StrBlob物件的參考而導致的
現在我們來設計一個可以接受const StrBlob物件的參考的建構式,
首先,為StrBlobPtr定義能接受const StrBlob &引數的建構式:
StrBlob(const StrBlob &a,size_t sz = 0):wptr(a.data),curr(sz){}
其次,為StrBlob定義能操作const物件的begin和end,
宣告:
StrBlobPtr begin() const;
StrBlobPtr end() const;
定義:
inline StrBlobPtr StrBlob::begin()const
{
return StrBlobPtr(*this);
}
inline StrBlobPtr StrBlob::end()const
{
auto ret = StrBlobPtr(*this,data->size());
return ret;
}
- 本人水平有限,如有錯誤,還請在評論區留言指出,謝謝!!!
- 創作不易,留個贊再走唄!!!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/274527.html
標籤:其他
