嵌入式现代C++教程——std::shared_ptr如何呢¶
unique_ptr在我们上一篇博客的时候,已经讲过了它可以表达资源独占的含义。那么,智能指针还有一个朋友,就是std::shared_ptr。理解std::shared_ptr,我们需要把 std::shared_ptr 想象成一个会记账的托管管家——谁拿着这把钥匙就多记一笔,钥匙都收回时,管家把东西收拾干净。听起来美好;在桌面/服务器上也常常很管用。但把它搬到内存受限、实时性敏感、没有操作系统的嵌入式世界,要先把账本翻一翻:这把"管家"到底要多大的办公桌、会不会老是在你耳边叨叨(原子操作)、会不会把内存分散得像散落的螺丝钉。
总分总的总¶
shared_ptr 很方便,但代价不小:额外内存、原子开销(多线程场景)、可能的堆分配与碎片化、以及循环引用导致的内存泄漏。在嵌入式优先考虑 unique_ptr 或更轻量的引用计数实现(intrusive/ref-counted pool)。只有当共享所有权确实能显著简化设计时,才用 shared_ptr。
shared_ptr 真正会付出的代价¶
- 控制块(control block)与额外内存
shared_ptr不只是个指针:它还有一个控制块(reference counts、deleter、allocator info 等)。最常见的情况是make_shared把对象和控制块一次性分配(节省一次分配,节约内存并提升局部性);而std::shared_ptr<T>(new T)则通常需要两次独立分配(对象 + 控制块),会增加碎片和开销。 - 原子操作(atomic increment/decrement)
默认实现对引用计数使用原子操作以保证多线程安全。每次拷贝/析构
shared_ptr都会产生一次原子加/减,这在高频路径或实时要求严格的场景里可能是致命的性能负担。 - 堆分配(malloc/free)与碎片 控制块与对象的分配会触发堆操作;在长期运行的嵌入式系统中频繁的堆分配会导致内存碎片,最终可能出现分配失败。
- 循环引用(memory leak)
两个对象互相持有
shared_ptr会导致引用计数永远不为 0,资源无法释放。要用weak_ptr打破环路。 - 不可在 ISR 中使用
因为会涉及堆和原子操作,不要在中断服务例程(ISR)中使用
shared_ptr或执行堆分配/释放。
怎么用(以及如何用得更安全、更高效)¶
推荐:尽量用 std::make_shared¶
make_shared 在多数实现里把控制块和对象放在一个连续内存块里,既减少一次分配,也提升缓存友好性。对于嵌入式,这点非常重要:少一次 malloc 就少一次碎片风险。
查看完整可编译示例
// std::shared_ptr 基本用法与代价分析示例
// 演示 shared_ptr 的使用和嵌入式环境下的注意事项
#include <memory>
#include <cstdio>
#include <cstdio>
struct Widget {
int id;
Widget(int i) : id(i) {
printf("Widget %d constructed\n", id);
}
~Widget() {
printf("Widget %d destroyed\n", id);
}
void use() const {
printf("Using widget %d\n", id);
}
};
// 基本用法示例
void basic_example() {
printf("=== Basic shared_ptr Example ===\n");
auto p1 = std::make_shared<Widget>(1);
printf("After creation: use_count = %ld\n", p1.use_count());
{
auto p2 = p1; // 拷贝,引用计数增加
printf("After copy: use_count = %ld\n", p1.use_count());
p2->use();
}
printf("After p2 destroyed: use_count = %ld\n", p1.use_count());
}
// 内存开销示例
void memory_overhead_example() {
printf("\n=== Memory Overhead Example ===\n");
printf("sizeof(int*): %zu bytes\n", sizeof(int*));
printf("sizeof(std::unique_ptr<int>): %zu bytes\n", sizeof(std::unique_ptr<int>));
printf("sizeof(std::shared_ptr<int>): %zu bytes\n", sizeof(std::shared_ptr<int>));
printf("sizeof(std::weak_ptr<int>): %zu bytes\n", sizeof(std::weak_ptr<int>));
// 控制块通常单独分配
auto p = std::make_shared<int>(42);
printf("Control block address: %p\n", p.get());
}
// 自定义删除器示例
void custom_deleter_example() {
printf("\n=== Custom Deleter Example ===\n");
auto deleter = [](Widget* w) {
printf("[Custom Deleter] Destroying widget %d\n", w->id);
delete w;
};
std::shared_ptr<Widget> p(new Widget(100), deleter);
p->use();
printf("use_count: %ld\n", p.use_count());
}
// make_shared 优化示例
void make_shared_example() {
printf("\n=== make_shared Optimization ===\n");
// make_shared 一次性分配对象和控制块
auto p1 = std::make_shared<Widget>(200);
printf("Widget created with make_shared\n");
// 相比之下,这种方式需要两次分配(对象 + 控制块)
std::shared_ptr<Widget> p2(new Widget(201));
printf("Widget created with shared_ptr constructor\n");
}
int main() {
basic_example();
memory_overhead_example();
custom_deleter_example();
make_shared_example();
printf("\n=== All Examples Complete ===\n");
return 0;
}
如果你在乎内存布局,考虑显式池 + 自定义删除器¶
如果你自己有内存池(你很可能有 —— 你之前的项目就实现过内存池),可以让 shared_ptr 在销毁时把内存还给池,而不是 delete:
// 假设有全局池 g_pool:alloc/free
void pool_deleter(MyType* p) {
p->~MyType();
g_pool.free(p);
}
std::shared_ptr<MyType> make_from_pool() {
void* mem = g_pool.alloc(sizeof(MyType));
if(!mem) throw std::bad_alloc();
MyType* obj = new(mem) MyType(...); // placement new
return std::shared_ptr<MyType>(obj, pool_deleter);
}
注意:上面这种方式如果你用 std::shared_ptr<T>(new T) 的替代,要避免额外的控制块分配问题;最好把 shared_ptr 构造时直接传入自定义删除器(如例)。
破环:用 weak_ptr 避免循环引用¶
struct A;
struct B {
std::shared_ptr<A> a;
};
struct A {
std::weak_ptr<B> b; // 如果用 shared_ptr,会循环引用
};
weak_ptr 不增加引用计数,仅查看对象是否仍存活。
查看完整可编译示例
// std::shared_ptr 循环引用与 weak_ptr 示例
// 演示循环引用导致的内存泄漏和 weak_ptr 的解决方案
#include <memory>
#include <cstdio>
#include <string>
// ============ 循环引用问题示例 ============
struct NodeBad {
std::string name;
std::shared_ptr<NodeBad> next;
NodeBad(const std::string& n) : name(n) {
printf("NodeBad '%s' constructed\n", name.c_str());
}
~NodeBad() {
printf("NodeBad '%s' destroyed\n", name.c_str());
}
void set_next(std::shared_ptr<NodeBad> n) {
next = n;
}
};
// 演示循环引用导致的内存泄漏
void circular_reference_leak() {
printf("=== Circular Reference Leak Demo ===\n");
auto node_a = std::make_shared<NodeBad>("A");
auto node_b = std::make_shared<NodeBad>("B");
printf("Before linking: A.use_count() = %ld, B.use_count() = %ld\n",
node_a.use_count(), node_b.use_count());
// 创建循环:A -> B, B -> A
node_a->set_next(node_b);
node_b->set_next(node_a);
printf("After linking: A.use_count() = %ld, B.use_count() = %ld\n",
node_a.use_count(), node_b.use_count());
// 离开作用域时,两个节点的引用计数都变成 1,不会被释放!
// 这是内存泄漏
}
// ============ 使用 weak_ptr 解决循环引用 ============
struct NodeGood {
std::string name;
std::shared_ptr<NodeGood> next;
std::weak_ptr<NodeGood> prev; // 使用 weak_ptr 打破循环
NodeGood(const std::string& n) : name(n) {
printf("NodeGood '%s' constructed\n", name.c_str());
}
~NodeGood() {
printf("NodeGood '%s' destroyed\n", name.c_str());
}
void set_next(std::shared_ptr<NodeGood> n) {
next = n;
n->prev = shared_from_this();
}
void print_prev() const {
if (auto p = prev.lock()) {
printf(" Prev: %s\n", p->name.c_str());
} else {
printf(" Prev: (null)\n");
}
}
};
// 正确的双向链表实现
void correct_doubly_linked_list() {
printf("\n=== Correct Doubly Linked List Demo ===\n");
auto node_a = std::make_shared<NodeGood>("A");
auto node_b = std::make_shared<NodeGood>("B");
printf("Before linking: A.use_count() = %ld, B.use_count() = %ld\n",
node_a.use_count(), node_b.use_count());
node_a->set_next(node_b);
printf("After linking: A.use_count() = %ld, B.use_count() = %ld\n",
node_a.use_count(), node_b.use_count());
node_b->print_prev();
}
// ============ enable_shared_from_this 示例 ============
class Task : public std::enable_shared_from_this<Task> {
std::string name_;
public:
Task(const std::string& name) : name_(name) {
printf("Task '%s' constructed\n", name_.c_str());
}
~Task() {
printf("Task '%s' destroyed\n", name_.c_str());
}
// 正确获取指向自身的 shared_ptr
std::shared_ptr<Task> get_shared() {
return shared_from_this();
}
void run() {
printf("Task '%s' running\n", name_.c_str());
}
};
void enable_shared_from_this_example() {
printf("\n=== enable_shared_from_this Demo ===\n");
auto task = std::make_shared<Task>("ImportantTask");
printf("Initial use_count: %ld\n", task.use_count());
// 从成员函数获取 shared_ptr,不会创建新的控制块
auto task2 = task->get_shared();
printf("After get_shared: use_count = %ld\n", task.use_count());
task->run();
}
// ============ weak_ptr 锁定示例 ============
void weak_ptr_lock_example() {
printf("\n=== weak_ptr lock() Example ===\n");
auto sp = std::make_shared<NodeGood>("Temp");
std::weak_ptr<NodeGood> wp = sp;
printf("weak_ptr expired: %s\n", wp.expired() ? "yes" : "no");
if (auto locked = wp.lock()) {
printf("Locked successfully: %s\n", locked->name.c_str());
}
sp.reset(); // 销毁对象
printf("After reset, weak_ptr expired: %s\n", wp.expired() ? "yes" : "no");
if (auto locked = wp.lock()) {
printf("This won't print\n");
} else {
printf("lock() failed, object was destroyed\n");
}
}
int main() {
circular_reference_leak();
// 注意:node_a 和 node_b 没有被销毁(内存泄漏)
correct_doubly_linked_list();
// node_a 和 node_b 会被正确销毁
enable_shared_from_this_example();
weak_ptr_lock_example();
printf("\n=== All Examples Complete ===\n");
printf("Notice: In circular_reference_leak(), the destructors were NOT called.\n");
return 0;
}
enable_shared_from_this 的正确使用¶
如果对象方法需要返回一个 shared_ptr 指向自身,请通过 enable_shared_from_this 实现,而别尝试用 shared_ptr(this)(那会导致双重控制块和极其糟糕的后果):
struct Foo : std::enable_shared_from_this<Foo> {
std::shared_ptr<Foo> getptr() { return shared_from_this(); }
};
查看完整可编译示例
// std::shared_ptr 循环引用与 weak_ptr 示例
// 演示循环引用导致的内存泄漏和 weak_ptr 的解决方案
#include <memory>
#include <cstdio>
#include <string>
// ============ 循环引用问题示例 ============
struct NodeBad {
std::string name;
std::shared_ptr<NodeBad> next;
NodeBad(const std::string& n) : name(n) {
printf("NodeBad '%s' constructed\n", name.c_str());
}
~NodeBad() {
printf("NodeBad '%s' destroyed\n", name.c_str());
}
void set_next(std::shared_ptr<NodeBad> n) {
next = n;
}
};
// 演示循环引用导致的内存泄漏
void circular_reference_leak() {
printf("=== Circular Reference Leak Demo ===\n");
auto node_a = std::make_shared<NodeBad>("A");
auto node_b = std::make_shared<NodeBad>("B");
printf("Before linking: A.use_count() = %ld, B.use_count() = %ld\n",
node_a.use_count(), node_b.use_count());
// 创建循环:A -> B, B -> A
node_a->set_next(node_b);
node_b->set_next(node_a);
printf("After linking: A.use_count() = %ld, B.use_count() = %ld\n",
node_a.use_count(), node_b.use_count());
// 离开作用域时,两个节点的引用计数都变成 1,不会被释放!
// 这是内存泄漏
}
// ============ 使用 weak_ptr 解决循环引用 ============
struct NodeGood {
std::string name;
std::shared_ptr<NodeGood> next;
std::weak_ptr<NodeGood> prev; // 使用 weak_ptr 打破循环
NodeGood(const std::string& n) : name(n) {
printf("NodeGood '%s' constructed\n", name.c_str());
}
~NodeGood() {
printf("NodeGood '%s' destroyed\n", name.c_str());
}
void set_next(std::shared_ptr<NodeGood> n) {
next = n;
n->prev = shared_from_this();
}
void print_prev() const {
if (auto p = prev.lock()) {
printf(" Prev: %s\n", p->name.c_str());
} else {
printf(" Prev: (null)\n");
}
}
};
// 正确的双向链表实现
void correct_doubly_linked_list() {
printf("\n=== Correct Doubly Linked List Demo ===\n");
auto node_a = std::make_shared<NodeGood>("A");
auto node_b = std::make_shared<NodeGood>("B");
printf("Before linking: A.use_count() = %ld, B.use_count() = %ld\n",
node_a.use_count(), node_b.use_count());
node_a->set_next(node_b);
printf("After linking: A.use_count() = %ld, B.use_count() = %ld\n",
node_a.use_count(), node_b.use_count());
node_b->print_prev();
}
// ============ enable_shared_from_this 示例 ============
class Task : public std::enable_shared_from_this<Task> {
std::string name_;
public:
Task(const std::string& name) : name_(name) {
printf("Task '%s' constructed\n", name_.c_str());
}
~Task() {
printf("Task '%s' destroyed\n", name_.c_str());
}
// 正确获取指向自身的 shared_ptr
std::shared_ptr<Task> get_shared() {
return shared_from_this();
}
void run() {
printf("Task '%s' running\n", name_.c_str());
}
};
void enable_shared_from_this_example() {
printf("\n=== enable_shared_from_this Demo ===\n");
auto task = std::make_shared<Task>("ImportantTask");
printf("Initial use_count: %ld\n", task.use_count());
// 从成员函数获取 shared_ptr,不会创建新的控制块
auto task2 = task->get_shared();
printf("After get_shared: use_count = %ld\n", task.use_count());
task->run();
}
// ============ weak_ptr 锁定示例 ============
void weak_ptr_lock_example() {
printf("\n=== weak_ptr lock() Example ===\n");
auto sp = std::make_shared<NodeGood>("Temp");
std::weak_ptr<NodeGood> wp = sp;
printf("weak_ptr expired: %s\n", wp.expired() ? "yes" : "no");
if (auto locked = wp.lock()) {
printf("Locked successfully: %s\n", locked->name.c_str());
}
sp.reset(); // 销毁对象
printf("After reset, weak_ptr expired: %s\n", wp.expired() ? "yes" : "no");
if (auto locked = wp.lock()) {
printf("This won't print\n");
} else {
printf("lock() failed, object was destroyed\n");
}
}
int main() {
circular_reference_leak();
// 注意:node_a 和 node_b 没有被销毁(内存泄漏)
correct_doubly_linked_list();
// node_a 和 node_b 会被正确销毁
enable_shared_from_this_example();
weak_ptr_lock_example();
printf("\n=== All Examples Complete ===\n");
printf("Notice: In circular_reference_leak(), the destructors were NOT called.\n");
return 0;
}
轻量侵入式引用计数示例(单线程场景友好)¶
当你确定运行在单线程或你能保证外层加锁时,非原子计数更快、更节省空间:
struct RefCounted {
int ref = 0; // 非原子——只在单线程或外层已加锁时使用
void add_ref() { ++ref; }
int release_ref() { return --ref; }
protected:
virtual ~RefCounted() = default;
};
template<typename T>
class SimpleIntrusivePtr {
T* p = nullptr;
public:
SimpleIntrusivePtr(T* t = nullptr) : p(t) { if(p) p->add_ref(); }
SimpleIntrusivePtr(const SimpleIntrusivePtr& o) : p(o.p) { if(p) p->add_ref(); }
SimpleIntrusivePtr& operator=(const SimpleIntrusivePtr& o){
if(p==o.p) return *this;
if(p && p->release_ref()==0) delete p;
p = o.p;
if(p) p->add_ref();
return *this;
}
~SimpleIntrusivePtr(){ if(p && p->release_ref()==0) delete p; }
T* get() const { return p; }
T* operator->() const { return p; }
};
优点:没有额外控制块分配、计数非常局部(对象内部),缺点:对象必须继承该基类,侵入性强。
常见误区(要是面试官问就拿出来秀)¶
- "
shared_ptr就是个指针,没啥开销。" —— 错。它有控制块、可能的额外分配、以及原子操作代价。 - "只要不循环引用,用
shared_ptr就安全。" —— 部分正确,但仍要考虑性能与内存碎片。 - "
make_shared比shared_ptr(new T)慢/一样" —— 通常make_shared更快且更节省内存(一次分配),更局部化缓存。
结论¶
- 把
shared_ptr当作工具箱里"有时必需但要慎用"的工具:当多处代码真正需要共享所有权且能接受额外开销时使用它。 - 在嵌入式优先考虑更轻量的替代:
unique_ptr、对象池 + 自定义删除器、或侵入式引用计数。 - 始终考虑分配次数、原子开销和 ISR 约束。把
make_shared、自定义删除器和weak_ptr作为你的防守手段——正确使用它们可以避免大多数坑。