设计 A:每个元素独立拥有 shared_ptr(推荐)

void test_vec_ptr() 
{
    std::vector<std::shared_ptr<Widget>> vec;
    for (int i = 1; i < 5; ++i) 
    {
        vec.emplace_back(std::make_shared<Widget>(i)); // 每个元素独立管理
    }
}

设计 B:单个 shared_ptr 管理整个 vector(谨慎使用)

void test_ptr_vec() 
{
    auto ptr = std::make_shared<std::vector<Widget>>(); // 整个 vector 被共享

    ptr->reserve(10);

    ptr->emplace_back(1);
    ptr->emplace_back(2);
    ptr->emplace_back(3);
}

一、两种设计的本质区别

一句话区别:

方案 A:元素被共享,容器本身通常是某个对象私有的。
方案 B:容器(含其中所有元素)被共享,元素是容器私有的。

二、内存布局(严谨版)

方案 A:vector<shared_ptr<Widget>>

要点:

  • vec 的元素缓冲区扩容时,里面移动的是 shared_ptr 对象(只是指针 + 控制块指针),代价很低。
  • 单个 Widget 的地址不会因为 vec 扩容而改变——因为它们在各自独立的堆块上,vector 只是重新排列指向它们的指针。

方案 B:shared_ptr<vector<Widget>>,且未扩容

方案 B:扩容之后

四层地址稳定性一句话总结:

方案 B 中,ptr 指向的是 vector 对象本身,vector 对象地址不会因扩容而改变;shared_ptr 不会失效。
扩容只会让 data() 和“指向某个元素的指针/引用/迭代器”可能失效。
所以方案 B 中 reserve() 只是性能优化(避免多次分配和迁移),不是正确性要求。
当且仅当你代码里跨扩容保存了元素地址/引用/迭代器时,才需要用 reserve() 把容量一次预留够,否则会出现悬垂指针。


方案 A 中 reserve() 的意义更小:扩容搬动的只是 shared_ptr(几字节),成本本身就很低。

四、方案 A 和方案 B 的优缺点
方案 A:vector<shared_ptr<T>>
优点:

支持多态:可以存派生类(vector<shared_ptr<Base>> 存 shared_ptr<Derived>)。
单个元素可以被其他地方长期持有,生命周期独立。
插入、删除、交换单个元素成本低:搬动的是 shared_ptr。
vector 扩容不会让元素地址失效(元素本身在独立堆块上)。
缺点:

访问是两跳:vector → shared_ptr → T,缓存不友好。
每个元素一次堆分配 + 一个控制块,内存开销和分配次数都更高(make_shared 可以把对象和控制块合并为一次分配,稍好一些)。
对小对象尤其不划算。
方案 B:shared_ptr<vector<T>>
优点:

元素连续存储,遍历缓存友好,可以用 SIMD、memcpy 等。
总分配次数少,内存紧凑。
shared_ptr 本身只有一个控制块,共享开销小。
拷贝 / 共享整张表非常轻量(只加引用计数)。
缺点:

无法直接存多态元素(vector<Base> 会发生对象切片)。要多态必须改成 shared_ptr<vector<unique_ptr<Base>>> 这类组合。
共享粒度是整张表:多个持有者共用整个 vector,并发修改需要外部同步。
扩容会让元素指针/引用/迭代器失效(但 shared_ptr 本身不会失效)。
把一个元素“借给”外部长期持有比较别扭,需要用 shared_ptr<vector<T>> + 下标,或者再做一层 aliasing shared_ptr。


五、代码示例
以下示例遵循:成员变量、函数名用 camelCase,类型用 PascalCase,构造函数用 explicit,尽量不写注释。

5.1 方案 A 基本用法

#include <iostream>
#include <memory>
#include <string>
#include <vector>

class Widget {
public:
    Widget(int id, std::string name) : id(id), name(std::move(name)) {}

    void print() const {
        std::cout << "Widget " << id << ": " << name << "\n";
    }

    void setName(std::string newName) { name = std::move(newName); }

    int getId() const { return id; }

private:
    int id;
    std::string name;
};

void demoVectorOfSharedPtr()
{
    std::vector<std::shared_ptr<Widget>> widgets;
    widgets.emplace_back(std::make_shared<Widget>(1, "A"));
    widgets.emplace_back(std::make_shared<Widget>(2, "B"));
    widgets.emplace_back(std::make_shared<Widget>(3, "C"));

    for (const auto& w : widgets) {
        w->print();
    }

    auto shared = widgets[0];
    std::cout << "use_count = " << shared.use_count() << "\n";

    widgets.erase(widgets.begin() + 1);
}

5.2 方案 B 基本用法

