首页 > 后端开发 > C++ > 正文

如何在C++中使用智能指针_C++智能指针使用核心指南

下次还敢
发布: 2025-09-22 14:03:02
原创
344人浏览过
C++智能指针通过RAII机制自动管理内存,解决了内存泄漏、野指针、重复释放和异常安全等问题。std::unique_ptr提供独占所有权,适用于单一所有者场景;std::shared_ptr通过引用计数实现共享所有权,适合多所有者共同管理资源;std::weak_ptr作为非拥有观察者,用于打破shared_ptr的循环引用。选择时应优先使用unique_ptr,需要共享时用shared_ptr,并配合weak_ptr避免循环引用。常见陷阱包括shared_ptr循环引用、裸指针混用导致多次释放、未使用make系列函数带来的性能与异常风险,以及自定义删除器缺失。最佳实践是默认选用unique_ptr,优先使用make_unique和make_shared,避免裸指针操作,明确资源所有权语义,并在必要时继承enable_shared_from_this以安全返回shared_ptr。

如何在c++中使用智能指针_c++智能指针使用核心指南

C++中的智能指针,本质上是RAII(Resource Acquisition Is Initialization)原则的完美体现,它通过对象生命周期来自动管理内存,从而有效避免了内存泄漏、野指针等困扰C++开发者多年的顽疾。简单来说,它们就是对原始指针的封装,让内存管理变得自动化且更安全。

解决方案

要在C++中使用智能指针,你主要会和

std::unique_ptr
登录后复制
std::shared_ptr
登录后复制
以及
std::weak_ptr
登录后复制
打交道。它们各自有独特的职责和适用场景,理解这些差异是高效使用的关键。

1.

std::unique_ptr
登录后复制
:独占所有权

unique_ptr
登录后复制
正如其名,它表示对所管理对象拥有独占所有权。这意味着在任何时候,只有一个
unique_ptr
登录后复制
可以指向特定的资源。它不能被复制,但可以被移动。当
unique_ptr
登录后复制
超出作用域时,它会自动删除所指向的对象。

立即学习C++免费学习笔记(深入)”;

#include <iostream>
#include <memory> // 包含智能指针头文件

class MyClass {
public:
    MyClass() { std::cout << "MyClass 构造" << std::endl; }
    ~MyClass() { std::cout << "MyClass 析构" << std::endl; }
    void doSomething() { std::cout << "MyClass doing something." << std::endl; }
};

void processUniquePtr() {
    // 推荐使用 std::make_unique 创建 unique_ptr
    std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>();
    ptr1->doSomething();

    // unique_ptr 不能被复制,会报错:
    // std::unique_ptr<MyClass> ptr2 = ptr1; // 编译错误

    // 但可以被移动
    std::unique_ptr<MyClass> ptr2 = std::move(ptr1);
    if (ptr1 == nullptr) {
        std::cout << "ptr1 已经被移动,现在为空。" << std::endl;
    }
    ptr2->doSomething();

    // 当 ptr2 超出作用域时,MyClass 对象会被自动析构
} // ptr2 离开作用域,MyClass 对象析构

int main() {
    processUniquePtr();
    return 0;
}
登录后复制

2.

std::shared_ptr
登录后复制
:共享所有权

shared_ptr
登录后复制
则实现了共享所有权。多个
shared_ptr
登录后复制
可以同时指向同一个对象,并通过引用计数(reference count)来追踪有多少个
shared_ptr
登录后复制
正在管理这个对象。当最后一个
shared_ptr
登录后复制
被销毁或重新赋值时,它所指向的对象才会被删除。

#include <iostream>
#include <memory>

class AnotherClass {
public:
    AnotherClass() { std::cout << "AnotherClass 构造" << std::endl; }
    ~AnotherClass() { std::cout << "AnotherClass 析构" << std::endl; }
    void greet() { std::cout << "Hello from AnotherClass!" << std::endl; }
};

