C++智能指针

1. 智能指针的作用

C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。

使用普通指针,容易造成:

  1. 堆内存泄露(忘记释放)
  2. 二次释放
  3. 程序发生异常时内存泄露

2. 理解智能指针

  1. 从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
  2. 智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
  3. 智能指针还有一个作用是把值语义转换成引用语义。C++和Java有一处最大的区别在于语义不同。
    在Java里面下列代码:
Animal a = new Animal();
Animal b = a;

你当然知道,这里其实只生成了一个对象,ab仅仅是把持对象的引用而已。但在C++中不是这样,这里却是就是生成了两个对象。

Animal a;
Animal b = a;

关于值语义,陈硕有一篇文章:值语义

3. 准备

class Monster {
public:
	Monster() {
		this->a = 0;
		std::cout << "create" << std::endl;
	}
	Monster(int a) {
		this->a = a;
		std::cout << "create" << std::endl;
	}
	~Monster() {
		std::cout << "delete" << std::endl;
	}
	void Run() {
		std::cout << "run a:" << a << std::endl;
	}
	int a;
};

写一个类(怪物),之后所有的示例都用该类的对象

4. auto_ptr(已废弃,不能使用)

auto_ptr是为了推动RAII而加入到c++标准的第一个智能指针,它实现了最基本的资源管理,是C++11以前最原始的智能指针,但是在c++11中已经被弃用了。所以不要使用!
不过与其他类型不同的是,auto_ptr是没有复制语义的,它在复制时实现的其实是移动语义,复制和赋值auto_ptr时会默认改变所管理资源的所有权,基于这个原因,它也不能在stl容器中使用,因为容器内的元素必需支持可复制和可赋值。另外还有一个限制它使用范围的因素是它的析构函数默认调用delete,而且无法重载,所以不能支持数组等对象。

void runGame() {
	std::auto_ptr<Monster> monster1(new Monster(12));//monster1 指向 一个怪物
	monster1->Run(); //run a:12
	//std::auto_ptr<Monster> monster2(monster1); // 转移指针
	std::auto_ptr<Monster> monster2 = monster1;  // 转移指针,效果同上一行
	std::cout << "&monster1 : " << monster1.get() << std::endl; // &monster1 : 0x0
	std::cout << "&monster2 : " << monster2.get() << std::endl; // &monster2 : 0x7f921a4006a0
	monster2->Run(); //run a:12
	monster1->Run(); //error
}

复制auto_ptr对象时,把指针指传给复制出来的对象,原有对象的指针成员随后重置为nullptr。这说明auto_ptr是独占性的,不允许多个auto_ptr指向同一个资源。

auto_ptr不能指向一组对象,就是说它不能和操作符new[]一起使用。

void runGame() {
	std::auto_ptr<Monster> monster1(new Monster[5]);//monster1 指向一个怪物
}

输出:
create
create
create
create
create
delete
Untitled(3239,0x1051c85c0) malloc: *** error for object 0x7ff82fd020e8: pointer being freed was not allocated
Untitled(3239,0x1051c85c0) malloc: *** set a breakpoint in malloc_error_break to debug

上面的代码将产生一个运行时错误。因为当auto_ptr离开作用域时,delete被默认用来释放关联的内存空间。当auto_ptr只指向一个对象时,这当然是没问题的,但是在上面的代码里,我们在堆里创建了一组对象,应该使用delete[]来释放,而不是delete.

5. unique_ptr

由于auto_ptr存在各种问题,c++11中unique_ptr华丽登场替换了auto_ptr,它们所做的事情基本一样,不过unique_ptr不支持复制语义,只支持移动语义,所以无法执行普通的复制或赋值操作(返回将要被销毁的unique_ptr时例外),但是可以进行移动构造赋值操作,说白了就是复制的话需要显示调用std::move(std::unique_ptr对象)。另外unique_ptr可以重载删除器,使其支持数组等类型。

void runGame() {
	std::unique_ptr<Monster> monster1(new Monster(12));//monster1 指向 一个怪物
	monster1->Run(); //run a:12
	//std::unique_ptr<Monster> monster2(monster1); // 编译器error
	//std::unique_ptr<Monster> monster3 = monster1;  // 编译器error
	std::unique_ptr<Monster> monster4(std::move(monster1)); //转移所有权给monster3
	std::cout << "&monster1 : " << monster1.get() << std::endl; // &monster1 : 0x0
	std::cout << "&monster4 : " << monster4.get() << std::endl; // &monster2 : 0x7f921a4006a0
	monster4->Run(); //run a:12
	monster1->Run(); //error
}
make_shared
std::shared_ptr<Monster> monster1 = std::make_shared<Monster>(12);