void demoSharedPtrOfVector()
{
    auto widgets = std::make_shared<std::vector<Widget>>();
    widgets->reserve(10);

    widgets->emplace_back(1, "A");
    widgets->emplace_back(2, "B");
    widgets->emplace_back(3, "C");

    for (const auto& w : *widgets) {
        w.print();
    }

    auto alsoWidgets = widgets;
    std::cout << "use_count = " << alsoWidgets.use_count() << "\n";
    std::cout << "size = " << alsoWidgets->size() << "\n";
}

5.3 多态:方案 A 可以,方案 B 不行

class Shape {
public:
    virtual ~Shape() = default;
    virtual void draw() const = 0;
};

class Circle : public Shape {
public:
    explicit Circle(double r) : radius(r) {}
    void draw() const override { std::cout << "Circle r=" << radius << "\n"; }
private:
    double radius;
};

class Rectangle : public Shape {
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    void draw() const override {
        std::cout << "Rect " << width << "x" << height << "\n";
    }
private:
    double width;
    double height;
};

void demoPolymorphism()
{
    std::vector<std::shared_ptr<Shape>> shapes;
    shapes.emplace_back(std::make_shared<Circle>(5.0));
    shapes.emplace_back(std::make_shared<Rectangle>(3.0, 4.0));

    for (const auto& s : shapes) {
        s->draw();
    }
}

想在方案 B 里支持多态,只能退化成这种组合:

auto shapes = std::make_shared<std::vector<std::unique_ptr<Shape>>>();
shapes->emplace_back(std::make_unique<Circle>(5.0));
shapes->emplace_back(std::make_unique<Rectangle>(3.0, 4.0));

5.4 验证四层地址稳定性
这个示例专门把 &ptr / ptr.get() / data() / 元素地址四件事分开打印,观察扩容后谁变、谁不变:

#include <iostream>
#include <memory>
#include <vector>

class Tag {
public:
    explicit Tag(int v) : id(v) {}
    int id;
};

int main()
{
    auto ptr = std::make_shared<std::vector<Tag>>();
    ptr->emplace_back(1);

    const auto ptrVarAddress = &ptr;
    const auto vectorObjectAddress = ptr.get();
    const auto dataAddressBefore = ptr->data();
    const auto firstElementAddressBefore = &(*ptr)[0];

    for (int i = 2; i < 10; ++i) {
        ptr->emplace_back(i);
    }

    std::cout << "&ptr stable:        "
              << (&ptr == ptrVarAddress ? "yes" : "no") << "\n";
    std::cout << "ptr.get() stable:   "
              << (ptr.get() == vectorObjectAddress ? "yes" : "no") << "\n";
    std::cout << "data() stable:      "
              << (ptr->data() == dataAddressBefore ? "yes" : "no (reallocated)")
              << "\n";
    std::cout << "&elem[0] still ok:  "
              << (&(*ptr)[0] == firstElementAddressBefore
                  ? "yes"
                  : "no (old element address invalidated)")
              << "\n";
}

输出:

&ptr stable:        yes
ptr.get() stable:   yes
data() stable:      no (reallocated)
&elem[0] still ok:  no (old element address invalidated)

六、性能与内存特征(定性结论,不做 micro-benchmark 保证)

对小对象(比如几十字节以内),方案 B 在遍历和内存占用上通常明显优于方案 A。对大对象 + 多态 + 生命周期各异的场景,方案 A 基本是唯一合理选择。

七、并发注意事项

两种方案都需要外部同步来保护数据,不要把“shared_ptr 是线程安全的”理解成“它指向的对象也是线程安全的”。

  • 方案 A:可以做得更细——不同线程各自持有指向不同元素的 shared_ptr,相互独立。但对同一个元素的读写仍需同步;对 vector 本身的修改(push_backerase、扩容)也需要同步。
  • 方案 B:天然是“整表共享”,对容器和元素的修改通常都要加同一把锁,除非额外设计(比如 copy-on-write 或分片)。

八、选择建议

先按照这两条判断:

  1. 需要多态? → 只能选方案 A(或方案 B 的 unique_ptr 变体)。
  2. 需要把单个元素交给其他模块长期持有? → 优先方案 A,否则方案 B 的元素地址随时可能失效。

上面两条都否定后,再看性能:

  1. 元素小、数量大、顺序遍历频繁 → 倾向方案 B。
  2. 元素大、增删频繁、修改集中在少数位置 → 倾向方案 A。
  3. 只是为“省指针”选方案 B,而没有 profile 数据支撑 → 先用方案 A,等 profile 证明瓶颈再换。

九、常见误区对照

十、一句话总结
vector<shared_ptr<T>>:“每个元素单独被共享”,安全、灵活、能多态,是默认选择。
shared_ptr<vector<T>>:“整张表作为一个整体被共享”,紧凑、缓存友好,是性能敏感场景的优化形态;代价是失去多态和“单元素独立生命周期”的能力。
稳定性口诀:&ptr 稳 → ptr.get() 稳 → data() 可能变 → 元素地址扩容后不能再用。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