《C++ Primer》學習筆記(十二):動態內存


程序用堆來存儲動態分配的對象,當動態對象不再使用時,我們的代碼必須顯式地銷燬它們。

動態內存與智能指針

C++中動態內存的管理是通過newdelete來完成的。爲了更安全地使用動態內存,提供了兩種智能指針類型來管理動態對象,均定義在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函數運行結束後,spaspb管理的動態資源並沒有得到釋放,產生了內存泄露。

使用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;
}

練習

  1. 如果你試圖拷貝或賦值unique_ptr,編譯器並不總能給出易於理解的錯誤信息。編寫包含這種錯誤的程序,觀察編譯器如何診斷這種錯誤。
 error C2248: “std::unique_ptr<_Ty>::unique_ptr”: 無法訪問 private 成員(在“std::unique_ptr<_Ty>”類中聲明)
  1. 下面的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()方法。

  1. 編寫一個程序,連接兩個字符串字面常量,將結果保存在一個動態分配的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;  
} 

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章