详解c++动态内存new/delete、智能指针shared_ptr、unique_ptr、weak_ptr

可编程内存基本上可分为以后三个大部分:

  • 静态存储区
    内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。保存局部 static 对象、类 static 数据成员 以及任何定义在函数之外的变量(即全局变量或常量)。
  • 栈区
    在执行函数时,函数内局部非static变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 堆区
    也称动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或delete释放内存。动态内存的生存期可以由我们决定,即当动态对象不再使用时,我们的代码必须显示的销毁它们 ,如果我们不释放内存,程序将在最后才释放掉动态内存。

动态内存与智能指针:
c++中动态内存时通过一对运算符来完成的:new,在动态内存中为对象分配内存空间并返回一个指向该对象的指针,我们可以选择对象进行初始化;delete,接受一个动态对象的指针,销毁该对象并释放与之关联的内存。

c++11标准库中提供了两种智能指针类型来管理动态内存对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr 允许多个指针指向同一个对象;unique_ptr 则 “独占” 所指向的对象。标准库还定义了一个名为 weak_ptr 的伴随类,它是一个弱引用,指向 shared_ptr 所管理的对象。

1、直接管理内存 new/delete

默认情况下,动态内存new分配的对象是默认初始化的,即,内置类型或组合类型的对象的值将是未定义的(随机值),而类类型对象将用默认构造函数进行初始化。
释放内存时,传递给 delete 的指针必须指向动态分配的内存,或者是一个空指针。

{
	int* pi = new int;
	delete pi;
}

{
	int* pi = new int[3];
	//delete pi;   //对于基本数据结构,也能正确。
	delete[] pi;   // 应该正确使用数组的释放方法 delete[] obj
}

{
	int* pi = new int[3]{ 0,1,2 };  // 列表初始化
	delete[] pi;  
	std::string* ps = new string[2]{ "abc","123" };
	//delete ps;  // 报错
	delete[] ps;   
}

{
	int obj = 1024;
	int obj1 = 1, obj2 = 2, obj3 = 3;
	auto p1 = new auto(obj);
	//auto p2 = new auto{obj1,obj2,obj3};  //vs2015不能编译成功
}

内存耗尽的处理

try {
	// 内存不够,默认情况选会抛出一个类型为 bad_alloc 的异常。
	// 需要手动捕获错误,使程序正常执行
	int* pi = new int[std::numeric_limits<long long>::max()];   
	std::cout << pi << std::endl;
}
catch (std::exception& e) {
	std::cout << e.what() << endl;
}

{
	// 使用定位new,在内存分配失败时不抛出异常,返回空指针 
	int* pi = new(nothrow) int[std::numeric_limits<long long>::max()];  
	std::cout << pi << std::endl;   //0x0
}

delete 之后重置指针:
delete之后指针就变成了空悬指针(不为空),若还有其他对象在使用(包括二次释放)会造成未定义的错误。若无法保证是否还有其他对象使用,在 delete 之后将 nullptr 赋予指针,这样就清楚地指出指针不指向任何对象。
另外,重复delete一个nullptr指针是安全的。

	int *p = new int();
	*p = 1;

	delete p;  // delete后p为野指针
	{
		p = nullptr;
		delete p;    // delete一个空指针是没有错误的
	}

2、智能指针shared_ptr

2.1 基本使用

智能指针也是模板,使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个判断条件中使用智能指针,效果就是检查它是否为空。

最安全的分配和使用动态内存的方法是调用一个名为 make_shared 的标准库函数。

{
    std::shared_ptr<std::string> p1;//shared_ptr,可以指向string
    std::shared_ptr<std::list<int>> p2;//shared_ptr,可以指向int的list
 
    if(p1 && p1->empty()) 
        *p1 = "hi";  //如果p1指向一个空string,解引用p1,将一个新值赋予string
}

{
	std::shared_ptr<char*> pChar;  // 空指针
	pChar = std::make_shared<char*>(new char[10]);
	auto xxx = *pChar; //解引用,指向管理对象,也就是char*内存
	auto yyy = pChar.get(); // 同xxx, 返回对象指针
}

