-
普通指针的不足:
① new 和 new[] 的内存需要 delete 和 delete[] 手动释放;
② 程序员的主观失误,可能导致漏了释放内存;
③ 在多线程编程中,程序员也可能不确定什么时候释放内存。 -
智能指针的设计思路:
① 智能指针是类模板,在栈上创建智能指针对象;
② 把普通指针交给智能指针对象;
③ 智能指针对象过期时,调用析构函数释放普通指针的内存。 -
智能指针在头文件
<memory>
中,有3种:
① unique_ptr
② shared_ptr
③ weak_ptr
独占智能指针unique_ptr
以类自定义的类型 AA 为例:
class AA {
public:
string m_name;
AA() {};
AA(const string& name) : m_name(name) {};
~AA() { cout << "析构了" << endl; }
}
初始化
-
方法一:用已存在的地址初始化(不推荐)
AA* p = new AA("张飞"); unique_ptr<AA> p1(p);
-
方法二: 分配内存并初始化
unique_ptr<AA> p(new AA("张飞"));
-
方法三:C++14标准
unique_ptr<AA> p = make_unique<AA>("你好");
使用方法
- 智能指针重载了
*
和->
操作符,可以像使用普通指针一样使用。
AA* p = new AA("张飞");
unique_ptr<AA> p1 = make_unique<AA>("关羽");
cout << p->m_name << endl;
cout << p1->m_name << endl;
cout << (*p).m_name << endl;
cout << (*p1).m_name << endl;
- 不能将普通指针直接传递赋值给智能指针
AA *p = new AA("张飞");
unique_ptr<AA> p1 = p; // 错误
unique_ptr<AA> p2 = new AA("张飞"); // 错误
// 原因:unique_ptr类中声明了不能隐式转换
explicit unique_ptr(T* p) noexcept;
- 不能使用拷贝构造函数
unique_ptr<AA> p1 = make_unique<AA>("张飞");
unique_ptr<AA> p2 = p1; // 错误,不能使用拷贝构造
// 原因:unique_ptr类中禁用了拷贝构造函数
unique_ptr(const unique_ptr&) = delete;
一个 unique_ptr 对象只对一个资源负责,如果 unique_ptr 允许复制,就会出现多个 unique_ptr 对象指向同一块内存的情况。当其中一个 unique_ptr 对象过期释放内存,其他的 unique_ptr 对象过期又会释放内存,造成对同一块内存释放多次。
所以不能使用拷贝构造函数和赋值构造函数!
- 不能使用赋值构造函数
unique_ptr<AA> p1 = make_unique<AA>("张飞");
unique_ptr<AA> p2;
p2 = p1; // 错误,不能使用赋值构造
// 原因:unique_ptr类中禁用了赋值构造函数
unique_ptr& operator=(const unique_ptr&) = delete;
-
不要用同一个裸指针初始化多个 unique_ptr 对象
因为会出现多次释放同一块内存,导致出错。
因此,不推荐使用上述的第一种初始化方法,以防出现这种错误。 -
get() 可以返回裸指针
AA* p = new AA("张飞");
unique_ptr<AA> p1(p);
cout << "裸指针的值:" << p << endl;
cout << "智能指针的值:" << p1 << endl;
cout << "p1.get(): " << p1.get() << endl;
cout << "p1 的地址是: " << &p1 << endl;
/*
输出:
裸指针的值:00FD9B80
智能指针的值:00FD9B80
p1.get(): 00FD9B80
p1 的地址是: 00BEF968
*/
-
不要用 unique_ptr 管理不是 new 分配的内存
-
智能指针用于函数参数时要传引用(不能传值,因为没有拷贝构造函数)
-
智能指针不支持指针的运算(+、-、++、–)
重要技巧
- 将一个 unique_ptr 赋给另一个时,如果源 unique_ptr 是一个临时右值,编译器允许这样做;如果源 unique_ptr 将存在一段时间,编译器禁止这样做。一般用于函数的返回值。
unique_ptr<AA> p;
p = unique_ptr<AA>(new AA("张飞"));
- 用 nullptr 给 unique_ptr 赋值将释放对象,空的 unique_ptr==nullptr。
unique_ptr<AA> p(new AA("张飞"));
if (p != nullptr) cout << "p 不为空 " << endl;
p = nullptr;
if (p == nullptr) cout << "p 为空" << endl;
/*
输出:
张飞
p 不为空
析构
p 为空
*/
-
release() 释放对原始指针的控制权,将 unique_ptr 置为空,返回裸指针。(可用于把 unique_ptr 传递给子函数,子函数将负责释放对象)
-
std:move() 可以转移对原始指针的控制权。(可用于把 unique_ptr 传递给子函数,子函数形参也是 unique_ptr)
-
reset() 释放对象
p.reset(); // 释放p对象指向的资源对象
p.reset(nullptr); // 释放p对象指向的资源对象
p.reset(new AA("张飞")); // 释放p对象指向的资源对象,同时指向新的对象
- swap() 交换两个 unique_ptr 的控制权
void swap(unique_ptr<T>& _Right);
-
unique_ptr 也可象普通指针那样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样。
-
unique_ptr 不是绝对安全,如果程序中调用 exit() 退出,全局的 unique_ptr 可以自动释放,但局部的 unique_ptr 无法释放。
-
unique_ptr 提供了支持数组的特化版本。数组版本的 unique_ptr,重载了操作符[],操作符[]返回的是引用,可以作为左值使用。
AA* parr1 = new AA[2]{ string("张飞"), string("关羽") };
AA* parr2 = new AA[2]; // 普通指针数组
parr2[0].m_name = "张飞";
parr2[1].m_name = "关羽";
unique_ptr<AA[]> parr3(new AA[2]{ string("张飞"), string("关羽") });
unique_ptr<AA[]> parr4(new AA[2]); // unique_ptr数组
parr4[0].m_name = "张飞";
parr4[1].m_name = "关羽";
共享智能指针shared_ptr
shared_ptr 共享它指向的对象,多个 shared_ptr 可以指向相同的对象,在内部使用技术机制实现。
当新的 shared_ptr 与对象关联时,引用计数增加 1。
当 shared_ptr 超出作用域时,引用计数减1。当引用计数变为 0 时,则表示没有任何 shared_ptr 与对象关联,则释放该对象。
shared_ptr 的构造函数也是 explicit,但是,没有删除拷贝构造函数和赋值函数。
初始化
-
方法一:分配内存并初始化
shared_ptr<AA> p(new AA("张飞"));
-
方法二:(推荐使用)
shared_ptr<AA> p = make_shared<AA>("张飞");
-
方法三:用已存在的地址初始化
AA* p = new AA("张飞"); shared_ptr<AA> p1(p);
-
方法四:用已存在的shared_ptr初始化(拷贝构造、赋值构造)
shared_ptr<AA> p1(new AA("张飞")); shared_ptr<AA> p2(p1); // 计数加1 shared_ptr<AA> p3 = p1; // 计数加1
使用方法
- 智能指针重载了
*
和->
操作符,可以像使用指针一样使用 shared_ptr。 - use_count() 方法返回引用计数器的值。
- unique() 方法,如果 use_count() 为1,返回 true,否则返回 false。
- 支持普通的拷贝和赋值,左值的 shared_ptr 的计数器将减 1,右值 shared_ptr 的计算器将加 1。
shared_ptr<AA> p1(new AA("张飞"));
shared_ptr<AA> p2 = p1; // 计数加1
shared_ptr<AA> p3(p1); // 计数加1
cout << "p1.use_count() = " << p1.use_count() << endl; // 值为3
shared_ptr<AA> p4(new AA("关羽"));
shared_ptr<AA> p5 = p4;
cout << "p5.use_count() = " << p5.use_count() << endl; // 值为2
p5 = p1; // p5指向的对象变了,所以p4的引用计数-1,p1的引用计数+1
cout << "p1.use_count() = " << p1.use_count() << endl; // 值为4
cout << "p5.use_count() = " << p4.use_count() << endl; // 值为1
- get() 方法返回裸指针。
- 不要用同一个裸指针初始化多个 shared_ptr。
- 不要用 shared_ptr 管理不是 new 分配的内存。
- 用作函数的参数,原理与 unique_ptr 相同。
- 智能指针不支持指针的运算(+、-、++、–)
重要技巧
- 用 nullptr 给 shared_ptr 赋值将把计数减 1,如果计数为 0,将释放对象,空的 shared_ptr==nullptr。
- std::move() 可以转移对原始指针的控制权。还可以将 unique_ptr 转移成 shared_ptr,反过来不行。
- reset() 改变与资源的关联关系。
p.reset(); // 解除与资源的关系,资源的引用计数减1
p.reset(new AA("张飞")); // 解除与资源的关系,资源的引用计数减1,关联新资源
- shared_ptr 的线程安全性:
- shared_ptr 的引用计数本身时线程安全的(引用计数是原子操作)
- 多个线程对同一个 shared_ptr 对象进行读写,则需要加锁
- 多个线程对同一个 shared_ptr 对象读,则是线程安全的
- 多线程读写 shared_ptr 所指向的同一个对象,不管是相同的 shared_ptr 对象,还是不同的 shared_ptr 对象,也需要加锁保护。
- 如果 unique_ptr 能解决问题,就不要用 shared_ptr。unique_ptr 的效率更高,占用的资源更少。
智能指针删除器
在默认情况下,智能指针过期的时候,用 delete 原始指针;释放它管理的资源。
程序员可以自定义删除器,改变智能指针释放资源的行为。
删除器可以是全局函数、仿函数和 Lambda 表达式,形参为原始指针。
- 案例:
// 对 shared_ptr shared_ptr<AA> p1(new AA("张飞1")); // 使用默认的删除器,即delete shared_ptr<AA> p2(new AA("张飞2"), deleteFunc); shared_ptr<AA> p3(new AA("张飞3"), deleteClass()); // 仿函数作删除器 auto myLambda = [](AA* a) { // Lambda表达式作删除器 cout << "自定义删除器(Lambda表达式)" << endl; delete a; }; shared_ptr<AA> p4(new AA("张飞4"), myLambda); // 对 unique_ptr 要复杂点 unique_ptr<AA, decltype(deleteFunc)*> p5(new AA("张飞5"), deleteFunc); unique_ptr<AA, deleteClass> p6(new AA("张飞6"), deleteClass()); unique_ptr<AA, decltype(myLambda)> p7(new AA("张飞7"), myLambda);
弱智能指针weak_ptr
weak_ptr 是为了配合 shared_ptr 而引入的,它指向一个由 shared_ptr 管理的资源但不影响资源的生命周期。也就是说,将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。
不论是否有 weak_ptr 指向,如果最后一个指向资源的 shared_ptr 被销毁,资源就会被释放。
-
循环引用
shared_ptr 内部维护了一个共享的引用计数器,多个 shared_ptr 可以指向同一个资源。但如果出现了循环引用的情况,引用计数永远无法归 0,资源不会被释放。(如图所示)
-
循环引用解决办法
将类内部的 shared_ptr 换成 weak_ptr 即可。
使用方法
weak_ptr 没有重载->
和*
操作符,不能直接访问资源。
有以下成员函数:
operator(); // 把shared_ptr或weak_ptr()赋值给weak_ptr
expired(); // 判断它指向的资源是否已经被释放
lock(); // 返回shared_ptr,如果资源已经被释放,返回空的shared_ptr
reset(); // 将当前weak_ptr指针设置为空
swap(); // 交换
重要技巧
- weak_ptr 不控制对象的生命周期,但它知道对象是否活着。
- 用 lock() 函数可以把 weak_ptr 提升为 shared_ptr,如果对象还或者,返回有效的 shared_ptr,如果对象已经死了,提升会失败,返回一个空的 shared_ptr。
- 提升的行为(lock())是线程安全的。
- 案例:
多线程下的某个场景为:先判断 weak_ptr 指向的资源是否被释放,如果没有被释放,则将该 weak_ptr 提升为 shared_ptr,代码如下。
shared_ptr<AA> pa = make_shared<AA>("张飞");
shared_ptr<BB> pb = make_shared<BB>("关羽");
// 解决过后的循环引用,m_p 是 weak_ptr
pa->m_p = pb;
pb->m_p = pa;
if(pa->m_p.expired() == true) { // 判断资源是否过期
cout << "pa->m_p已经过期\n";
} else {
pa->m_p.lock(); // 如果没有过期将pa->mp提升为 shared_ptr
}
上述代码的问题是,资源在 expired() 和 lock() 这两步操作之间可能被释放了,因此在 expired() 判断为 true 的情况下可能会去执行 lock() 操作。正确的写法如下。
shared_ptr<AA> pa = make_shared<AA>("张飞");
shared_ptr<BB> pb = make_shared<BB>("关羽");
// 解决过后的循环引用,m_p 是 weak_ptr
pa->m_p = pb;
pb->m_p = pa;
// 把weak_ptr提升为shared_ptr
shared_ptr<BB> pp = pa->m_p.lock();
if(pp == nullptr) { // 提升失败返回空指针
cout << "pa->m_p已经过期\n";
} else { // 若资源没被释放则可对资源进行操作了
cout << pp->m_name << endl;
}
评论区