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

C++如何避免智能指针导致的内存泄漏

P粉602998670
发布: 2025-09-03 11:09:01
原创
688人浏览过
std::shared_ptr循环引用导致内存泄漏因引用计数无法归零,解决方法是使用std::weak_ptr打破循环;混合使用裸指针可能引发重复释放或悬空指针,应避免用裸指针初始化多个智能指针,并通过get()谨慎传递非所有权访问;对于非内存资源,需通过自定义删除器(如Lambda、函数对象)确保智能指针正确释放资源,从而实现全面的RAII管理。

c++如何避免智能指针导致的内存泄漏

智能指针在C++中是防止内存泄漏的利器,但它们并非万无一失。我们之所以会遇到智能指针导致的“内存泄漏”,往往不是智能指针本身的设计缺陷,而是对其使用场景、所有权语义理解不透彻,或是未能妥善处理一些特殊情况,比如循环引用。说到底,智能指针是工具,工具用不好,自然达不到预期效果。

在C++中,智能指针是用来自动化管理动态分配内存的,它们的核心思想是RAII(Resource Acquisition Is Initialization)。但要彻底避免内存泄漏,我们得深入理解它们的行为模式和潜在陷阱。

一个常见的误区是,认为只要用了智能指针就高枕无忧了。实际上,最典型的“泄漏”场景是

std::shared_ptr
登录后复制
的循环引用。当两个或多个对象通过
std::shared_ptr
登录后复制
相互持有对方的强引用时,它们的引用计数永远不会降到零,导致这些对象及其占用的内存无法被释放。这就像两个人互相抓住对方的手不放,谁也走不了。

另一个问题出在智能指针与裸指针的混合使用。如果你从

std::shared_ptr
登录后复制
中提取出裸指针(通过
get()
登录后复制
方法),然后又试图用这个裸指针去初始化一个新的
std::shared_ptr
登录后复制
,那就会导致同一个内存区域被两个独立的
std::shared_ptr
登录后复制
管理,最终在析构时发生二次释放(double free),这比泄漏更糟糕。或者,将裸指针传递给一个不清楚所有权语义的函数,如果该函数意外地
delete
登录后复制
了它,也会出问题。

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

此外,智能指针默认管理的是

new
登录后复制
分配的内存。如果你的资源不是通过
new
登录后复制
获取的(比如
malloc
登录后复制
出来的内存、文件句柄、互斥锁),那么你需要提供一个自定义的删除器(deleter),否则智能指针在析构时会错误地调用
delete
登录后复制
,导致未定义行为或资源未释放。

避免这些问题的关键在于:明确所有权、警惕循环引用、避免裸指针的滥用,并为非标准资源提供正确的删除策略。

std::shared_ptr
登录后复制
循环引用是如何导致内存泄漏的?又该如何有效解决?

std::shared_ptr
登录后复制
的核心机制是引用计数。每当一个
std::shared_ptr
登录后复制
实例指向一个对象,该对象的引用计数就会增加;当一个
std::shared_ptr
登录后复制
实例被销毁或重新赋值时,引用计数就会减少。只有当引用计数降到零时,对象才会被真正删除。循环引用恰恰破坏了这个机制。

想象一下,我们有两个类

A
登录后复制
B
登录后复制
A
登录后复制
有一个
std::shared_ptr<B>
登录后复制
成员,而
B
登录后复制
也有一个
std::shared_ptr<A>
登录后复制
成员。当
A
登录后复制
的实例
A
登录后复制
B
登录后复制
的实例
B
登录后复制
被创建并互相持有对方的
std::shared_ptr
登录后复制
时:

class B; // 前向声明

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

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() { std::cout << "B destroyed" << std::endl; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();

    a->b_ptr = b; // b的引用计数变为2
    b->a_ptr = a; // a的引用计数变为2

    // 当a和b离开作用域时,它们的引用计数都只会降到1,永远不会到0
    // 导致A和B的对象都无法被销毁,这就是内存泄漏。
    return 0;
}
登录后复制

在这个例子中,