当进行拷贝和赋值操作时,每个 shared_ptr 都会记录有多少个其它 shared_ptr 指向相同的对象。每个 shared_ptr 都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个 shared_ptr,计数器都会递增。如,当用一个 shared_ptr 初始化另一个 shared_ptr,或将它作为参数传递给另一个函数以及作为函数的返回值时,它所关联的计数器都会递增。当我们给 shared_ptr 赋予一个新值或是 shared_ptr 被销毁(如,一个局部的 shared_ptr 离开其作用域)时,计数器都会递减。一旦一个 shared_ptr 的计数器变为 0,它就会自动释放自己所管理的对象(基本数据直接释放内存,类对象通常会自动调用其析构函数)。

{
	std::shared_ptr<int> ptr1(new int(1));  // 引用计数为1
	std::shared_ptr<int> ptr2 = ptr1;   //  ptr2共享ptr1的指向对象, 引用计数为2

	ptr2.reset();  // ptr2置为空   //引用计数减为1

	int *pi = new int(2);
	ptr2.reset(pi);  // ptr2指向pi内存
}

使用了动态生存,处于以下三种原因之一:
1.程序不知道自己需要使用多少对象
2.程序不知道所需对象的准确类型
3.程序需要在多个对象间共享数据

分析第 3 种情况:之前用过的类中,分配的资源都与对应对象生存期一致。如:每个 vector “拥有” 其自己的元素。当我们拷贝一个 vector 时,原 vector 和 副本 vector 中的元素是相互分离的:

vector<string> v1;//空vector
{//新作用域
    vector<string> v2 = {"f", "jf"};
    v1 = v2;//从v2拷贝元素到v1中
 }//离开作用域,v2被销毁,其中元素也被销毁
 //v1中有2个元素,是原来v2中元素的拷贝

但某些类分配的资源具有与原对象相独立的生存期。如,假定我们希望定义一个名为 Blob 的类,保存一组元素。与容器不同,我们希望 Blob 对象的不同拷贝之间共享相同的元素。即,当我们拷贝一个 Blob 时,原 Blob对象及其拷贝应该引用相同的底层元素。

通常,如果两个对象共享底层数据,当某个对象被销毁时,我们不能单方面销毁底层数据:

 Blob<string> b1;//空Blod
 {//新作用域
     Blob<string> b2 = {"f", "jf"};
     v1 = v2;//从b2拷贝元素到b1中
 }//离开作用域,b2被销毁,但其中元素不能销毁
 //b1指向最初由b2创建的元素

注意:b1 和 b2 共享相同的元素,当 b2 离开作用域时,这些元素必须保留,因为 b1 仍任在使用它们

2.2 使用动态内存资源的类示例

定义一个管理 string 的 Blob类,命名为 strBlob,当我们拷贝、赋值或销毁一个 strBlob 对象时,它的 shared_ptr 成员会被拷贝、赋值或销毁。因此,对于由 strBlod 构造函数分配的 vector,当最后一个指向它的 strBlob 对象被销毁时,它也会随之被自动销毁。
使用了动态生存期的资源的类StrBlob定义如下

class StrBlob {
public:
	using size_type = std::vector<std::string>::size_type;

	//StrBlob();
	StrBlob(std::initializer_list<std::string> il) :
		data(std::make_shared<std::vector<std::string>>(il))
	{
	}  // 注意:使用了initializer_list初始化列表构造函数

	size_type size() const { return data->size(); }
	bool empty() const { return data->empty(); }

	void push_back(const std::string& str) { data->push_back(str); }

	void pop_back() { check(0, "pop_back on empty StrBlob"); data->pop_back(); }

	// 这里没有修改成员对象的值,是调用成员的成员函数修改内部值
	void pop_back()const { check(0, "pop_back on empty StrBlob"); data->pop_back(); }


	//std::string& front(){	check(0, "front on empty StrBlob");	return data->front();}
	//std::string& back()	{check(0, "back on empty StrBlob");	return data->back();}