void processSharedPtr() {
    // 推荐使用 std::make_shared 创建 shared_ptr,效率更高
    std::shared_ptr<AnotherClass> s_ptr1 = std::make_shared<AnotherClass>();
    s_ptr1->greet();
    std::cout << "引用计数: " << s_ptr1.use_count() << std::endl; // 1

    std::shared_ptr<AnotherClass> s_ptr2 = s_ptr1; // 复制,共享所有权
    std::cout << "引用计数: " << s_ptr1.use_count() << std::endl; // 2

    std::shared_ptr<AnotherClass> s_ptr3;
    s_ptr3 = s_ptr1; // 再次复制
    std::cout << "引用计数: " << s_ptr1.use_count() << std::endl; // 3

    // 当 s_ptr2 离开作用域时,引用计数变为 2
    // 当 s_ptr3 离开作用域时,引用计数变为 1
    // 当 s_ptr1 离开作用域时,引用计数变为 0,AnotherClass 对象被析构
} // s_ptr1, s_ptr2, s_ptr3 离开作用域,AnotherClass 对象析构

int main() {
    processSharedPtr();
    return 0;
}
登录后复制

3.

std::weak_ptr
登录后复制
:非拥有观察者

weak_ptr
登录后复制
shared_ptr
登录后复制
的补充,它不拥有所指向的对象,因此不会影响对象的引用计数。它的主要作用是解决
shared_ptr
登录后复制
可能导致的循环引用问题。你可以把它看作是一个“旁观者”,它能观察到
shared_ptr
登录后复制
管理的对象,但不会阻止对象被销毁。你需要通过调用
lock()
登录后复制
方法来获取一个
shared_ptr
登录后复制
,如果对象已被销毁,
lock()
登录后复制
会返回一个空的
shared_ptr
登录后复制

#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr;
    A() { std::cout << "A 构造" << std::endl; }
    ~A() { std::cout << "A 析构" << std::endl; }
};

class B {
public:
    // 如果这里是 shared_ptr<A> a_ptr,就会形成循环引用
    std::weak_ptr<A> a_ptr; 
    B() { std::cout << "B 构造" << std::endl; }
    ~B() { std::cout << "B 析构" << std::endl; }
};

void createCircularReference() {
    std::shared_ptr<A> p_a = std::make_shared<A>();
    std::shared_ptr<B> p_b = std::make_shared<B>();

    p_a->b_ptr = p_b;
    p_b->a_ptr = p_a; // 使用 weak_ptr 避免循环引用

    // 此时,p_a 和 p_b 的引用计数都是 1。
    // 如果 B::a_ptr 也是 shared_ptr,那么 A 和 B 都无法被析构。
    // 但现在 B::a_ptr 是 weak_ptr,它不增加 A 的引用计数。

    if (auto shared_a = p_b->a_ptr.lock()) {
        std::cout << "B 仍然可以访问 A。" << std::endl;
    }
} // p_a, p_b 离开作用域,A 和 B 对象会被正确析构

int main() {
    createCircularReference();
    return 0;
}
登录后复制

C++智能指针解决了哪些内存管理难题?

在我看来,智能指针的出现,简直是C++内存管理领域的一场革命。它主要解决了传统C++裸指针(raw pointer)在内存管理中面临的几个核心难题,这些问题往往是导致程序不稳定、崩溃甚至安全漏洞的罪魁祸首。

首先,内存泄漏。这是最常见的问题,当你使用

new
登录后复制
分配内存后,如果忘记调用
delete
登录后复制
,或者在
delete
登录后复制
之前程序因异常提前退出,那么这块内存就永远不会被释放,造成内存泄漏。智能指针通过RAII机制,将内存的分配和释放绑定到对象的生命周期。一旦智能指针对象被销毁(比如超出作用域),它会自动调用析构函数来释放所管理的内存,根本上杜绝了忘记
delete
登录后复制
的可能。这就像你租了一个房子,智能指针就是那个到期会自动帮你退房的管家,你根本不用操心。

其次,野指针(dangling pointer)。当一块内存被

delete
登录后复制
后,如果还有其他指针指向这块内存,那么这些指针就成了野指针。再次访问它们会导致未定义行为,程序可能崩溃。
unique_ptr
登录后复制
shared_ptr
登录后复制
在管理资源时,确保了资源的唯一或共享所有权。特别是
unique_ptr
登录后复制
,它在被移动后会置空,避免了原指针成为野指针。
shared_ptr
登录后复制
则通过引用计数,保证只有当没有
shared_ptr
登录后复制
指向对象时才释放内存,大大降低了野指针的风险。