6. shared_ptr

  1. shared_ptr多个指针指向相同的对象,使用引用计数,每一次shared_ptr的拷贝指向相同的内存,每使用一次,内部引用计数加1,每析构一次,内部引用计数减1,直至减为0,自动删除指向的堆内存,shared_ptr内部的引用计数是线程安全的,但对象的读取需要加锁
  2. 智能指针是一个模板类,可以指定其类型,传入的指针使用构造函数初始化,也可使用make_shared函数初始化,不能把一个指针赋给智能指针,因为一个是类,一个是指针,如shared_ptr p=new int(1);错误
  3. 拷贝使得对象的引用计数加1,赋值使得原对象的引用计数减1,当计数为0时,自动删除指向的内存,后来指向的对象的引用计数加1,指向后来的对象。
  4. 缺陷为:循环引用,会导致内存泄漏问题,解决办法为使用若引用的智能指针,即weak_ptr
void runGame() {
	std::shared_ptr<Monster> monster1(new Monster(12));//monster1 指向 一个怪物
	std::cout << "1 - use_count : " << monster1.use_count() << std::endl;  // use_count : 1
	std::shared_ptr<Monster> monster2(monster1); // 编译器error
	std::cout << "2 - use_count : " << monster1.use_count() << std::endl;  // use_count : 1
	std::shared_ptr<Monster> monster3 = monster1;  // 编译器error
	std::cout << "3 - use_count : " << monster1.use_count() << std::endl;  // use_count : 1
	
	monster1 = monster3;
	std::cout << "4 - use_count : " << monster1.use_count() << std::endl;  // use_count : 1
}

输出

create
1 - use_count : 1
2 - use_count : 2
3 - use_count : 3
4 - use_count : 3
delete
析构
void runGame() {
	std::shared_ptr<Monster> monster1(new Monster[5]);
}

输出:
create
create
create
create
create
delete
Untitled(3423,0x11be9a5c0) malloc: *** error for object 0x7f8570c004a8: pointer being freed was not allocated
Untitled(3423,0x11be9a5c0) malloc: *** set a breakpoint in malloc_error_break to debug

在此场景下,shared_ptr指向一组对象,但是当离开作用域时,默认的析构函数调用delete释放资源。实际上,我们应该调用delete[]来销毁这个数组。用户可以通过调用一个函数,例如一个lamda表达式,来指定一个通用的释放步骤。

void runGame() {
	std::shared_ptr<Monster> monster1(new Monster[5], [](Monster*p) { delete[] p; } );
}
输出:
create
create
create
create
create
delete
delete
delete
delete
delete
接口

就像一个普通指针一样,shared_ptr也提供解引用操作符*,->。除此之外,它还提供了一些更重要的接口:

get(): 获取shared_ptr绑定的资源.
reset(): 释放关联内存块的所有权,如果是最后一个指向该资源的shared_ptr,就释放这块内存。
unique: 判断是否是唯一指向当前内存的shared_ptr.
operator bool : 判断当前的shared_ptr是否指向一个内存块,可以用if 表达式判断。
缺陷:一个裸指针(naked pointer)创建多个shared_ptr.
void runGame() {
	Monster* p = new Monster;
	std::shared_ptr<Monster> monster1(p);
	std::shared_ptr<Monster> monster2(p);
}

避免这个问题,尽量不要从一个裸指针(naked pointer)创建shared_ptr。

缺陷:模型循环依赖(互相引用或环引用)时,计数会不正常
class Monster {
public:
	Monster() {
		std::cout << "create" << std::endl;
	}
	void setFather(std::shared_ptr<Monster>& father) {
		father_	= father;
	}
	void setSon(std::shared_ptr<Monster>& son) {
		son_ = son;
	}
	~Monster() { 
		std::cout << "delete" << std::endl; 
	}
private:
	std::shared_ptr<Monster> father_;
	std::shared_ptr<Monster> son_;
};

void runGame() {
	std::shared_ptr<Monster> father(new Monster());
	std::shared_ptr<Monster> son(new Monster());
	std::cout << "father_use_count: " << father.use_count() << std::endl;
	std::cout << "son_use_count: " << son.use_count() << std::endl;
	father->setSon(son);
	son->setFather(father);
	std::cout << "-------------" << std::endl;
	std::cout << "father_use_count: " << father.use_count() << std::endl;
	std::cout << "son_use_count: " << son.use_count() << std::endl;
}

输出:

create
create
father_use_count: 1
son_use_count: 1
-------------
father_use_count: 2
son_use_count: 2

很明显输出有问题,没有调用对象的析构函数。什么原因呢?
father和son指向的堆对象,互相set之后shared计数都是为2。
son智能指针出作用域,退出栈:
son指向的堆对象计数use_count = 2 - 1 = 1,father指向的堆对象,计数仍为2。

