智能指针shared_ptr:现代C++内存管理的答案,面试必考
从今天开始进入现代C++的世界。智能指针是C++11引入的最重要的特性之一,面试必考。
在机器人开发里,shared_ptr用得非常多。传感器对象、通信节点、算法模块……这些需要共享所有权的场景,几乎都用shared_ptr管理。
面试官问智能指针,通常从shared_ptr开始。因为它是三个智能指针里最复杂的,涉及引用计数、线程安全、循环引用等问题。
为什么需要智能指针
先看个问题。
void processSensor() {
Sensor* s = new LidarSensor();
s->init();
if (someError()) {
return; // 忘了delete,内存泄漏!
}
process(s);
delete s;
}
手动管理new/delete,很容易出问题。异常、提前return、复杂的控制流……任何一个地方忘了delete,就是内存泄漏。
智能指针的思路是:用栈上的对象管理堆上的内存。栈上的shared_ptr析构时,自动delete它管理的堆内存。这就是RAII思想。
void processSensor() {
auto s = std::make_shared<LidarSensor>();
s->init();
if (someError()) {
return; // s自动释放,不会泄漏
}
process(s);
// 函数结束,s自动释放
}
shared_ptr的基本用法
// 推荐用make_shared创建
auto p1 = std::make_shared<Sensor>();
// 也可以直接new(不推荐)
std::shared_ptr<Sensor> p2(new Sensor());
// 查看引用计数
cout << p1.use_count(); // 1
// 共享所有权
auto p3 = p1;
cout << p1.use_count(); // 2
// 重置
p3.reset();
cout << p1.use_count(); // 1
p1.reset();
// 此时Sensor对象被销毁
make_shared比直接new好在哪?两个原因。第一,make_shared只分配一次内存(对象和控制块一起),直接new要分配两次(对象一次,控制块一次)。第二,make_shared异常安全——如果参数构造过程中抛异常,直接new可能泄漏。
引用计数的底层实现
面试深挖:"shared_ptr的引用计数是怎么实现的?"
每个shared_ptr关联一个控制块(control block),控制块里存着:强引用计数、弱引用计数、析构器、分配器等。
shared_ptr<Sensor> p1 ──→ 控制块 ──→ Sensor对象
│
├── 强引用计数: 2
├── 弱引用计数: 0
└── 析构器: delete
shared_ptr<Sensor> p2 ───┘
拷贝shared_ptr的时候,控制块的强引用计数+1。析构或reset的时候,计数-1。当强引用计数变为0,销毁对象。当弱引用计数也变为0,销毁控制块。
控制块是线程安全的——引用计数的增减操作是原子的。但注意:被管理的对象本身不是线程安全的。两个线程同时通过shared_ptr修改同一个对象,还是需要加锁。
循环引用:shared_ptr最大的坑
shared_ptr有个致命的问题:循环引用会导致内存泄漏。
class Node {
public:
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev;
~Node() { cout << "Node destroyed" << endl; }
};
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->next = n2;
n2->prev = n1;
// 离开作用域时,n1和n2的引用计数都是2
// n1析构后计数变为1(n2->prev还指着它)
// n2析构后计数变为1(n1->next还指着它)
// 两个对象的引用计数永远到不了0,永远不会被销毁!
"Node destroyed"永远不会被打印。这就是循环引用导致的内存泄漏。
解决方案是用weak_ptr打破循环。这个下篇详细讲。
自定义删除器
shared_ptr支持自定义删除器,这在机器人开发里很实用。
// 管理C语言的资源
FILE* fp = fopen("data.txt", "r");
std::shared_ptr<FILE> file_ptr(fp, [](FILE* f) {
fclose(f);
});
// 管理ROS节点
auto node = std::shared_ptr<rclcpp::Node>(
rclcpp::Node::create("my_node"),
[](rclcpp::Node* n) {
n->shutdown();
delete n;
}
);
自定义删除器不影响shared_ptr的类型——shared_ptr<T>的删除器类型不是模板参数的一部分。这和unique_ptr不同(下篇会讲)。
enable_shared_from_this
还有个容易踩的坑:在类的成员函数里需要获取指向自身的shared_ptr。
class Sensor {
public:
std::shared_ptr<Sensor> getPtr() {
return shared_from_this(); // 安全地获取自身的shared_ptr
}
};
// 必须继承enable_shared_from_this
class Sensor : public std::enable_shared_from_this<Sensor> {
public:
std::shared_ptr<Sensor> getShared() {
return shared_from_this();
}
};
auto s = std::make_shared<Sensor>();
auto p = s->getShared(); // p和s共享所有权,引用计数为2
如果不继承enable_shared_from_this,直接shared_ptr<Sensor>(this)会创建新控制块,导致double free。
在机器人开发里,这个模式常用于回调注册。传感器对象需要把自己注册到数据处理管线里时,就需要传一个shared_ptr给管线。
shared_ptr在机器人开发里的实践建议
几个实际项目中的经验:
第一,工厂函数返回shared_ptr。传感器创建、算法模块初始化,都返回shared_ptr而不是裸指针,调用方不需要关心内存释放。
第二,函数参数用const shared_ptr&传递。不要按值传shared_ptr,否则会多一次引用计数的原子增减操作。
// 不好:按值传递,引用计数+1再-1,有原子操作开销
void process(std::shared_ptr<Sensor> s);
// 好:const引用传递,零开销
void process(const std::shared_ptr<Sensor>& s);
// 更好:如果函数不需要共享所有权,传裸指针或引用
void process(Sensor& s);
第三,不要滥用shared_ptr。所有权明确的时候用unique_ptr,shared_ptr的引用计数有性能开销,高频场景不可忽视。
给正在准备面试的你一点建议
shared_ptr在面试里考得非常细。
必须掌握的:make_shared的优势、引用计数的机制、循环引用问题。
加分项:知道控制块的结构、引用计数的线程安全性、自定义删除器的用法。
面试有个经典问题:"shared_ptr和裸指针相比有什么缺点?"答案是引用计数的原子操作有性能开销。在对性能极度敏感的场景(比如每帧执行几万次的热路径),可能需要考虑用裸指针或weak_ptr来避免引用计数的开销。
下篇讲unique_ptr和weak_ptr——三种智能指针的对比和选型。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。
「机器人软件开发面试·从入门到精通」连载系列 上一篇:第34篇 迭代器模式——STL的灵魂设计思想 下一篇预告:第36篇 智能指针unique_ptr与weak_ptr——三种智能指针的选型
有任何问题欢迎评论区留言,我会尽量回复。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)