当一个类内部持有另一个对象的引用时,比如一个订单类里包含商品信息、一个部门对象里挂着多个员工,这就构成了复合对象。传统做法是用裸指针或引用成员,但随之而来的是资源释放、拷贝语义、异常安全等一系列麻烦。C++11引入的智能指针让这些问题有了系统性的解法,不过在复合对象里用智能指针并不是简单地换个类型声明那么容易,选错指针类型或忽略拷贝语义,照样会踩坑。本文结合具体代码,把复合对象中智能指针的正确用法讲透。

三种智能指针在复合对象中的定位
标准库提供了三种智能指针:std::unique_ptr、std::shared_ptr和std::weak_ptr,它们在复合对象中扮演的角色完全不同。
std::unique_ptr表达的是独占所有权。也就是说,一个子对象只属于一个父对象,父对象析构时子对象自动跟着销毁。这是复合对象中最常见、也是最推荐的形态。比如一辆车拥有它的引擎,引擎不会同时被两辆车共用,这种严格的从属关系用unique_ptr再合适不过。它的开销几乎为零,只是比裸指针多了一次析构时的delete调用,没有引用计数的原子操作成本。
std::shared_ptr表达的是共享所有权。当多个父对象可能引用同一个子对象时,比如多个订单共享同一个客户资料对象,就需要引用计数来管理生命周期。shared_ptr的拷贝会使计数加一,最后一个持有者析构时对象才销毁。它的代价是引用计数的原子增减带来的性能损耗,以及控制块占用的额外内存。在复合对象中,只有在确实存在共享关系时才应该用它,不要图省事一律用shared_ptr,那样既浪费性能又容易埋下循环引用的隐患。
std::weak_ptr本身不拥有对象,它是对shared_ptr所管理对象的弱引用,主要用于观察对象或者打破循环引用。如果父对象持有子对象的shared_ptr,而子对象又需要反向访问父对象,子对象那边就应该用weak_ptr,否则两个对象互相强引用,谁也析构不了,内存就永久泄漏了。
用unique_ptr设计独占式复合对象
来看一个典型的独占式复合结构:一个Team对象拥有若干个Member。用unique_ptr实现的写法如下:
#include <memory>
#include <string>
#include <vector>
#include <iostream>
class Member {
public:
explicit Member(std::string name) : name_(std::move(name)) {}
const std::string& name() const { return name_; }
private:
std::string name_;
};
class Team {
public:
explicit Team(std::string teamName) : teamName_(std::move(teamName)) {}
// 添加成员,内部创建并独占持有
void addMember(std::string name) {
members_.push_back(std::make_unique<Member>(std::move(name)));
}
// 提供只读访问,不转移所有权
const Member& memberAt(size_t i) const { return *members_.at(i); }
~Team() { std::cout << "Team destroyed: " << teamName_ << std::endl; }
private:
std::string teamName_;
std::vector<std::unique_ptr<Member>> members_;
};
int main() {
Team t("Platform Group");
t.addMember("Alice");
t.addMember("Bob");
// t 离开作用域时,members_ 中每个 unique_ptr 自动释放 Member
return 0;
}
这段代码有几个值得注意的细节。第一,Team的析构函数里不需要写任何delete,vector析构时会依次析构每个unique_ptr,链条自动传导下去。第二,访问子对象时返回引用而不是指针,调用方无法偷偷保存指针绕过所有权模型。第三,unique_ptr是不可拷贝的,这直接导致Team的拷贝构造函数被隐式删除,这是编译器在强制你思考拷贝语义,其实是好事。
如果确实需要拷贝复合对象,就要自己实现深拷贝。深拷贝的核心思路是:遍历所有子对象,为每个子对象创建新的副本。可以封装一个clone方法来完成这件事:
class Team {
public:
Team(const Team& other) : teamName_(other.teamName_) {
members_.reserve(other.members_.size());
for (const auto& m : other.members_) {
members_.push_back(std::make_unique<Member>(*m)); // 逐个深拷贝
}
}
Team& operator=(const Team& other) {
if (this != &other) {
teamName_ = other.teamName_;
members_.clear();
members_.reserve(other.members_.size());
for (const auto& m : other.members_) {
members_.push_back(std::make_unique<Member>(*m));
}
}
return *this;
}
private:
std::string teamName_;
std::vector<std::unique_ptr<Member>> members_;
};
注意拷贝构造和拷贝赋值必须同时处理,否则会出现一半深拷贝一半浅拷贝的不一致状态。如果子类型存在继承体系,深拷贝需要在基类里定义虚函数clone,通过虚派发保证拷贝出来的是动态类型一致的副本,这一点在多态复合对象中尤其重要。
共享与观察:shared_ptr和weak_ptr的配合
当子对象确实被多方共享时,shared_ptr是正确选择。举个例子,一个缓存模块管理配置对象,多个业务模块同时持有配置的引用,任意一方都不能单独决定配置的生死:
#include <memory>
#include <string>
#include <vector>
class Config {
public:
explicit Config(std::string content) : content_(std::move(content)) {}
std::string content_;
};
class Service {
public:
explicit Service(std::shared_ptr<Config> cfg) : cfg_(std::move(cfg)) {}
private:
std::shared_ptr<Config> cfg_; // 多个 Service 可共享同一份配置
};
int main() {
auto config = std::make_shared<Config>("timeout=30");
Service a(config);
Service b(config);
// config 的引用计数为 3,全部析构后 Config 才被销毁
return 0;
}
用std::make_shared而不是先new再构造shared_ptr,可以让对象和控制块在一次内存分配中完成,既省内存又更快,而且避免了裸指针在构造中途泄漏的可能。
再来看循环引用问题。假设订单对象持有客户的shared_ptr用于业务处理,客户对象又想保存自己的历史订单列表以便查询。如果两边都用shared_ptr,就会形成引用环,引用计数永远降不到零。正确做法是强引用方向表达所有权,弱引用方向表达可观察性:
#include <memory>
#include <vector>
class Order; // 前置声明
class Customer {
public:
void addOrder(std::shared_ptr<class Order> order);
std::vector<std::weak_ptr<Order>> orders_; // 弱引用,不参与计数
};
class Order {
public:
std::shared_ptr<Customer> customer_; // 强引用,表达归属
double amount_ = 0.0;
};
int main() {
auto customer = std::make_shared<Customer>();
auto order = std::make_shared<Order>();
order->customer_ = customer;
customer->addOrder(order);
// Customer 持有 weak_ptr,不会阻止 Order 析构,环被打破
return 0;
}
使用weak_ptr访问对象时不能直接解引用,必须先调用lock()升级为shared_ptr,并检查返回值是否为空:
for (const auto& wp : customer->orders_) {
if (auto sp = wp.lock()) { // 尝试提升为强引用
// sp 有效,可以安全使用 *sp
} else {
// 订单已析构,跳过或做清理
}
}
工程实践中的几条经验
第一,优先选unique_ptr,需要共享时再换shared_ptr。unique_ptr默认不可拷贝,能促使你显式设计拷贝语义,而shared_ptr用起来太顺手,容易被滥用成到处共享的网状依赖,最后没人说得清对象到底什么时候销毁。默认独占、按需共享,是保持所有权清晰的原则。
第二,函数参数尽量不传智能指针。如果函数只是要使用子对象,传引用或裸指针即可,所有权不发生转移就没必要把智能指针传来传去。只有当函数要接管或共享所有权时,才以值传递的方式接收unique_ptr或shared_ptr,让所有权转移在函数签名上一目了然。
第三,类成员中的智能指针要考虑对类默认函数的影响。含unique_ptr成员的类拷贝被删除但移动构造可用;含shared_ptr成员的类默认拷贝是浅拷贝引用计数,如果业务上需要深拷贝共享的子对象,必须自己实现。析构函数方面,如果父类会被继承并在多态场景下通过基类指针删除,基类析构函数必须声明为virtual,智能指针只能保证调用所指对象完整类型的析构,多态删除的错误它救不了。
第四,注意构造过程中的泄漏风险。在构造函数里用new出来的指针初始化多个shared_ptr成员时,如果第二个初始化表达式抛异常,第一个已构造的成员能正常清理,但如果同一个裸指针被两个shared_ptr分别接管,就会双重释放。坚持用make_unique和make_shared,从源头上避免裸指针和智能指针混用,这类问题基本可以根绝。
总结一下,复合对象中使用智能指针的关键在于先理清所有权关系:独占用unique_ptr,共享用shared_ptr,观察和反向引用用weak_ptr。所有权模型清晰了,析构、拷贝、循环引用这些问题就都有了标准答案,代码的可读性和可维护性也会随之提升。
C++智能指针unique_ptrshared_ptr修改时间:2026-09-15 08:08:40