A
登录后复制
B
登录后复制
离开
main
登录后复制
函数作用域时,它们各自的
shared_ptr
登录后复制
会被销毁,引用计数会从2降到1。但因为它们还被对方的
shared_ptr
登录后复制
强引用着,引用计数永远不会降到0,
A
登录后复制
B
登录后复制
的析构函数永远不会被调用,内存也就泄漏了。

解决方案:使用

std::weak_ptr
登录后复制

std::weak_ptr
登录后复制
是一种不增加对象引用计数的智能指针。它“观察”一个
std::shared_ptr
登录后复制
所管理的对象,但不会阻止该对象被销毁。当需要访问对象时,可以通过
std::weak_ptr::lock()
登录后复制
方法尝试获取一个
std::shared_ptr
登录后复制
。如果对象已被销毁,
lock()
登录后复制
会返回一个空的
std::shared_ptr
登录后复制

修改上面的例子:

#include <iostream>
#include <memory>

class B; // 前向声明

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

class B {
public:
    std::weak_ptr<A> a_ptr; // 将强引用改为弱引用
    ~B() { std::cout << "B destroyed" << std::endl; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();

    a->b_ptr = b; // b的引用计数变为2
    b->a_ptr = a; // a的引用计数仍为1 (因为是weak_ptr)

    // 当a离开作用域时,a的引用计数降到0,A对象被销毁。
    // 此时b->a_ptr观察的对象已不存在。
    // 随后b离开作用域时,b的引用计数降到0,B对象被销毁。
    return 0;
}
登录后复制

在这个修正后的版本中,

B
登录后复制
持有
A
登录后复制
std::weak_ptr
登录后复制
。当
main
登录后复制
函数结束,
A
登录后复制
智能指针离开作用域时,
A
登录后复制
对象的引用计数降为0(因为它只被
A
登录后复制
强引用,
b->a_ptr
登录后复制
是弱引用),
A
登录后复制
对象被销毁。随后,
B
登录后复制
智能指针离开作用域,
B
登录后复制
对象的引用计数也降为0,
B
登录后复制
对象被销毁。成功避免了内存泄漏。

std::weak_ptr
登录后复制
通常用于解决父子关系、观察者模式或缓存等场景中的循环引用问题。一般原则是,拥有所有权的一方使用
std::shared_ptr
登录后复制
,而仅需要访问但不影响对象生命周期的一方使用
std::weak_ptr
登录后复制

在C++中,智能指针与裸指针混合使用有哪些潜在风险?如何安全地进行操作?

智能指针和裸指针混合使用是C++中一个常见的陷阱,它可能导致悬空指针、重复释放或未定义行为。核心问题在于所有权语义的模糊。

智谱清言 - 免费全能的AI助手
智谱清言 - 免费全能的AI助手

智谱清言 - 免费全能的AI助手

智谱清言 - 免费全能的AI助手 2
查看详情 智谱清言 - 免费全能的AI助手

潜在风险:

  1. 重复释放 (Double Free): 如果你有一个

    std::shared_ptr
    登录后复制
    正在管理一块内存,然后你通过
    get()
    登录后复制
    获取到裸指针,再用这个裸指针去初始化一个新的
    std::shared_ptr
    登录后复制
    ,那么当这两个
    std::shared_ptr
    登录后复制
    实例都析构时,它们会尝试对同一块内存进行两次
    delete
    登录后复制
    操作,导致程序崩溃。

    int* raw_ptr = new int(10);
    std::shared_ptr<int> sp1(raw_ptr); // sp1管理raw_ptr指向的内存
    // ...
    std::shared_ptr<int> sp2(raw_ptr); // 错误!sp2也试图管理同一块内存
    // 当sp1和sp2析构时,会发生double free
    登录后复制

    正确做法是,如果已经有

    std::shared_ptr
    登录后复制
    管理了该内存,直接复制或移动这个
    std::shared_ptr
    登录后复制

  2. 悬空指针 (Dangling Pointer): 如果你将

    std::unique_ptr
    登录后复制
    std::shared_ptr
    登录后复制
    管理的对象的裸指针传递给某个函数,而该函数在智能指针的生命周期结束前就删除了该裸指针,那么智能指针在析构时会尝试删除已经无效的内存,或者在你后续尝试通过智能指针访问对象时,会访问到已经被释放的内存。