	// const函数, 建议返回const引用
	std::string& front() const { check(0, "front on empty StrBlob");return data->front(); }
	std::string& back() const { check(0, "back on empty StrBlob");	return data->back(); }

private:
	std::shared_ptr<std::vector<std::string>> data;

	void check(size_type i, const std::string& msg) const
	{
		if (i >= data->size())	throw std::out_of_range(msg);
	}
};

测试代码如下

{
	StrBlob strBlob = { "123","abc" };   //初始化列表构造, ref = 1
	strBlob.push_back(",.?");
	while (!strBlob.empty()) {
		auto& str = strBlob.back();   // 非const对象,先找非const函数实现,若无再找const函数
		std::cout << str << std::endl;
		strBlob.pop_back();
	}
	{
		StrBlob b = strBlob;   //ref = 2    data数据共用
		b.push_back("a");
	} // b释放后,由于底层数据共享,所有a中保留b操作添加的元素

	strBlob.push_back("a"); // 两个元素 ["a","a"]
}

{
	const StrBlob strBlob = { "123","abc" };
	// const 对象只能使用const函数
	while (!strBlob.empty()) {
		auto& str = strBlob.back();
		str = "aaa"; 
		std::cout << str << std::endl;
		strBlob.pop_back();
	}
}

2.3 shared_ptr和new的结合使用

接受指针参数的智能指针构造函数是 explicit 的,我们不能将一个内置指针隐式转化一个智能指针,必须使用直接初始化形式来初始化一个智能指针。

{
	std::shared_ptr<int> p1;     // 空指针
	//不能显示地转换
	p1 = std::make_shared<int>(22);

	//std::shared_ptr p2 = new int(21); //必须使用直接初始化
	std::shared_ptr<int> p2 = std::make_shared<int>(21);
	std::shared_ptr<int> p3(new int(20));
}

另外,可将shared_ptr绑定到一个想要返回的指针上

std::shared_ptr clone(int p)
{
	return std::shared_ptr<int>(new int(p));
}

不要混合使用普通指针和智能指针:
当将一个 shared_ptr 绑定到一个普通指针时,我们就将内存的管理交给了这个 shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问 shared_ptr 所指向的内存了。

auto process = [](std::shared_ptr<int> ptr) {//值传参,拷贝后引用计数+1
	*ptr += 1;
};//离开ptr作用域,ptr被销毁,其所指对象引用计数-1

{
	std::shared_ptr<int> p(new int(1));  // ref 1
	process(p);    // 调用时ref 2, 返回时 ref 1
	int res = *p;
}

{ // 不能传递一个内置指针给process函数,但是可以传递一个临时的shared_ptr
  // 这个shared_ptr是内置指针显示构造,很可能导致错误
	int *x(new int(1));
	// process(x);	// 不能从int* 转换为 shared_ptr
	process(std::shared_ptr<int>(x)); // 用内置指针显示构造一个临时shared_ptr接管, 函数返回时会释放x的内存
	int res = *x;						// 未定义,x目前是一个空悬指针
}

不要使用 get 初始化另一个智能指针或为智能指针赋值:
get 通常用于将 shared_ptr 中保存的值赋给普通指针,只有在确定代码不会 delete 指针的情况下才能用 get。
两个相互独立但指向相同的内存的 shared_ptr 各自的引用计数都是 1,着意味着当其中一个自动销毁但另一个没被销毁时,没被销毁的 shared_ptr 会成为空悬指针。

{
	// get()返回内置指针,用于不接受智能指针的代码, 
	std::shared_ptr<int> p(new int(1));
	int *x = p.get(); //正确,但使用时要注意,不要让它管理的指针被释放
	{//新作用域
		std::shared_ptr<int>(x);//未定义,两个独立的shared_ptr指向相同的内存
	}//程序块结束,p被销毁,它指向的内存被释放
	int foo = *p;  //未定义,p指向的内存已经被释放了
}

其它 shared_ptr 操作:
与赋值类似,reset 会更新引用计数,如果需要的话,会释放 p 指向的对象。reset 经常与 unique 其使用,来控制多个 shared_ptr 共享的对象。

