1. 智能指针的作用
C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。
使用普通指针,容易造成:
- 堆内存泄露(忘记释放)
- 二次释放
- 程序发生异常时内存泄露
2. 理解智能指针
- 从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
- 智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
- 智能指针还有一个作用是把值语义转换成引用语义。C++和Java有一处最大的区别在于语义不同。
在Java里面下列代码:
Animal a = new Animal();
Animal b = a;
你当然知道,这里其实只生成了一个对象,a
和b
仅仅是把持对象的引用而已。但在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
- shared_ptr多个指针指向相同的对象,使用引用计数,每一次shared_ptr的拷贝指向相同的内存,每使用一次,内部引用计数加1,每析构一次,内部引用计数减1,直至减为0,自动删除指向的堆内存,shared_ptr内部的引用计数是线程安全的,但对象的读取需要加锁
- 智能指针是一个模板类,可以指定其类型,传入的指针使用构造函数初始化,也可使用make_shared函数初始化,不能把一个指针赋给智能指针,因为一个是类,一个是指针,如shared_ptr p=new int(1);错误
- 拷贝使得对象的引用计数加1,赋值使得原对象的引用计数减1,当计数为0时,自动删除指向的内存,后来指向的对象的引用计数加1,指向后来的对象。
- 缺陷为:循环引用,会导致内存泄漏问题,解决办法为使用若引用的智能指针,即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