再者,重复释放(double free)。如果你不小心对同一块内存调用了两次

delete
登录后复制
,这也会导致未定义行为,通常是程序崩溃。智能指针内部机制会确保资源只被释放一次。
unique_ptr
登录后复制
的独占性保证了这一点,而
shared_ptr
登录后复制
的引用计数机制也同样能避免重复释放。

最后,异常安全。在传统C++代码中,如果在

new
登录后复制
delete
登录后复制
之间抛出异常,
delete
登录后复制
可能永远不会被执行,从而导致内存泄漏。智能指针的RAII特性使得它们在异常发生时也能正确地释放资源,因为对象的析构函数总会在展开时被调用。这让我们的代码在面对各种不可预测的情况时,依然能保持健壮性。对我个人而言,这一点尤其重要,它让我在编写复杂逻辑时能更专注于业务本身,而不是时刻担心内存问题。

unique_ptr、shared_ptr 和 weak_ptr 各自适用场景是什么?如何选择?

选择正确的智能指针,就像选择合适的工具来完成一项任务一样,是C++编程中一个非常实用的技能。它们各有侧重,理解这些才能避免“大炮打蚊子”或者“巧妇难为无米之炊”的窘境。

1.

std::unique_ptr
登录后复制
:独占所有权,清晰明了

  • 适用场景:

    • 单一所有者资源: 当你明确知道某个资源只应该被一个对象或一个代码块拥有时,
      unique_ptr
      登录后复制
      是首选。比如,一个文件句柄、一个数据库连接,或者一个工厂函数创建的对象,这些资源通常只归一个使用者所有。
    • 工厂函数返回对象: 当工厂函数创建了一个新对象并希望将所有权转移给调用者时,返回
      unique_ptr
      登录后复制
      是最佳实践。
    • PImpl(Pointer to Implementation)模式: 这是一种常见的C++设计模式,用于隐藏类的实现细节,减少编译依赖。
      unique_ptr
      登录后复制
      在这里完美契合,因为它能管理私有实现类的生命周期。
    • 作为类成员: 如果一个类拥有某个资源,并且这个资源不希望被其他对象共享,那么将它声明为
      unique_ptr
      登录后复制
      成员非常合适。
  • 如何选择: 只要资源不需要共享,就优先考虑

    unique_ptr
    登录后复制
    。它开销最小(几乎和裸指针一样),语义最清晰,强制了独占所有权,避免了不必要的复杂性。如果后续发现需要共享,再考虑升级到
    shared_ptr
    登录后复制

2.

std::shared_ptr
登录后复制
:共享所有权,复杂对象生命周期

Gnomic智能体平台
Gnomic智能体平台

国内首家无需魔法免费无限制使用的ChatGPT4.0,网站内设置了大量智能体供大家免费使用,还有五款语言大模型供大家免费使用~

Gnomic智能体平台 47
查看详情 Gnomic智能体平台
  • 适用场景:

    • 多个所有者共享资源: 当一个资源需要被多个对象共同管理,并且这些对象的生命周期相互独立时,
      shared_ptr
      登录后复制
      是理想选择。比如,一个配置对象、一个缓存数据,或者一个图形界面的控件,它们可能被多个模块引用。
    • 对象生命周期不确定: 如果你无法确定一个对象何时不再被需要,或者它的生命周期由多个不相关的部分共同决定,
      shared_ptr
      登录后复制
      的引用计数机制能确保对象在所有引用都消失后才被销毁。
    • 容器存储多态对象: 当容器需要存储指向基类的指针,但实际对象是派生类,且这些对象的生命周期需要被容器或其使用者共同管理时。
  • 如何选择: 当你明确需要多个对象共同管理一个资源的生命周期时,选择

    shared_ptr
    登录后复制
    。但要警惕循环引用问题,这往往是
    shared_ptr
    登录后复制
    最让人头疼的地方,也是
    weak_ptr
    登录后复制
    存在的理由。

3.

