《C++ Primer》學習筆記(十二):動態內存
程序用堆來存儲動態分配的對象,當動態對象不再使用時,我們的代碼必須顯式地銷燬它們。
動態內存與智能指針
C++中動態內存的管理是通過new
和delete
來完成的。爲了更安全地使用動態內存,提供了兩種智能指針類型來管理動態對象,均定義在memory
頭文件中。
shared_ptr
:允許多個指針指向同一個對象;unique_ptr
: 獨佔所指向地對象;weak_ptr
:是一種指向shared_ptr
所管理對象的弱引用。
shared_ptr
//指向一個值爲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 p = make_shared<int>(42);//p指向的對象只有p一個引用者
auto q(p);//p和q指向相同對象,此對象有兩個引用者
每個shared_ptr
都有一個關聯的計數器,稱爲引用計數。一旦引用計數變爲0,它就會自動釋放自己所管理的對象。
void use_factory(T arg)
{
shared_ptr<Foo> p = factory(arg);
//使用p
}//p離開了作用域,它指向的內存會被自動釋放掉
注意:如果你將shared_ptr
存放於一個容器中,而後不再需要全部元素,而只使用其中一部分,要記得用erase
刪除不再需要的那些元素。
使用動態內存的一個常見原因是允許多個對象共享相同的狀態。
class StrBlob{
public:
typedef std::vector<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;
//如果data[i]不合法,拋出一個異常
void check(size_type i, const std::string &msg) const;
};
StrBlob::StrBlob():data(make_shared<vector<string>>()) {}
StrBlob::StrBlob(std::initializer_list<string> il): data(make_shared<vector<string>>(il)) {}
void StrBlob::check(size_type i, const std::string &msg) const
{
if(i >= data->size()){
throw out_of_range(msg);
}
}
string& StrBlob::front()
{
//如果vector爲空,check會拋出一個異常
check(0, "front on empty StrBlob");
return data->front();
}
string& StrBlob::back()
{
//如果vector爲空,check會拋出一個異常
check(0, "back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back()
{
//如果vector爲空,check會拋出一個異常
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
StrBlob
類只有一個shared_ptr
類型的數據成員,因此當我們拷貝、賦值或銷燬一個StrBlob
對象時,它的shared_ptr
成員會被拷貝、賦值或銷燬。
拷貝一個shared_ptr
會遞增其引用計數;將一個shared_ptr
賦予另一個shared_ptr
會遞增賦值號右側的shared_ptr
的引用計數,而遞減左側shared_ptr
的引用計數。
內存耗盡
如果new
不能分配所要求的內存空間,會拋出一個bad_alloc
類型的異常。我們可以改變使用new
的方式來阻止它拋出異常:
int *p1 = new int(10); //如果分配失敗,拋出`bad_alloc`類型的異常
int *p2 = new (nothrow) int(10); //如果分配失敗,`new`返回一個空指針
shared_ptr與new結合使用
可以用new
返回的指針來初始化智能指針,但是要注意接受指針參數的智能指針的構造函數是explicit
的,因此必須使用直接初始化形式。出於相同的原因,一個返回shared_ptr
的函數不能在其返回語句中隱式轉換一個普通指針。
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>
}
注意:當我們將一個shared_ptr
綁定到一個普通指針後,我們就不應該再使用內置指針來訪問shared_ptr
所指向的內存了,否則可能發生意想不到的錯誤。
注意:get
用來將指針的訪問權限傳遞給代碼,只有在確定代碼不會delete
該指針的情況下,才能使用get
。特別的,永遠不要用get
獲得的指針來初始化另一個智能指針。
shared_ptr<int> p(new int(42)); //引用計數爲1
int *q = p.get(); // 正確:但是使用q時要注意,不要讓它管理的指針被釋放
{//新程序塊
//未定義:兩個獨立的shared_ptr指向相同的內存
shared_ptr<int>(q);
}//程序塊結束,引用計數減1,q被銷燬,它指向的內存被釋放
int foo = *p; //未定義:p指向的內存已經被釋放了
智能指針和異常
包括所有標準庫在內的很多C++類都定義了析構函數,負責清理對象使用的資源。但不是所有的類都是這樣良好定義的。特別是那些爲C和C++兩種語言設計的類,通常都需要用戶顯式地釋放所使用的任何資源,稱這種類爲啞類。
對於啞類,同樣可以使用shared_ptr
來管理,這是我們需要首先定義一個函數來代替delete
,這個刪除器(deleter)函數必須能夠完成對shared_ptr
中保存的指針進行釋放的操作。例如對於一個connection
類:
void end_connection(connection *p) {disconnect(*p);}
void f(destination &d)//其他參數
{
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
//使用連接
//當f退出時(即使是由於異常退出),connection會正確關閉
}
爲了正確使用智能指針,需要遵守一些基本規範:
- 不使用相同的內置指針初始化(或
reset
)多個智能指針。 - 不
delete get()
返回的指針。 - 不使用
get()
初始化或reset
另一個智能指針 - 如果你使用了
get()
返回的指針,記住當最後一個對應的智能指針銷燬後,你的指針就變爲無效了。 - 如果你使用智能指針來管理的資源不是
new
分配的資源,記住傳遞給它一個刪除器(deleter)。
unique_ptr
與shared_ptr
不同,某個時刻只能有一個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));
}
weak_ptr
weak_ptr
是一種不控制所指向對象生存期的智能指針,它指向由一個shared_ptr
管理的對象。將一個weak_prt
綁定到一個shared_ptr
不會改變shared_ptr
的引用計數。
當我們創建一個weak_ptr
時,要用一個shared_ptr
來初始化它:
auto p = make_shared<int>(42);
weak_ptr<int> wp(p);//wp弱共享p;p的引用計數未改變
我們不能使用weak_ptr
直接訪問對象,而必須調用lock
。
if(shared_ptr<int> np = wp.lock()) //如果np不爲空則條件成立
{
//在if中,np與p共享對象
}
shared_ptr
是採用引用計數的智能指針,多個shared_ptr
實例可以指向同一個動態對象,並維護了一個共享的引用計數器。對於引用計數法實現的計數,總是避免不了循環引用(或環形引用)的問題,shared_ptr
也不例外。爲了解決類似這樣的問題,C++11引入了weak_ptr
,來打破這種循環引用。
舉個例子:
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class ClassB;
class ClassA
{
public:
ClassA() { cout << "ClassA Constructor..." << endl; }
~ClassA() { cout << "ClassA Destructor..." << endl; }
shared_ptr<ClassB> pb; // 在A中引用B
};
class ClassB
{
public:
ClassB() { cout << "ClassB Constructor..." << endl; }
~ClassB() { cout << "ClassB Destructor..." << endl; }
shared_ptr<ClassA> pa; // 在B中引用A
};
int main() {
shared_ptr<ClassA> spa = make_shared<ClassA>();
shared_ptr<ClassB> spb = make_shared<ClassB>();
spa->pb = spb;
spb->pa = spa;
// 函數結束,思考一下:spa和spb會釋放資源麼?
}
上面代碼的輸出如下:
ClassA Constructor...
ClassB Constructor...
Program ended with exit code: 0
從上面代碼中,ClassA和ClassB間存在着循環引用,從運行結果中我們可以看到:當main
函數運行結束後,spa
和spb
管理的動態資源並沒有得到釋放,產生了內存泄露。
使用weak_ptr
來改造前面的代碼,可以打破循環引用問題。
class ClassB;
class ClassA
{
public:
ClassA() { cout << "ClassA Constructor..." << endl; }
~ClassA() { cout << "ClassA Destructor..." << endl; }
weak_ptr<ClassB> pb; // 在A中引用B
};
class ClassB
{
public:
ClassB() { cout << "ClassB Constructor..." << endl; }
~ClassB() { cout << "ClassB Destructor..." << endl; }
weak_ptr<ClassA> pa; // 在B中引用A
};
int main() {
shared_ptr<ClassA> spa = make_shared<ClassA>();
shared_ptr<ClassB> spb = make_shared<ClassB>();
spa->pb = spb;
spb->pa = spa;
// 函數結束,思考一下:spa和spb會釋放資源麼?
}
輸出結果如下:
ClassA Constructor...
ClassB Constructor...
ClassA Destructor...
ClassB Destructor...
Program ended with exit code: 0
動態數組
大多數應用應該使用標準庫容器而不是動態分配的數組。使用容器更加簡單,更不容易出錯,且一般有更好的性能。
int *pia = new int[get_size()];//pia指向第一個int
delete[] pia;
注意:動態數組並不是數組類型
標準庫提供了一個可以管理new
分配的數組的unique_ptr
版本。
//up指向一個包含10個未初始化int的數組
unique_ptr<int[]> up(new int[10]);
up.release(); //自動調用delete[]銷燬其指針
allocator類
標準庫allocator
類定義在memory
頭文件中,幫助我們將內存分配與對象構造分離開來。它分配的內存是原始的、未構造的。
allocator<string> alloc;//可以分配string的allocator對象
auto const p = alloc.allocate(n);//分配n個未初始化的string
auto q = p;//q指向最後構造的元素之後的位置
alloc.construct(q++); //*q爲空字符串
alloc.construct(q++, 10, 'c'); //*q爲cccccccccc
alloc.construct(q++, "hi"); //*q爲hi
注意:爲了使用allocate
返回的內存,我們必須用construct
構造對象。使用未構造的內存,其行爲是未定義的。
當我們用完對象後,必須對每個構造的元素調用destroy
來銷燬它們。但要注意只能對真正構造了的元素進行destroy
操作。
while( q != p)
alloc.destroy(--q); //釋放我們真正構造的string
一旦元素被銷燬後,就可以重新使用這部分內存來保存其他string
,也可以通過調用deallocate
來釋放內存,歸還給系統。
alloc.deallocate(p, n);
//分配比vi中的元素所佔用空間大一倍的動態內存
auto p = alloc.allocate(vi.size() * 2);
//通過拷貝vi中的元素來構造從p開始的元素
auto q = uninitialized_copy(vi.begin(), vi.end(), p);
//將剩餘元素初始化爲42
uninitialized_fill_n(q, vi.size(), 42);
傳遞給uninitialized_copy
的目的位置迭代器必須指向未構造的內存。與copy
不同,uninitalized_copy
在給定目的位置構造元素。uninitalized_copy
的返回值是一個指針,指向最後一個構造元素之後的位置。
使用標準庫設計文本查詢程序
當我們設計一個類時,在真正實現成員之前先編寫程序使用這個類,可以幫助我們看到類是否有我們需要的操作。例如下面的函數接受一個指向要處理文件的ifstream
,並與用戶交互,打印給定單詞的查詢結果。
void runQueries(ifstream &infile)
{
TextQuery tq(infile);//保存文件並建立查詢map
//與用戶交互:提示用戶輸入要查詢的單詞,完成查詢並打印結果
while(true)
{
cout << "Enter word to look for, or q to quit:";
string s;
//若遇到文件尾或用戶輸入了`q`時循環終止
if( !(cin >> s) || s=="q") break;
//指向查詢並打印結果
print(cout, tq.query(s)) << endl;
}
}
class QueryResult;//爲了定義函數query的返回類型,這個定義是必須的
class TextQuery
{
public:
using line_no = std::vector<std::string>::size_type;
TextQuery(std::ifstream&);
QueryResult query(const std::string&) const;
private:
std::shared_ptr<std::vector<std::string>> file; //輸入文件
//每個單詞到它所在的行號的集合的映射
std::map<std::string, std::shared_ptr<std::set<line_no>>> wm;
};
TextQuery::TextQuery(ifstream &is):file(new vector<string>)
{
string text;
while(getline(is, text)){
file->push_back(text); //保存此行文本
int n = file->size() - 1; //當前行號
istringstream line(text);//將行文本分解爲單詞
string word;
while(line >> word){//對行中每個單詞
//如果單詞不在wm中,以之爲下標在wm中添加一項
auto &lines = wm[word]; //lines是一個shared_prt
if(!lines) //在我們第一次遇到這個單詞時,此指針爲空
lines.reset(new set<line_no>); // 分配一個新的set
lines->insert(n); //將此行號插入set中
}
}
}
QueryResult TextQuery::query(const string &sought) const
{
//如果未找到sought,我們就返回下面這個局部static對象
static shared_ptr<set<line_no>> nodata(new set<line_no>);
//使用find而不是下標運算符來查找單詞,避免將單詞添加到wm中
auto loc = wm.find(sought);
if(loc == wm.end())
return QueryResult(sought, nodata, file); //未找到
else
return QueryResult(sought, loc->second, file);
}
class QueryResult
{
friend std::ostream& QueryResultPrint(std::ostream&, const QueryResult&);
public:
using line_no = std::vector<std::string>::size_type;
QueryResult(std::string s,
std::shared_ptr<std::set<line_no>> p,
std::shared_ptr<std::vector<string>> f):
sought(s), lines(p), file(f) { }
private:
std::string sought; //查詢單詞
std::shared_ptr<std::set<line_no>> lines; //出現的行號
std::shared_ptr<std::vector<std::string>> file; //輸入文件
};
std::ostream &QueryResultPrint(std::ostream &os, const QueryResult &qr)
{
//如果找到了單詞,打印出現的次數和所有出現位置
os << qr.sought << " occurs " << qr.lines->size() << " "
<< ( qr.lines->size() > 1 ? "times": "time") << endl;
//打印單詞出現的每一行
for(auto num : *qr.lines)
//避免行號從0開始給用戶帶來疑惑
os << "\t(line "<< num+1 <<") "
<< *(qr.file->begin() + num) << endl;
return os;
}
練習
- 如果你試圖拷貝或賦值
unique_ptr
,編譯器並不總能給出易於理解的錯誤信息。編寫包含這種錯誤的程序,觀察編譯器如何診斷這種錯誤。
error C2248: “std::unique_ptr<_Ty>::unique_ptr”: 無法訪問 private 成員(在“std::unique_ptr<_Ty>”類中聲明)
- 下面的
unique_ptr
的聲明中,哪些是合法的。哪些可能導致後續的程序錯誤?
int ix = 1024, *pi = &ix, *pi2 = new int(2018);
typedef unique_ptr<int> IntP;
(a) IntP p0(ix);
(b) IntP p1(pi);
(c) IntP p2(pi2);
(d) IntP p3(&ix);
(e) IntP p4(new int(2048));
(f) IntP p5(p2.get());
(a):不合法,ix
不是new
返回的指針
(b):同上
©:合法,unique_ptr
必須採用直接初始化
(d):不合法,同(a)
(e):合法
(f):不合法,必須使用new
返回的指針進行初始化,賦值和拷貝的操作也不包含get()
方法。
- 編寫一個程序,連接兩個字符串字面常量,將結果保存在一個動態分配的
char
數組中。重寫這個程序,連接兩個標準庫string
對象。
#include<iostream>
#include <string>
#include <cstring>
using namespace std;
int main(int argc, char**argv)
{
char *s1 = "abc";
char *s2 = "efg";//字符串字面常量,字符串末尾有空格
string si = "a";
string sl = "b";//標準庫string對象
char *p = new char[strlen(s1)+strlen(s2)+1];//必須指明要分配對象的個數
strcpy(p,s1);//複製
strcat(p,s2);//拼接並覆蓋p中已有的terminating null character
cout<<p<<endl;
strcpy(p,(si+sl).c_str());//必須轉換爲c類型字符串(c中無string類型)
cout<<p<<endl;
delete [] p;
system("pause");
return 0;
}