导读:本期聚焦于梁博渊创作的《C++如何在复合对象中使用智能指针?掌握这几类场景的正确写法》,敬请观看详情。把裸指针塞进类成员里,析构时忘了释放就内存泄漏,复制时浅拷贝就双重释放,这类问题在C++项目里反复出现。智能指针正是解决这些问题的利器,但一到复合对象场景,不少人就开始纠结:类成员该用unique_ptr还是shared_ptr?拷贝构造和赋值怎么处理?循环引用又该怎么破?本文围绕复合对象中智能指针的使用展开,先讲清楚unique_ptr、shared_ptr、weak_ptr各自的适用场景,再给出类成员设计、深拷贝实现、打破循环引用的完整代码示例,最后总结几条工程实践中踩过的坑,帮你写出内存安全又易于维护的C++代码。

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

C++如何在复合对象中使用智能指针?掌握这几类场景的正确写法

三种智能指针在复合对象中的定位

标准库提供了三种智能指针:std::unique_ptrstd::shared_ptrstd::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_ptrunique_ptr默认不可拷贝,能促使你显式设计拷贝语义,而shared_ptr用起来太顺手,容易被滥用成到处共享的网状依赖,最后没人说得清对象到底什么时候销毁。默认独占、按需共享,是保持所有权清晰的原则。

第二,函数参数尽量不传智能指针。如果函数只是要使用子对象,传引用或裸指针即可,所有权不发生转移就没必要把智能指针传来传去。只有当函数要接管或共享所有权时,才以值传递的方式接收unique_ptrshared_ptr,让所有权转移在函数签名上一目了然。

第三,类成员中的智能指针要考虑对类默认函数的影响。含unique_ptr成员的类拷贝被删除但移动构造可用;含shared_ptr成员的类默认拷贝是浅拷贝引用计数,如果业务上需要深拷贝共享的子对象,必须自己实现。析构函数方面,如果父类会被继承并在多态场景下通过基类指针删除,基类析构函数必须声明为virtual,智能指针只能保证调用所指对象完整类型的析构,多态删除的错误它救不了。

第四,注意构造过程中的泄漏风险。在构造函数里用new出来的指针初始化多个shared_ptr成员时,如果第二个初始化表达式抛异常,第一个已构造的成员能正常清理,但如果同一个裸指针被两个shared_ptr分别接管,就会双重释放。坚持用make_uniquemake_shared,从源头上避免裸指针和智能指针混用,这类问题基本可以根绝。

总结一下,复合对象中使用智能指针的关键在于先理清所有权关系:独占用unique_ptr,共享用shared_ptr,观察和反向引用用weak_ptr。所有权模型清晰了,析构、拷贝、循环引用这些问题就都有了标准答案,代码的可读性和可维护性也会随之提升。

C++智能指针unique_ptrshared_ptr修改时间:2026-09-15 08:08:40

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260915/57144.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。