{ // 修改智能指针的底层数据,先要判断是否有其他用户使用
	std::shared_ptr<int> p(new int(1));
	std::shared_ptr<int> p2(p);

	if (!p.unique()) {
		// 不是唯一的用户,分配新的拷贝
		// p.reset(new int(2));    
		p = std::make_shared<int>(2);  // 效果同上
	}
	*p += 1; //唯一用户,可以改变,不影响其他对象的值

	std::cout << *p2 << "  " << *p << std::endl;  // 1  3
}

删除器:
当一个 shared_ptr 管理的对象被释放时,默认是进行 delete 操作,而 delete 操作是调用对应对象的析构函数来完成的。
但是,对于分配了资源而又没有定义析构函数的类,我们就需要定义一个函数来代替 delete 完成对 shared_ptr 中保存的指针进行释放操作,即删除器。
例如,定义一个具有分配资源的类但无自定义析构的类DataMem

class DataMem {
public:
	//DataMem();
	DataMem(size_t sz) : p(new int[10]) {}
	// ~DataMem() { release(); }

	int* getPtr() { return p; }
	void release() { delete[] p;  p = nullptr; }
private:
	int* p;
};

使用内置指针管理内存,且在 new 之后再对应的 delete 之前发生了异常,则内存不会被释放,如下。

void f(){
	int ip = new int(42);
    //...//此处发生一个异常且未在f中被捕获
    delete ip;  // 退出前释放内存
}

在这里,当函数退出(正常结束或发生异常)时,无析构函数时的类DataMem对象都会被销毁,但是已经分配的资源不会释放。

int *ptr = nullptr;
try {

	{
		DataMem dm = DataMem(10);   // 局部对象, 无析构函数
		ptr = dm.getPtr();

		//使用代码
		// ....
		throw 1;          // 假装有异常 , 局部对象无析构函数,不会执行异常抛出后面的代码
		// ....

		//  不要忘记释放 (异常发生后不会执行,导致内存泄漏) 
		dm.release();
	}

	//	{
	//		DataMem dm = DataMem(10);   // 局部对象,有析构函数
	//		ptr = dm.getPtr();

	//		//使用代码
	//		// ....
	//		throw 1;          // 假装有异常 , 局部对象有析构函数,会执行析构函数(用户确保对象动态内存释放)
	//		// ....
	//	}
		
	{
		// 创建一个删除器作为智能指针的自定义操作参数,类型为对象指针的函数
		//  删除器原型 void funcName(DataMem* dm) {  ...;  }
		auto ReleaseMem = [](DataMem* dm)-> void {
			dm->release();
		};

		DataMem dm = DataMem(10);   // 局部对象,无析构函数
		//std::shared_ptr pdm(&dm, ReleaseMem);   // 局部对象,无析构函数; 删除器; 
		std::shared_ptr<DataMem> pdm(&dm, [](DataMem* dm) {dm->release(); });

		ptr = dm.getPtr();

		//使用代码
		// ....
		throw 1;          // 假装有异常 , 局部对象无析构,shared_ptr接管并负责用删除器回收内存
		// ....
	}
}
catch (int e) {
	std::cout << "catch throw " << e << std::endl;
}
catch (std::exception& e) {
	std::cout << e.what() << std::endl;
}

Tip:只要自己定义类时保证析构函数能正确释放类中分配资源,就不再需要使用删除器了。

2.4 智能指针的陷阱:

智能指针的安全性是建立在正确使用的前提下的,我们必须坚持一些基本规范:

  • 不使用相同的内置指针初始化(或 reset)多个智能指针。这会造成前面我们提到的相互独立的 shared_ptr 之间共享相同地址的问题
  • 不 delete get() 返回的指针
  • 不适用 get() 初始化或 reset 另一个智能指针
  • 如果使用 get() 返回的指针,记住当最后一个对应的 shared_ptr 销毁后,之前 get() 返回的的指针就无效了
  • 如果使用智能指针管理的不是 new 分配的内存,必须传给它一个删除器
    (析构函数能正确释放类中资源可不使用删除器)

