为什么要使用智能指针?
先看一个例子:
#include <iostream>
#include <string>
#include <exception>
using namespace std;
void memory_leak_demo1() {
string* str = new string("今天又敲了一天代码,太累了,回家休息了!!!");
cout << *str << endl;
return; 因为太累忘记delete
}
int memory_leak_demo2() {
string* str = new string("这个世界到处是坑,所以异常处理要谨记在心!!!");
/***********************************************
* 程序执行一段复杂的逻辑,假设尝试从一个必须存在
* 的文件中读取某些数据,而文件此时不存在
************************************************/
{
throw exception("文件不存在"); 处理的逻辑太复杂在异常处理中忘记delete
}
cout << *str << endl;
delete str;
return 0;
}
int main()
{
memory_leak_demo1();
try {
memory_leak_demo2();
}
catch (exception e) {
cout<<"catch exception: "<<e.what()<<endl;
}
system("pause");
return 0;
}
以上两种情况都会出现内存泄漏!
更好的解决方案: 把string 定义为auto 变量,在函数生命周期结束时释放!
思考:如果我们分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存,这看似是一个 very nice 的方案?
智能指针:就是通过这个原理来解决指针自动释放的问题!
C++98 提供了 auto_ptr 模板的解决方案
C++11 增加unique_ptr、shared_ptr 和weak_ptr
auto_ptr 使用详解 (C++98)
auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用delete 来释放内存!
用法:
头文件: #include <memory>
用 法: auto_ptr<类型> 变量名(new 类型)
-------------------------------------------
例 如:
auto_ptr<string> str(new string("我要成为大牛~ 变得很牛逼!"));
auto_ptr<vector<int>> ap(new vector<int>(10));
在使用智能指针访问对象时,使用方式和普通指针一样
cout<< "-> test: "<<t->GetTest()<< endl;
cout << "* test: " << (*t).GetTest() << endl;
release 取消指针指针对动态内存的托管,之前分配的内存必须手动释放
test* tmp = t.release();
delete tmp;
reset 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉
t.reset();
t.reset(new Test());
int memory_leak_demo() {
Test* t = new Test();
auto_ptr< Test> t(new Test());
/***********************************************
* 程序执行一段复杂的逻辑,假设尝试从一个必须存在
* 的文件中读取某些数据,而文件此时不存在
************************************************/
{
throw exception("文件不存在");
}
return 0;
}//调用结束自动释放内存
使用建议:
1.尽可能不要将auto_ptr 变量定义为全局变量或指针
2.除非自己知道后果,不要把auto_ptr 智能指针赋值给同类型的另外一个智能指针
3.C++11 后auto_ptr 已经被“抛弃”,已使用unique_ptr替代!
unique_ptr 使用详解 (C++11)
auto_ptr是用于C++11之前的智能指针。由于 auto_ptr 基于排他所有权模式:两个指针不能指向同一个资源,复制或赋值都会改变资源的所有权。auto_ptr 主要有两大问题:
- 复制和赋值会改变资源的所有权,不符合人的直觉。
- 在 STL 容器中使用auto_ptr存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。
- 不支持对象数组的操作
所以,C++11用更严谨的unique_ptr 取代了auto_ptr!
unique_ptr特性
- 基于排他所有权模式:两个指针不能指向同一个资源
- 无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时 右值赋值构造和赋值
- 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
- 在容器中保存指针是安全的
构造函数
unique_ptr<T> up ; 空的unique_ptr,可以指向类型为T的对象
unique_ptr<T> up1(new T()) ; 定义unique_ptr,同时指向类型为T的对象
unique_ptr<T[]> up ; 空的unique_ptr,可以指向类型为T[的数组对象
unique_ptr<T[]> up1(new T[]) ; 定义unique_ptr, 同时指向类型为T的数组对象
unique_ptr<T,D> up(); 空的unique_ptr,接受一个D类型的删除器d,使用d释放内存
unique_ptr<T,D> up(new T()); 定义unique_ptr, 同时指向类型为T的对象,接受一个D类型的删除器d,使用删除器d来释放内存
赋值
unique_ptr<int> up1(new int(10));
unique_ptr<int> up2(new int(11));
up1 = std::move(up2); 必须使用移动语义std::move(),结果,up1 内存释放, up2 交由up1 管理
主动释放对象
up = nullptr ; 释放up指向的对象,将up置为空
或 up = NULL; 作用相同
放弃对象控制权
up.release(); 放弃对象的控制权,返回指针,将up置为空,不会释放内存
重置
up.reset(…) ; 参数可以为 空、内置指针,先将up所指对象释放,然后重置up的值
交换
up.swap(up1); 将智能指针up 和up1管控的对象进行交换
shared_ptr 使用详解 (C++11)
熟悉了unique_ptr 后,其实我们发现unique_ptr 这种排他型的内存管理并不能适应所有情况,有很大的局限!如果需要多个指针变量共享怎么办?
如果有一种方式,可以记录引用特定内存对象的智能指针数量,当复制或拷贝时,引用计数加1,当智能指针析构时,引用计数减1,如果计数为零,代表已经没有指针指向这块内存,那么我们就释放它!这就是 shared_ptr 采用的策略!
构造函数
shared_ptr<T> sp ; 空的shared_ptr,可以指向类型为T的对象
shared_ptr<T> sp1(new T()) ; 定义shared_ptr,同时指向类型为T的对象
shared_ptr<T[]> sp2 ; 空的shared_ptr,可以指向类型为T[的数组对象 C++17后支持
shared_ptr<T[]> sp3(new T[]{...}) ; 指向类型为T的数组对象 C++17后支持
shared_ptr<T> sp4(NULL, D()); 空的shared_ptr,接受一个D类型的删除器,使用D
释放内存
shared_ptr<T> sp5(new T(), D()); 定义shared_ptr,指向类型为T的对象,接受一个D 类型的删除器,使用D删除器来释放内存
初始化
方式一 构造函数
shared_ptrr<int> up1(new int(10)); int(10) 的引用计数为1
shared_ptrr<int> up2(up1); 使用智能指针up1构造up2, 此时int(10) 引用计数为2
方式二 使用make_shared 初始化对象,分配内存效率更高
make_shared 函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr; 用法:
make_shared<类型>(构造类型对象需要的参数列表);
shared_ptr<int> p4 = make_shared<int>(2); 多个参数以逗号','隔开,最多接受十个
shared_ptr<string> p4 = make_shared<string>("字符串");
赋值
shared_ptrr<int> up1(new int(10)); int(10) 的引用计数为1
shared_ptr<int> up2(new int(11)); int(11) 的引用计数为1
up1 = up2; int(10) 的引用计数减1,计数归零内存释放,up2共享int(11)给up1, int(11)的引用计数为2
主动释放对象
shared_ptrr<int> up1(new int(10));
up1 = nullptr ; int(10) 的引用计数减1,计数归零内存释放
或
up1 = NULL; 作用同上
重置
up.reset() ; 将p重置为空指针,所管理对象引用计数 减1
up.reset(p1); 将p重置为p1(的值),p 管控的对象计数减1,p接管对p1指针的管控
up.reset(p1,d); 将p重置为p(的值),p 管控的对象计数减1并使用d作为删除器
交换
std::swap(p1,p2); 交换p1 和p2 管理的对象,原对象的引用计数不变
p1.swap(p2); 同上
使用陷阱
shared_ptr作为被管控的对象的成员时,小心因循环引用造成无法释放资源!
class boy {
public:
boy() {
cout << "boy construct!" << endl;
}
~boy() {
cout << "boy destruct!" << endl;
}
void set_girl_friend(shared_ptr<girl> &_girl) {
girl_friend = _girl;
}
private:
shared_ptr<girl> girl_friend;
};
class girl {
public:
girl() {
cout << "girl construct!" << endl;
}
~girl() {
cout << "girl destruct!" << endl;
}
void set_boy_friend(shared_ptr<boy> &_boy) {
boy_friend = _boy;
}
private:
shared_ptr<boy> boy_friend;
};
创建两个shared_ptr对象的智能指针
shared_ptr<girl> sp_girl(new girl());
shared_ptr<boy> sp_boy(new boy());
把两个shared_ptr对象的智能指针通过函数调用互相指向对方
sp_girl->set_boy_friend(sp_boy);
sp_boy->set_girl_friend(sp_girl);
这样照成的后果就是函数调用结束智能指针不能正确的释放!
weak_ptr 使用详解 (自从C++11)
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少. 同时weak_ptr 没有重载*和->但可以使用 lock 获得一个可用的 shared_ptr 对象。
智能指针的使用陷阱
int *x = new int(10);
unique_ptr<int> up1(x);
unique_ptr<int> up2(x);
//警告! 以上代码使up1 up2指向同一个内存,非常危险
或以下形式:
up1.reset(x);
up2.reset(x);
在调用u.release()时是不会释放u所指的内存的,这时返回值就是对这块内存的唯一索引,如果没有使用这个返回值释放内存或是保存起来,这块内存就泄漏了
3. 禁止delete 智能指针get 函数返回的指针
如果我们主动释放掉get 函数获得的指针,那么智能 指针内部的指针就变成野指针了,析构时造成重复释放,带来严重后果!
4. 禁止用任何类型智能指针get 函数返回的指针去初始化另外一个智能指针!
shared_ptr<int> sp1(new int(10));
//一个典型的错误用法 shared_ptr<int> sp4(sp1.get());