C++11 智能指针深度解析:核心设计思想与面试要点
文章目录
引言
c++11标准库引入了智能指针,分为shared_ptr, unique_ptr, weak_ptr。智能指针是现代c++编程的核心,本文将详细介绍其核心设计思想、和面试的一些考点。
一、智能指针的核心思想:RAII
首先回顾一下内存泄漏的问题:
如果我们用指针去管理从堆空间开辟的内存,那么当在这个指针变量作用域的末尾需要动delete,否则就会导致这段堆空间内存泄漏了。当代码很复杂时,开发人员就会很容易忘记手动delete。这正是是c++的一个痛点,而RAII设计思想就能有效降低内存泄漏的风险
RAII是Resource Acquisition Is Initialization的缩写,它意味着将资源委托给一个对象,将资源的释放写在该对象的析构函数中。由于析构函数会自动被调用,因此,RAII就实现了资源的自动释放,避免了内存泄漏。
为节省篇幅,这里只贴出关键的析构函数,后文会详细介绍完整代码的编写
#include <iostream>
template<class T>
class SmartPtr {
public:
SmartPtr(T* data) : _data(data) {}
~SmartPtr() {
delete _data;
}
private:
T* _data;
};
二、认识三种智能指针
1. unique_ptr :
unique_ptr翻译过来就是“唯一指针”,它禁止了拷贝构造,只支持移动构造。不需要拷贝的场景首选它。
2.shared_ptr:
翻译过来就是"共享指针", 支持拷贝和移动,需要拷贝的场景首选它。其底层用到了引用计数。
引用计数介绍:

逻辑描述:
当发生shared_ptr的对象(如sp1、sp2)在创建或者拷贝的时候引用计数都会增加;一个对象的生命周期结束,引用计数随之减一;当引用计数为0时,说明没有其他的shared_ptr占用这份资源了,于是析构资源。
实现注意要点:
- 引用计数不能通过静态成员变量来实现:因为静态成员变量是一个类初始化的所有对象所共享的,而shared_ptr 实际是对每一份资源维护一个引用计数
参考下面的例子:
auto ptr1 = std::make_shared<int>(42); // 资源42的引用计数=1
auto ptr2 = std::make_shared<int>(100); // 资源100的引用计数=1
auto ptr3 = ptr1; // 资源42的引用计数=2
auto ptr4 = ptr2; // 资源100的引用计数=2
如果采用静态成员,那么ptr1, ptr2, ptr3, ptr4的引用计数就是1, 2, 3, 4 ?这显然是不对的。
2. 正确实现方式:堆上开辟空间:定义一个指针类型成员变量,构造函数的时候,为该指针成员开辟一段四字节整数空间,将空间值置为1。
引用计数实现细节:
- 拷贝构造时,复制原对象的引用计数指针成员变量即可,这样两个shared_ptr对象就在管理同一份资源了和同一份引用计数了。
- 赋值操作重载:以执行 ptr1=ptr2 语句为例,需要先让ptr1的引用计数减一,然后才是正常的ptr1去复制ptr2的资源指针和引用计数指针。同时,需要加判断语句,当出现ptr1=ptr1这样的自赋值时,不进行任何操作,避免性能损失。
- 析构函数时:引用计数减一,当减到0时,释放资源
- 需要确保引用计数自增自减操作的线程安全:采用加锁或者std::atomic(推荐)
赋值操作时,为什么ptr1的引用计数要减一?
ptr1=ptr2 的本质含义时ptr1放弃原来的资源,再复制ptr2的资源。如果不先让ptr1的引用计数减一,那么就会导致ptr1的引用计数永远减不到0,无法释放资源造成内存泄漏。
关于线程安全:
标准库shared_ptr 保证引用计数的线程安全。但多线程同时访问这个shared_ptr对象或者其管理的资源时,需要额外加锁保证线程安全。
make_shared接口:
c++创建shared_ptr有两种方式:一是传递普通指针,二是调用make_shared接口
class A {
public:
A() {}
};
std::shared_ptr<A> sp1 = std::shared_ptr<A>(new A());
std::shared_ptr<A> sp2 = std::make_shared<A>();
现代c++推荐使用:make_shared
make_shared优势:
1)简化代码:使用make_shared 可以简化创建shared_ptr 实例的代码,代码更加清晰。
2)性能优势: shared_ptr 有两个指针成员,一是管理对象的普通指针,二是引用计数指针。采用new的话,底层会调用两次次分配内存接口,并且将这二者分在两个内存块中。倘若使用make_shared, 底层一次就分配好这两者成员的内存,并且分在一个内存块中,性能更好。
3)降低内存泄漏风险:使用make_shared可以避免由于异常导致的部分已分配内存未释放的问题。
循环引用问题:
示例代码:
struct ListNode {
ListNode(int val) : _val(val)
{}
int _val;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
};
int main() {
auto node1 = std::make_shared<ListNode>(11); // node1 计数=1
auto node2 = std::make_shared<ListNode>(22); // node2 计数=1
node1->_next = node2; // node2 计数+1 → 2
node2->_prev = node1; // node1 计数+1 → 2
}