3、智能指针unique_ptr

与 shared_ptr 不同,某个时刻只能有一个 unique_ptr 指向给定的对象。当 unique 被销毁时,它所指向的对象也被销毁。

基本操作
unique_ptr u1   空 unique_ptr,可以指向一个类型为 T 的对象。u1 会使用 delete 来释放它的
unique_ptr u2  指针;u2 会使用一个类型为 D 的可调用对象来释放它的指针,初始化 u2 时
           除了需要一个能转化成 T* 类型的对象外还需要一个 D 类型的对象来代替 delete
unique_ptr u(d)  空 unique_ptr,指向类型为 T 的对象,用类型为 D 的对象 d 代替 delete
u = nullptr       释放 u 指向的对象,将 u 置为空
u.release()      u 放弃对指针的控制权,返回指针,并将 u 置为空
u.reset()       释放 u 指向的对象
u.reset(q)        如果提供了内置指针 q,令 u 指向这个对象,否则将 u 置为空
u.reset(nullptr)


由于一个 unique_ptr 拥有它指向的对象,因此 unique_ptr 不支持普通的拷贝或赋值操作。初始化 unique_ptr 采用直接初始化形式、或者类似make_shared的形式(c++14)。

基本使用方式

std::unique_ptr<int> p1;
p1 = std::unique_ptr<int>(new int(1));
//p1 = std::make_unique(2); //c++14

std::unique_ptr<int> p2(new int(3));
//p1 = p2; 不允许

//p1 = nullptr; // 释放p1指向对象
p1.reset();   // 释放p1指向对象,效果同上

int *ptr = p2.release(); // 仅放弃控制权,p2为空指针,并返回指向对象的内置指针
delete ptr; //若使用了release, 需要自己管理内存数据

如果 unique_ptr 不为空,则调用 reset 会释放其原来所指的内存。而 release 则不会释放 unique_ptr 原来所指的内存。调用 release 会切断 unique_ptr 和它原来所指对象间的联系,如果我们不是用另一个智能指针来保存 release 返回的指针,则我们的程序就要负责资源的释放。

std::unique_ptr<int> p3(new int(4));
std::unique_ptr<int> p4(new int(5));

//auto ptr = p4.release(); // p4释放对象的控制权,返回对象内置指针
//p3.reset(ptr);           // p3释放对象的内存,并用原p4的对象指针初始化并接管其内存
p3.reset(p4.release());   //  p4释放控制权,返回对象指针; p3释放原有对象,接管p4的对象

传递 unique_ptr 参数和返回 unique_ptr
不能拷贝 unique_ptr 的规则有一个例外,可以拷贝或赋值一个将要销毁的 unique_ptr:从函数返回一个 unique_ptr;返回一个局部对象的拷贝。

unique_ptr<int> clone_1(int p){
    return unique_ptr<int>(new int(p));
}

unique_ptr<int> clone_2(int p){
    unique_ptr<int> ret(new int(p));
    return ret;
}

这两段代码,编译器都知道要返回的对象将要被销毁。在此种情况下,编译器执行一种特殊的 “拷贝” (移动构造函数)。

删除器
类似 shared_ptr,unique_ptr 默认情况下用 delete 释放它指向的对象。同样的,我们可以重载一个 unique_ptr 中默认的删除器:
//p 指向一个类型为 objT 的对象,并使用一个类型为 delT 的对象 fnc 来释放 objT 对象
unique_ptr p(new objT, fcn);
同样以DataMem类来进行说明

class DataMem {
public:
	//DataMem();
	DataMem(size_t sz) : p(new int[10]) {}
	//~DataMem() { release(); }

	int* getPtr() { return p; }
	void release() { delete[] p;  p = nullptr; }
private:
	int* p;
};