father智能指针出作用域,退出栈:
father指向的堆对象计数use_count = 2 - 1 = 1,son指向的堆对象 计数仍为1。
函数结束:所有计数都没有变0,也就是说中途没有释放任何堆对象。
为了解决这一缺陷的存在,解决方法是将father_和son_中任意一个声明为weak_ptr类型。

7. weak_ptr(一种弱引用指针)

weak_ptr是配合shared_ptr出现的,不具有普通指针的行为,没有重载operator*和->,最大作用在于协助shared_ptr观测资源的使用情况,可以从一个shared_ptr或一个weak_ptr对象构造,使用其成员函数use_count()获得观测资源的引用计数,expired()功能等价于use_count()== 0标识被观测的资源已经不存在。
weak_ptr可以使用lock()获得一个新的shared_ptr对象,从而操作资源,但expired()==true时,lock函数返回一个存储空指针的shared_ptr对象

weak_ptr可以由一个shared_ptr或者另一个weak_ptr构造。
weak_ptr的构造和析构不会引起shared_count的增加或减少,只会引起weak_count的增加或减少。

被管理资源的释放只取决于shared计数,当shared计数为0,才会释放被管理资源,也就是说weak_ptr不控制资源的生命周期。

但是计数区域的释放却取决于shared计数和weak计数,当两者均为0时,才会释放计数区域。

class Monster {
public:
	Monster() {
		std::cout << "create" << std::endl;
	}
	void setFather(std::shared_ptr<Monster>& father) {
		father_	= father;
	}
	void setSon(std::shared_ptr<Monster>& son) {
		son_ = son;
	}
	~Monster() { 
		std::cout << "delete" << std::endl; 
	}
private:
	std::weak_ptr<Monster> father_; //尽管父子可以互相访问,但是彼此都是独立的个体,无论是谁都不应该拥有另一个人的所有权。
	std::weak_ptr<Monster> son_;
};

void runGame() {
	std::shared_ptr<Monster> father(new Monster());
	std::shared_ptr<Monster> son(new Monster());
	std::cout << "father_use_count: " << father.use_count() << std::endl;
	std::cout << "son_use_count: " << son.use_count() << std::endl;
	father->setSon(son);
	son->setFather(father);
	std::cout << "-------------" << std::endl;
	std::cout << "father_use_count: " << father.use_count() << std::endl;
	std::cout << "son_use_count: " << son.use_count() << std::endl;
}

输出:

create
create
father_use_count: 1
son_use_count: 1
-------------
father_use_count: 1
son_use_count: 1
delete
delete

此时输出是正确的,什么原因呢?
一开始:
father指向的堆对象 shared计数为1,weak计数为1
son指向的堆对象 shared计数为1,weak计数为1

son智能指针退出栈:
son指向的堆对象 shared计数减为0,weak计数为1,释放son的堆对象,发出第一个死亡的悲鸣
father指向的堆对象 shared计数为1,weak计数减为0;

son智能指针出作用域,退出栈:
son指向的堆对象shared计数=1-1=0,weak计数为1,释放son;
father指向的堆对象shared计数为1,weak计数为=1-1=0。

father智能指针出作用域,退出栈:
father指向的堆对象shared计数=1-1=0,weak计数为0,释放father;
son指向的堆对象shared计数为0,weak计数为=1-1=0,释放son的计数区域。

函数结束,释放行为正确。

当生命控制权没有彼此互相掌握时,才能正确解决循环引用问题,而弱引用的使用可以使生命控制权互相掌握的情况消失。

weak_ptr没有重载 * 和 -> ,所以并不能直接使用资源。但可以使用lock()获得一个可用的shared_ptr对象,
如果对象已经释放了,lock()会失败,返回一个空的shared_ptr。

void runGame() {
	std::shared_ptr<Monster> monster1(new Monster(12));//monster1 指向 一个怪物
	std::weak_ptr<Monster> r_monster1 = monster1;
	std::cout << "shared:use_count : " << monster1.use_count() << "  weak:use_count :" << r_monster1.use_count() <<  std::endl;  // use_count : 1
	//r_monster1->Run(); // Error! 编译器出错!weak_ptr没有重载* 和 -> ,无法直接当指针用
	std::shared_ptr<Monster> monster2 = r_monster1.lock(); //OK!可以通过weak_ptr的lock方法获得shared_ptr。
	std::cout << "shared:use_count : " << monster1.use_count() << "  weak:use_count :" << r_monster1.use_count() <<  std::endl;  // use_count : 1
}

输出:

create
shared:use_count : 1  weak:use_count :1
shared:use_count : 2  weak:use_count :2
delete
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值