    std::unique_ptr<int> up(new int(5));
    int* raw = up.get(); // 获取裸指针
    // delete raw; // 假设某个函数内部错误地执行了这行
    // ...
    // up离开作用域时,会再次尝试delete raw,导致double free
    登录后复制
  3. this
    登录后复制
    指针问题: 在类的成员函数中,如果你想返回一个指向当前对象的
    std::shared_ptr
    登录后复制
    ,直接
    return std::shared_ptr<MyClass>(this);
    登录后复制
    是错误的。这会导致一个独立的
    std::shared_ptr
    登录后复制
    实例管理
    this
    登录后复制
    指向的内存,与外部可能存在的
    std::shared_ptr
    登录后复制
    形成冲突,最终导致重复释放。

安全操作方法:

  1. 避免从裸指针创建多个智能指针: 一旦内存被智能指针管理,就应该通过智能指针本身来传递所有权或共享所有权。 使用

    std::make_shared
    登录后复制
    std::make_unique
    登录后复制
    是创建智能指针的最佳实践,它们不仅提供了异常安全,还能避免上述裸指针初始化问题。

  2. 传递裸指针用于观察或临时访问: 当需要将智能指针管理的对象传递给接受裸指针的旧API或函数时,使用

    get()
    登录后复制
    方法获取裸指针是允许的。但必须明确,这种传递不涉及所有权转移,接收方不应该删除该指针,也不应该存储该指针以供智能指针生命周期之外使用。

    void legacy_api_process(int* data) {
        // 假设这个API只会使用data,不会删除它
        std::cout << *data << std::endl;
    }
    
    std::shared_ptr<int> sp = std::make_shared<int>(100);
    legacy_api_process(sp.get()); // 安全,只要legacy_api_process不删除data
    登录后复制
  3. this
    登录后复制
    获取
    std::shared_ptr
    登录后复制
    std::enable_shared_from_this
    登录后复制
    如果一个类需要返回一个指向自身对象的
    std::shared_ptr
    登录后复制
    ,它应该继承自
    std::enable_shared_from_this<MyClass>
    登录后复制
    。然后,在成员函数中通过
    shared_from_this()
    登录后复制
    方法来获取一个
    std::shared_ptr
    登录后复制
    。这个方法会安全地创建一个新的
    std::shared_ptr
    登录后复制
    ,并与现有的
    std::shared_ptr
    登录后复制
    共享所有权。

    class MyClass : public std::enable_shared_from_this<MyClass> {
    public:
        std::shared_ptr<MyClass> get_shared_this() {
            return shared_from_this();
        }
    };
    
    int main() {
        std::shared_ptr<MyClass> obj = std::make_shared<MyClass>();
        std::shared_ptr<MyClass> another_obj = obj->get_shared_this(); // 安全
        // obj和another_obj现在共享同一个MyClass对象
        return 0;
    }
    登录后复制
  4. 明确所有权语义: 在使用智能指针时,始终要思考谁拥有资源。

    std::unique_ptr
    登录后复制
    表示独占所有权,
    std::shared_ptr
    登录后复制
    表示共享所有权。当资源的所有权需要转移或共享时,使用智能指针本身的操作(如
    std::move
    登录后复制
    或复制
    std::shared_ptr
    登录后复制
    )。

如何使用自定义删除器管理非内存资源,确保智能指针的全面性?

智能指针的默认行为是使用

delete
登录后复制
运算符来释放其管理的内存。然而,许多资源并非通过
new
登录后复制
分配,而是通过特定的API函数获取和释放的,例如文件句柄 (
fopen
登录后复制
/
fclose
登录后复制
)、互斥锁 (
pthread_mutex_lock
登录后复制
/
pthread_mutex_unlock
登录后复制
)、C风格的内存分配 (
malloc
登录后复制
/
free
登录后复制
) 等。在这种情况下,我们需要为智能指针提供一个“自定义删除器”(Custom Deleter),告诉它在对象生命周期结束时如何正确地释放资源。

自定义删除器可以是普通函数、函数对象(functor)或 Lambda 表达式。

1.

std::unique_ptr
登录后复制
与自定义删除器:

std::unique_ptr
登录后复制
在模板参数中可以指定删除器类型,这使得它非常适合管理独占的非内存资源。