int *ptr = nullptr;
try {

	//{
	//	DataMem dm = DataMem(10);   // 局部对象,有析构函数
	//	ptr = dm.getPtr();
	//
	//	//使用代码
	//	// ....
	//	throw 1;
	//}

	//{
	//	auto ReleaseMem = [](DataMem* dm)-> void {
	//		dm->release();
	//	};

	//	DataMem dm = DataMem(10);   // 局部对象,无析构函数
	//	//std::shared_ptr pdm(&dm, [](DataMem* dm)->void {dm->release(); });

	//	std::unique_ptr pdm(&dm, ReleaseMem); //使用删除器必须指定模板类型
	//	//std::unique_ptr pdm(&dm, ReleaseMem);  // 同上

	//	ptr = dm.getPtr();

	//	//使用代码
	//	// ....
	//	throw 1;          // 假装有异常 , 局部对象无析构,shared_ptr接管并负责用删除器回收内存
	//					  // ....
	//}

	{
		std::unique_ptr<DataMem> pdm1(new DataMem(10));
		
		// 移交指针控制权,指针初始化新的unique_ptr,并置空。
		//std::unique_ptr pdm2 = std::move(pdm1); 
		std::unique_ptr<DataMem> pdm2(std::move(pdm1));

		//类似如下
		//  std::unique_ptr pdm2;   
		//  pdm2.reset(pdm1.release());
		//可简写为  std::unique_ptr pdm2(pdm1.release());
	}
}
catch (int e) {
	std::cout << "catch throw " << e << std::endl;
}
catch (std::exception& e) {
	std::cout << e.what() << std::endl;
}

4、智能指针weak_ptr

weak_ptr 是一种不控制所指向对象生存周期的智能指针,它指向一个 shared_ptr 管理的对象。将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。一旦最后一个指向对象的 shared_ptr 被销毁,即便有 weak_ptr 指向对象,对象也还是会被释放的。

基本操作
weak_ptr w    空 weak_ptr 可以指向类型为 T 的对象
weak_ptr w(sp)  与 shared_str sp 指向相同对象的 weak_ptr。T 必须能转化为 sp 指向的对象的类型
w = p         p 可以是一个 shared_ptr 或一个 weak_ptr。赋值后 w 与 p 共享对象
w.reset()       将 w 置为空
w.use_count()    与 w 共享对象的 shared_ptr 的数量
w.expired()     若 w.use_count 为 0,返回 true,否则返回 false
w.lock()       如果 w.expired() 为 true,返回一个空 shared_ptr,否则返回一个指向 w 的对象的 shared_ptr

基本使用方式

{
	std::shared_ptr<std::string> sp(new std::string("hello, world!")); // 引用计数 1

	//std::weak_ptr wp(sp);  // wp也指向sp对象, 但不会增加引用计数, 仍为1 
	std::weak_ptr<std::string> wp = sp;   // 同上

	std::weak_ptr<std::string> wp2 = wp;   
}

{
	std::shared_ptr<std::string> sp = std::make_shared<std::string>("hello, world!"); // 引用计数
	std::weak_ptr<std::string> wp(sp);
	wp.reset();   // wp置为空, 不会对sp有任何影响, 引用计数不变换,仍为1
}

{
	std::shared_ptr<std::string> sp = std::make_shared<std::string>("hello, world!"); // 引用计数
	std::weak_ptr<std::string> wp(sp);

	sp.reset();   // sp置为空, 引用计数为0,释放对象。  wp指向是已经释放的内存,不能直接访问

	std::cout << wp.use_count() << std::endl; // 0
	std::cout << wp.expired() << std::endl; // true <=  use_count=0

	std::shared_ptr<std::string> p = wp.lock();  // 为空指针

}

使用 weak_ptr 时,由于我们不确定其所指对象是否存在,要先调用 lock 函数检查一下其所指对象是否存在。