std::weak_ptr
登录后复制
:非拥有观察者,打破循环引用

  • 适用场景:

    • 打破
      shared_ptr
      登录后复制
      循环引用:
      这是
      weak_ptr
      登录后复制
      最核心、最主要的用途。当两个或多个对象通过
      shared_ptr
      登录后复制
      相互引用时,它们会形成一个引用环,导致引用计数永远不会降到零,从而造成内存泄漏。
      weak_ptr
      登录后复制
      作为其中一个引用,不增加引用计数,从而允许对象在其他
      shared_ptr
      登录后复制
      都失效后被正确销毁。
    • 观察者模式: 在某些观察者模式的实现中,观察者可能需要持有对主题(Subject)的引用,但不希望影响主题的生命周期。这时可以使用
      weak_ptr
      登录后复制
    • 缓存: 当你希望缓存某些对象,但又不希望缓存本身阻止这些对象被销毁时,可以使用
      weak_ptr
      登录后复制
      。如果缓存中的
      weak_ptr
      登录后复制
      过期,你可以选择重新创建或从其他地方获取对象。
  • 如何选择:

    weak_ptr
    登录后复制
    很少单独使用,它几乎总是与
    shared_ptr
    登录后复制
    搭配出现。当你发现使用了
    shared_ptr
    登录后复制
    后,出现了循环引用导致的内存泄漏,或者你需要观察一个对象而不影响其生命周期时,就应该考虑使用
    weak_ptr
    登录后复制
    。它本身不能直接访问对象,必须先通过
    lock()
    登录后复制
    方法尝试获取一个
    shared_ptr
    登录后复制

总的来说,我的个人经验是:先从

unique_ptr
登录后复制
开始,它最简单、高效。如果发现需要共享资源,再转向
shared_ptr
登录后复制
。一旦使用了
shared_ptr
登录后复制
,就应该警惕循环引用问题,并适时引入
weak_ptr
登录后复制
来解决。

使用智能指针时常见的陷阱和最佳实践有哪些?

尽管智能指针极大地简化了C++的内存管理,但它们并非万无一失。在我多年的编程实践中,也遇到过一些因为对智能指针理解不深而导致的“坑”。了解这些陷阱并遵循最佳实践,能让你的代码更加健壮。

