跳转至

嵌入式现代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 真正会付出的代价

  1. 控制块(control block)与额外内存 shared_ptr 不只是个指针:它还有一个控制块(reference counts、deleter、allocator info 等)。最常见的情况是 make_shared 把对象和控制块一次性分配(节省一次分配,节约内存并提升局部性);而 std::shared_ptr<T>(new T) 则通常需要两次独立分配(对象 + 控制块),会增加碎片和开销。
  2. 原子操作(atomic increment/decrement) 默认实现对引用计数使用原子操作以保证多线程安全。每次拷贝/析构 shared_ptr 都会产生一次原子加/减,这在高频路径或实时要求严格的场景里可能是致命的性能负担。
  3. 堆分配(malloc/free)与碎片 控制块与对象的分配会触发堆操作;在长期运行的嵌入式系统中频繁的堆分配会导致内存碎片,最终可能出现分配失败。
  4. 循环引用(memory leak) 两个对象互相持有 shared_ptr 会导致引用计数永远不为 0,资源无法释放。要用 weak_ptr 打破环路。
  5. 不可在 ISR 中使用 因为会涉及堆和原子操作,不要在中断服务例程(ISR)中使用 shared_ptr 或执行堆分配/释放

怎么用(以及如何用得更安全、更高效)

推荐:尽量用 std::make_shared

auto p = std::make_shared<MyType>(args...);

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_sharedshared_ptr(new T) 慢/一样" —— 通常 make_shared 更快且更节省内存(一次分配),更局部化缓存。

结论

  • shared_ptr 当作工具箱里"有时必需但要慎用"的工具:当多处代码真正需要共享所有权且能接受额外开销时使用它。
  • 在嵌入式优先考虑更轻量的替代:unique_ptr、对象池 + 自定义删除器、或侵入式引用计数。
  • 始终考虑分配次数、原子开销和 ISR 约束。把 make_shared、自定义删除器和 weak_ptr 作为你的防守手段——正确使用它们可以避免大多数坑。