{  //weak_ptr是弱引用,指向对象可能不存在,不能直接访问对象
	std::shared_ptr<std::string> sp1 = std::make_shared<std::string>("hello, world!");

	std::weak_ptr<std::string> wp;
	std::cout << wp.use_count() << std::endl; // 0
	std::cout << wp.expired() << std::endl; // true <=  use_count=0

	wp = sp1;
	std::cout << wp.use_count() << std::endl; // 1
	std::cout << wp.expired() << std::endl; // false <=  use_count!=0

	std::shared_ptr<std::string> sp2(sp1);
	std::cout << wp.use_count() << std::endl; // 2
	std::cout << wp.expired() << std::endl; // false <=  use_count!=0

	sp2.reset(); // sp2置空, 但不影响sp1

	// 通过 lock(), 若wp对象存在,返回一个共享对象的sp,否则返回一个空sp
	if (std::shared_ptr<std::string> p = wp.lock()) {
		std::cout << *p << std::endl;    // 引用计数为2
	}//作用于结束后引用计数减为1
}

利用 weak_ptr访问对象数据,不影响对象生命周期,同时防止操作对象已经不存在的情况
同样以StrBlob类为例,定义一个伴随指针类StrBlobPtr,保存一个wek_ptr指向StrBlob的data成员变量。通过使用weak_Ptr,不会影响一个指定的StrBlob所指向的的vector的生命周期,但是可以阻止用户访问一个已经不存在的vector的企图。

class StrBlobPtr;  //前置声明

class StrBlob {
	// 声明友元类,让StrBlobPtr对象可以访问StrBlob对象的私有变量或函数
	friend class StrBlobPtr;  
public:
	using size_type = std::vector<std::string>::size_type;

	StrBlob() = default;
	StrBlob(std::initializer_list<std::string> il) :
		data(std::make_shared<std::vector<std::string>>(il))
	{
	}

	size_type size() const { return data->size(); }
	bool empty() const { return data->empty(); }

	void push_back(const std::string& str)
	{
		if (!data) {
			data = std::make_shared<std::vector<std::string>>();
		}
		data->push_back(str);
	}

	void pop_back() { check(0, "pop_back on empty StrBlob"); data->pop_back(); }

	// 这里没有修改成员对象的值,是调用成员的成员函数修改内部值
	void pop_back() const { check(0, "pop_back on empty StrBlob"); data->pop_back(); }

	// const函数, 建议返回const引用
	const std::string& front() const { check(0, "front on empty StrBlob");	return data->front(); }
	const std::string& back() const { check(0, "back on empty StrBlob");	return data->back(); }

	// 实现必须放在 StrBlobPtr类实现之后,否则出现未定义,因为只有声明
	StrBlobPtr begin();
	StrBlobPtr end();

private:
	std::shared_ptr<std::vector<std::string>> data;

	void check(size_type i, const std::string& msg) const
	{
		if (i >= data->size())	throw std::out_of_range(msg);
	}
};


class StrBlobPtr
{
public:
	StrBlobPtr() : curr(0) {}

	StrBlobPtr(StrBlob& a, std::size_t sz = 0) :
		wptr(a.data), curr(sz)   // StrBlobPtr为StrBlob友元类,可以直接访问私有数据
	{}

	//void push_back(const std::string& str) { check(0,"")->push_back(str); }
	//void pop_back() const { check(0, "pop_back on empty StrBlob")->pop_back(); }
	 const函数, 建议返回const引用
	//const std::string& front() const { return check(0, "front on empty StrBlob")->front(); }
	//const std::string& back() const { return check(0, "back on empty StrBlob")->back(); }

	// 解引用访问当前指向vector中的元素
	std::string& deref() const
	{
		auto p = check(curr, "dereference past end");
		return (*p)[curr];
	}

	// 前缀递增,返回递增后的对象的引用
	StrBlobPtr& incr(){
		check(curr, "increment past end of StrBlobPtr");
		++curr;
		return *this;
	}

	StrBlobPtr& operator++ () { // 调用方式为 ++StrBlobPtr
		return incr();
	}
	StrBlobPtr& operator++ (int) {  // 调用方式为 StrBlobPtr++
		return incr();
	}
	
	friend bool eq(const StrBlobPtr &lhs, const StrBlobPtr &rhs);

private:
	using SrtArr = std::vector<std::string>;