常见的陷阱:

  1. shared_ptr
    登录后复制
    的循环引用: 这大概是所有
    shared_ptr
    登录后复制
    使用者最容易踩的坑。两个对象通过
    shared_ptr
    登录后复制
    相互引用,导致它们的引用计数永远不会降到零,从而造成内存泄漏。

    // 错误示例:导致循环引用
    struct Node {
        std::shared_ptr<Node> next;
        std::shared_ptr<Node> prev; // 如果这里也是 shared_ptr
        ~Node() { std::cout << "Node 析构" << std::endl; }
    };
    
    void bad_cycle() {
        std::shared_ptr<Node> n1 = std::make_shared<Node>();
        std::shared_ptr<Node> n2 = std::make_shared<Node>();
        n1->next = n2;
        n2->prev = n1; // 形成循环,n1和n2都不会被析构
    } // 离开作用域,Node不会析构
    登录后复制

    解决方案: 使用

    std::weak_ptr
    登录后复制
    打破循环。将其中一个引用改为
    weak_ptr
    登录后复制

    // 正确示例:使用 weak_ptr
    struct NodeFixed {
        std::shared_ptr<NodeFixed> next;
        std::weak_ptr<NodeFixed> prev; // 使用 weak_ptr
        ~NodeFixed() { std::cout << "NodeFixed 析构" << std::endl; }
    };
    
    void good_cycle() {
        std::shared_ptr<NodeFixed> n1 = std::make_shared<NodeFixed>();
        std::shared_ptr<NodeFixed> n2 = std::make_shared<NodeFixed>();
        n1->next = n2;
        n2->prev = n1; // n1的引用计数不会增加
    } // 离开作用域,NodeFixed会被正确析构
    登录后复制
  2. shared_ptr
    登录后复制
    和裸指针的混用: 将一个裸指针多次传递给
    shared_ptr
    登录后复制
    构造函数,或者从一个裸指针创建
    shared_ptr
    登录后复制
    后,又通过另一个裸指针创建新的
    shared_ptr
    登录后复制
    ,会导致同一个对象被多个独立的
    shared_ptr
    登录后复制
    管理,各自维护一套引用计数,最终导致多次释放。

    // 错误示例:裸指针和 shared_ptr 混用导致多次释放
    int* raw_ptr = new int(10);
    std::shared_ptr<int> s_ptr1(raw_ptr); // OK
    // std::shared_ptr<int> s_ptr2(raw_ptr); // 错误!会导致二次释放
    // 正确的做法是 s_ptr2 = s_ptr1;
    登录后复制

    解决方案: 尽量避免从裸指针创建多个

    shared_ptr
    登录后复制
    。如果必须从裸指针创建
    shared_ptr
    登录后复制
    ,确保只做一次,后续都通过复制现有
    shared_ptr
    登录后复制
    来共享所有权。

  3. 使用

    new
    登录后复制
    而不是
    make_unique
    登录后复制
    /
    make_shared
    登录后复制
    虽然
    std::shared_ptr<T> p(new T())
    登录后复制
    std::unique_ptr<T> p(new T())
    登录后复制
    在语法上是合法的,但
    make_unique
    登录后复制
    make_shared
    登录后复制
    是更优的选择。

    // 不推荐:两次内存分配,且可能存在异常安全问题
    // std::shared_ptr<MyClass> ptr = std::shared_ptr<MyClass>(new MyClass());
    登录后复制

    解决方案: 总是优先使用

    std::make_unique
    登录后复制
    std::make_shared
    登录后复制
    。它们有以下优点:

    • 效率更高:
      make_shared
      登录后复制
      只进行一次内存分配,同时为对象和控制块(引用计数等)分配内存,而
      new
      登录后复制
      然后构造
      shared_ptr
      登录后复制
      会进行两次。
      make_unique
      登录后复制
      也通常更高效。
    • 异常安全: 在某些情况下,
      new T()
      登录后复制
      std::shared_ptr<T>(...)
      登录后复制
      之间的函数调用可能导致资源泄漏。
      make_shared
      登录后复制
      make_unique
      登录后复制
      能保证原子性,避免这种风险。
  4. 自定义删除器(deleter)的误用或遗漏: 如果智能指针管理的是非堆内存(如文件句柄、网络套接字等),或者需要特殊的释放逻辑,必须提供自定义删除器。忘记提供或提供错误的删除器会导致资源泄漏或崩溃。

    // 示例:自定义文件删除器
    void closeFile(FILE* fp) {
        if (fp) {
            fclose(fp);
            std::cout << "文件已关闭。" << std::endl;
        }
    }
    // std::unique_ptr<FILE, decltype(&closeFile)> file_ptr(fopen("test.txt", "w"), &closeFile);
    // 如果没有 &closeFile,unique_ptr 会尝试用 delete 关闭文件,导致错误。
    登录后复制

    解决方案: 当管理非标准堆内存或需要特殊清理逻辑的资源时,务必提供正确的自定义删除器。

最佳实践:

  1. 默认使用
    unique_ptr
    登录后复制
    除非你明确需要共享所有权,否则请优先使用
    unique_ptr
    登录后复制
    。它开销最小,语义清晰,且强制了独占所有权,能帮助你更好地设计代码。
  2. 优先使用
    make_unique
    登录后复制
    make_shared
    登录后复制
    避免直接使用
    new
    登录后复制
    来构造智能指针。
  3. 避免裸指针与智能指针混用: 尽量在代码中保持智能指针的“纯洁性”。如果必须从智能指针获取裸指针(通过
    get()
    登录后复制
    ),要非常小心其生命周期,确保裸指针在使用期间智能指针仍然有效。
  4. 理解所有权语义: 清楚地知道你的代码中哪个对象拥有资源,以及所有权是如何转移或共享的。这是正确使用智能指针的基石。
  5. 警惕
    shared_from_this
    登录后复制
    当一个类对象希望通过
    shared_ptr
    登录后复制
    将自身作为
    shared_ptr
    登录后复制
    返回或传递给其他对象时,该类应该继承
    std::enable_shared_from_this<T>
    登录后复制
    ,并通过
    shared_from_this()
    登录后复制
    方法获取
    shared_ptr
    登录后复制
    。直接在成员函数中
    return std::shared_ptr<T>(this)
    登录后复制
    是错误的

以上就是如何在C++中使用智能指针_C++智能指针使用核心指南的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号