  • 使用 Lambda 表达式作为删除器: 这是最灵活和现代的方式,可以直接在创建

    unique_ptr
    登录后复制
    的地方定义删除逻辑。

    #include <iostream>
    #include <memory>
    #include <cstdio> // For FILE* and fclose
    
    // 假设我们有一个需要特殊关闭函数的文件句柄
    void close_file(FILE* file) {
        if (file) {
            std::cout << "Closing file..." << std::endl;
            fclose(file);
        }
    }
    
    int main() {
        // 使用lambda表达式作为自定义删除器
        std::unique_ptr<FILE, decltype(&close_file)> file_ptr(
            fopen("example.txt", "w"), close_file);
    
        if (file_ptr) {
            fprintf(file_ptr.get(), "Hello from unique_ptr!\n");
        } else {
            std::cerr << "Failed to open file." << std::endl;
        }
        // file_ptr离开作用域时,close_file会被自动调用
        return 0;
    }
    登录后复制

    注意

    decltype(&close_file)
    登录后复制
    用于指定删除器的类型。

  • 使用函数对象(Functor)作为删除器: 当删除逻辑比较复杂,或者需要在多个地方复用时,可以定义一个函数对象。

    struct FileCloser {
        void operator()(FILE* file) const {
            if (file) {
                std::cout << "Closing file via functor..." << std::endl;
                fclose(file);
            }
        }
    };
    
    int main() {
        std::unique_ptr<FILE, FileCloser> file_ptr(fopen("another.txt", "w"));
        if (file_ptr) {
            fprintf(file_ptr.get(), "Hello from functor!\n");
        }
        // file_ptr离开作用域时,FileCloser()会被自动调用
        return 0;
    }
    登录后复制

    这里

    unique_ptr
    登录后复制
    的模板参数直接是
    FileCloser
    登录后复制
    类型,而不是
    decltype(&close_file)
    登录后复制

2.

std::shared_ptr
登录后复制
与自定义删除器:

std::shared_ptr
登录后复制
的自定义删除器不需要在模板参数中指定类型,它作为构造函数的第二个参数传入。这使得
shared_ptr
登录后复制
在管理非内存资源时更加灵活,因为它可以在运行时决定删除器。

  • 使用 Lambda 表达式作为删除器:

    #include <iostream>
    #include <memory>
    #include <mutex> // For std::mutex
    
    int main() {
        std::mutex mtx;
        // 使用lambda表达式作为自定义删除器,管理互斥锁的解锁
        std::shared_ptr<std::mutex> lock_ptr(&mtx, [](std::mutex* p) {
            std::cout << "Unlocking mutex..." << std::endl;
            p->unlock();
        });
    
        lock_ptr->lock(); // 锁定互斥锁
        std::cout << "Mutex is locked." << std::endl;
        // lock_ptr离开作用域时,lambda会被自动调用,解锁互斥锁
        return 0;
    }
    登录后复制
  • 使用普通函数作为删除器:

    void free_c_memory(void* ptr) {
        if (ptr) {
            std::cout << "Freeing C-style memory..." << std::endl;
            free(ptr); // 使用free而不是delete
        }
    }
    
    int main() {
        // 使用malloc分配内存,并用shared_ptr管理
        std::shared_ptr<int> c_array_ptr(
            static_cast<int*>(malloc(10 * sizeof(int))), free_c_memory);
    
        if (c_array_ptr) {
            for (int i = 0; i < 10; ++i) {
                c_array_ptr.get()[i] = i;
            }
            std::cout << "C-style array managed by shared_ptr: " << c_array_ptr.get()[5] << std::endl;
        }
        // c_array_ptr离开作用域时,free_c_memory会被自动调用
        return 0;
    }
    登录后复制

通过自定义删除器,智能指针的能力得到了极大的扩展,不再局限于管理

new/delete
登录后复制
分配的内存。它们成为了一个通用的RAII工具,能够可靠地管理各种类型的资源,从而全面避免资源泄漏。这正是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号