	// 检查成功 返回一个底层数据的 shared_ptr
	std::shared_ptr<SrtArr> 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;
	}

	std::weak_ptr<SrtArr> wptr;  // 保存一个weak_ptr, 意味着底层数据vector可能被销毁
	std::size_t curr; // 在数组中的当前位置
};


StrBlobPtr StrBlob::begin(){	
	return StrBlobPtr(*this);
}

StrBlobPtr StrBlob::end(){
	auto ret = StrBlobPtr(*this, data->size()); return ret;
}

// 该函数需要访问StrBlobPtr的私有变量,所以将其定义有友元函数
bool eq(const StrBlobPtr & lhs, const StrBlobPtr & rhs)
{
	auto l = lhs.wptr.lock(), r = rhs.wptr.lock();
	if (l == r)
		return (!r || lhs.curr == rhs.curr);
	else
		return false;
}

bool neq(StrBlobPtr &lhs, StrBlobPtr &rhs)
{
	return !eq(lhs, rhs);
}


测试的代码如下:

{
	try {
		 配合 StrBlobPtr注释的函数代码使用
		//{
		//	StrBlob sb;
		//	sb.push_back("abc");
		//	{
		//		StrBlobPtr sbp(sb);
		//		std::cout << sbp.front() << std::endl;
		//		sbp.push_back("123");
		//	}
		//	std::cout << sb.back() << std::endl;
		//}

		{
			StrBlob sb;
			sb.push_back("abc");
			sb.push_back("123");
			// 类似迭代器的使用
			for (auto it = sb.begin(); it != sb.end(); ++it)
				cout << it.deref() << endl;
		}
	}
	catch (int e) {
		std::cout << "catch throw " << e << std::endl;
	}
	catch (std::exception& e) {
		std::cout << e.what() << std::endl;
	}
}

5、其他

5.1 动态数组与智能指针

标准库提供了一个可以管理new分配的数组unique_ptr版本,必须在对象类型后跟一对方括号。

{
	std::unique_ptr<int[] > up(new int[4]{ 0,1,2,3 });
	auto pi = up.get();

	int arr1 = up[1];  //可以使用下标访问

	up.reset(); //释放内存,自动调用 delete[]
}

shared_ptr未定义下标运算,智能指针也不支持算术运算, 因此访问数组元素必须用get获取内置指针,然后再用它来访问数组

{ 
	std::shared_ptr<int> sp(new int[4]{ 0,1,2,3 }); 
	auto pi = sp.get();
	int arr1 = pi[1];//获取元素必须用内置指针进行数组元素访问

	sp.reset(); //c++11的shared_ptr不支持管理动态数组,还需要指定删除器;c++14正常;
}

5.2 Allocator类

简单使用

{
	std::allocator<std::string> strAlloc;  // 创建一个string的分配器
	std::string* p = strAlloc.allocate(10);  // 分配10个为初始化的string

	auto q = p;
	strAlloc.construct(q++);
	strAlloc.construct(q++, "123");
	strAlloc.construct(q++, 10, '1');   // 10个'1'组成的字符串

	// 执行对象"111111111"的析构,但是没有释放内存;; 只能对已经构造的元素进行操作;
	//strAlloc.destroy(p+2);  
	
	while (q != p) {
		strAlloc.destroy(--q);
	}

	strAlloc.deallocate(p, 10);  // 释放内存, n必须为ps创建时的大小
}

{ //拷贝和填充未初始化内存的算法
	std::vector<int> vi{ 0,1,2,3,4,5,6,7,8,9 };  // 10个元素的vector

	std::allocator<int> alloc;
	auto p = alloc.allocate(vi.size() * 2);  //分配一个大小为20的未初始化的int内存

	auto q = std::uninitialized_copy(vi.begin(), vi.end(), p);  // vi初始化p, 返回最后指向(第11个未初始化int内存)
	std::uninitialized_fill_n(q, vi.size(), 11);      //剩下10个未初始化int内存的用11填充   //c4996 warning

	alloc.deallocate(p, vi.size()*2 );
}

你可能感兴趣的:(c/c++)