以编写双向链表为例,node1,node2,prev,next指向彼此,让引用计数都上升到了2。node2, node1出作用域时,都会先将自己的引用计数减一,这就导致二者的引用计数为一,都无法释放资源,且node1, node2出作用域了,我们访问不到这两块资源了,就内存泄漏了。
解决方案:替换定义中的 _prev , _next的其中之一为weak_ptr。
3. weak_ptr:
std::weak_ptr 的核心特性:
-
不增加引用计数:
weak_ptr不会增加所指对象的引用计数,因此不会影响对象的生命周期。即使weak_ptr存在,对象仍可能被销毁。 -
必须从
shared_ptr或另一个weak_ptr构造
auto shared = std::make_shared<int>(42); // 创建 shared_ptr
std::weak_ptr<int> weak(shared); // 从 shared_ptr 构造
- 无法直接访问对象
weak_ptr不能直接解引用(如*weak或weak->member),必须先通过lock()转换为shared_ptr:
if (auto shared = weak.lock()) { // 检查对象是否存活并获取 shared_ptr
std::cout << *shared << std::endl;
} else {
std::cout << "Object expired" << std::endl;
}
使用weak_ptr 消除循环引用
思路:替换_next或者_prev其中之一为weak_ptr
代码:
struct ListNode {
ListNode(int val) : _val(val)
{}
int _val;
std::shared_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev; // 弱引用
};
int main() {
auto node1 = std::make_shared<ListNode>(11); // node1 计数=1
auto node2 = std::make_shared<ListNode>(22); // node2 计数=1
node1->_next = node2; // node2 计数+1 → 2
node2->_prev = node1; // node1 计数仍为1
}
node1引用计数为1,node2为2
析构顺序:node2(未释放(2-1=1))、node1(释放(1-1=0))、node1->next(析构1-1=0)、node2(释放)
先析构node2,其引用计数减到1,然后析构node1,其引用计数减到0,其所有资源都被销毁了,于是node1-> _next 析构了,则node2的引用计数也减到0了,node2也就被释放了。
三、 面试题
1. C++中为什么要引入make_shared?它有什么优点?
make_shared是C++11引入的工厂函数,用于安全创建shared_ptr管理的对象。相比直接使用new,它的优点包括:
- 代码更简洁,无需显式使用
new; - 异常安全,避免因构造函数异常导致的内存泄漏;
- 效率更高,通过单次内存分配同时存储对象和引用计数(而
shared_ptr<T>(new T)需要两次分配)。此外,make_shared还能防止隐式转换带来的潜在风险。
2. 介绍一下unique_ptr的原理
unique_ptr是一种独占式智能指针,通过移动语义确保对象的唯一所有权。它禁止拷贝构造和赋值,但允许通过std::move转移所有权。其原理基于删除拷贝操作符并实现移动操作符,同时在析构时自动释放管理的资源(调用delete或自定义删除器)。这种设计使得unique_ptr轻量高效(通常与原始指针大小相同),适合管理独占资源(如文件句柄、动态数组)。
3.请介绍一下shared_ptr的原理,shared_ptr是线程安全的吗?
-
shared_ptr通过引用计数实现资源的自动管理:多个shared_ptr共享同一个控制块(包含强引用计数和弱引用计数)。每次拷贝或赋值时,强引用计数递增;当引用计数减为0时,释放管理的对象。控制块的内存通常在首次创建shared_ptr时分配(如使用make_shared)。 -
线程安全性方面:
shared_ptr的引用计数操作是原子的(如递增、递减),因此多线程同时读写不同的shared_ptr实例是安全的;但对同一shared_ptr的读写操作需外部同步(如使用互斥锁),否则可能导致数据竞争。此外,通过shared_ptr访问对象成员时也不保证线程安全,需由对象自身确保。
4. 介绍一下weak_ptr的工作原理
weak_ptr是一种非拥有型智能指针,用于解决shared_ptr的循环引用问题。它通过弱引用观察shared_ptr管理的对象,不增加引用计数。weak_ptr的核心是指向shared_ptr内部的控制块(包含强引用计数和弱引用计数)。当需要访问对象时,可通过lock()方法尝试创建临时shared_ptr(若对象已销毁则返回空指针),从而安全地获取对象使用权。
更多推荐



所有评论(0)