vector<shared_ptr> vs shared_ptr<vector>
设计 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_back、erase、扩容)也需要同步。 - 方案 B:天然是“整表共享”,对容器和元素的修改通常都要加同一把锁,除非额外设计(比如 copy-on-write 或分片)。
八、选择建议
先按照这两条判断:
- 需要多态? → 只能选方案 A(或方案 B 的
unique_ptr变体)。 - 需要把单个元素交给其他模块长期持有? → 优先方案 A,否则方案 B 的元素地址随时可能失效。
上面两条都否定后,再看性能:
- 元素小、数量大、顺序遍历频繁 → 倾向方案 B。
- 元素大、增删频繁、修改集中在少数位置 → 倾向方案 A。
- 只是为“省指针”选方案 B,而没有 profile 数据支撑 → 先用方案 A,等 profile 证明瓶颈再换。
九、常见误区对照

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



所有评论(0)