C++shared_ptr对象销毁顺序与内存管理(销毁.顺序.内存管理.对象.shared_ptr...)

wufei123 发布于 2025-09-11 阅读(1)
shared_ptr通过引用计数精确管理对象生命周期,强引用归零时立即销毁对象;其线程安全性体现在控制块的引用计数操作是原子的,但多线程访问同一对象仍需外部同步。

c++shared_ptr对象销毁顺序与内存管理

shared_ptr
在C++中主要通过引用计数(reference counting)机制来管理对象的生命周期。当一个对象的引用计数归零时,
shared_ptr
会自动调用其析构函数并释放内存。这意味着对象的销毁是确定性的,且发生在最后一个
shared_ptr
实例被销毁或重置的那一刻。 解决方案

shared_ptr
是C++11引入的智能指针,旨在解决动态内存管理中常见的资源泄漏和悬空指针问题,尤其是在多所有权场景下。它的核心思想是“共享所有权”:多个
shared_ptr
实例可以共同拥有同一个对象。

每个

shared_ptr
内部都维护着一个控制块(control block),这个控制块通常存储着两个计数器:
  1. 强引用计数(strong count):记录有多少个
    shared_ptr
    实例正在管理这个对象。
  2. 弱引用计数(weak count):记录有多少个
    weak_ptr
    实例正在观察这个对象。

当一个

shared_ptr
被创建、拷贝或赋值时,强引用计数会增加。反之,当一个
shared_ptr
实例超出其作用域、被重置(
reset()
)或被赋新值时,强引用计数会减少。

对象的销毁逻辑是这样的:

  • 当强引用计数降为零时,
    shared_ptr
    会立即调用被管理对象的析构函数,从而销毁对象。
  • 随后,当强引用计数和弱引用计数都降为零时,控制块本身所占用的内存才会被释放。

这种机制确保了只要有任何一个

shared_ptr
还“活着”,被管理的对象就不会被销毁。这在很多场景下都非常方便,比如在容器中存储对象,或者在函数之间传递对象,无需担心谁来负责
delete
。我个人觉得,这种设计极大地简化了复杂系统的内存管理逻辑,让开发者可以更专注于业务逻辑本身,而不是纠结于
new
delete
的配对问题。

然而,

shared_ptr
并非万能。它最大的挑战在于处理循环引用。如果两个对象互相持有对方的
shared_ptr
,它们的强引用计数将永远不会降为零,即使它们已经不再被外部代码访问,这会导致内存泄漏。解决这个问题通常需要引入
weak_ptr
,它提供了一种非拥有性的引用方式。
#include <iostream>
#include <memory>
#include <vector>

class MyObject {
public:
    int id;
    MyObject(int i) : id(i) {
        std::cout << "MyObject " << id << " created." << std::endl;
    }
    ~MyObject() {
        std::cout << "MyObject " << id << " destroyed." << std::endl;
    }
};

void func(std::shared_ptr<MyObject> obj) {
    std::cout << "Inside func, obj " << obj->id << " use_count: " << obj.use_count() << std::endl;
} // obj离开作用域,强引用计数-1

int main() {
    std::cout << "--- Start main ---" << std::endl;
    {
        std::shared_ptr<MyObject> ptr1 = std::make_shared<MyObject>(1); // 强引用计数: 1
        std::cout << "ptr1 created, use_count: " << ptr1.use_count() << std::endl;

        std::shared_ptr<MyObject> ptr2 = ptr1; // 强引用计数: 2
        std::cout << "ptr2 copied from ptr1, use_count: " << ptr1.use_count() << std::endl;

        func(ptr1); // 传参会创建临时shared_ptr,强引用计数: 3 (进入func时), 2 (退出func时)

        std::vector<std::shared_ptr<MyObject>> vec;
        vec.push_back(ptr1); // 强引用计数: 3
        std::cout << "ptr1 added to vector, use_count: " << ptr1.use_count() << std::endl;

        ptr2.reset(); // ptr2重置,不再管理对象,强引用计数: 2
        std::cout << "ptr2 reset, ptr1 use_count: " << ptr1.use_count() << std::endl;

    } // ptr1和vec中的shared_ptr离开作用域,强引用计数最终降为0,对象被销毁
    std::cout << "--- End main ---" << std::endl;
    return 0;
}

运行上述代码,你会清晰地看到

MyObject 1 destroyed.
的输出发生在
--- End main ---
之前,并且是在
ptr1
vec
中的最后一个
shared_ptr
离开作用域之后。这正是
shared_ptr
生命周期管理的体现。
shared_ptr
如何精确控制对象的生命周期?

shared_ptr
对对象生命周期的精确控制,主要得益于其内部的控制块(Control Block)机制。这个控制块是一个独立于被管理对象但与
shared_ptr
实例共享的数据结构。它至少包含两个关键信息:强引用计数(strong reference count)和弱引用计数(weak reference count)。

当一个

shared_ptr
首次被创建(例如通过
std::make_shared
或直接构造)时,一个新的控制块会被创建,并将其强引用计数初始化为1。后续每当有新的
shared_ptr
实例拷贝自现有实例,或通过赋值操作指向同一个对象时,强引用计数就会原子性地递增。反之,当一个
shared_ptr
实例被销毁(例如超出作用域)、被重置(
reset()
方法)或被赋值为另一个对象时,强引用计数就会原子性地递减。

对象的实际销毁,即调用其析构函数并释放内存,发生在强引用计数降至零的那一刻。这一机制确保了:

  1. 资源不被过早释放:只要至少有一个
    shared_ptr
    实例指向该对象,对象就不会被销毁。
  2. 资源及时释放:一旦最后一个
    shared_ptr
    实例不再指向该对象,对象就会立即被销毁,避免了资源长期占用。

这种“即时且延迟”的销毁策略,完美契合了RAII(Resource Acquisition Is Initialization)原则,将资源的生命周期与管理它们的

shared_ptr
对象的生命周期绑定。从我的经验来看,这比手动管理内存要安全得多,尤其是当对象的所有权在多个模块或函数之间传递时。你不再需要去思考“谁来
delete
这个指针?”的问题,因为
shared_ptr
会帮你搞定。 PIA PIA

全面的AI聚合平台,一站式访问所有顶级AI模型

PIA226 查看详情 PIA
#include <iostream>
#include <memory>

class Resource {
public:
    Resource(int id) : id_(id) {
        std::cout << "Resource " << id_ << " acquired." << std::endl;
    }
    ~Resource() {
        std::cout << "Resource " << id_ << " released." << std::endl;
    }
private:
    int id_;
};

void process(std::shared_ptr<Resource> res) {
    std::cout << "Processing Resource " << res->id_ << ". Use count: " << res.use_count() << std::endl;
} // res离开作用域,强引用计数-1

int main() {
    std::shared_ptr<Resource> r1 = std::make_shared<Resource>(101); // 强引用计数: 1
    std::cout << "Main scope: r1 use count: " << r1.use_count() << std::endl;

    {
        std::shared_ptr<Resource> r2 = r1; // 强引用计数: 2
        std::cout << "Inner scope: r1 use count: " << r1.use_count() << std::endl;
        process(r2); // 传参导致临时shared_ptr,强引用计数: 3 (进入), 2 (退出)
    } // r2离开作用域,强引用计数: 1
    std::cout << "Main scope: r1 use count after inner scope: " << r1.use_count() << std::endl;

    // r1离开作用域,强引用计数: 0,Resource 101 被销毁
    return 0;
}

通过观察输出,我们可以清楚地看到

Resource 101 released.
语句在
main
函数即将结束时才打印,这正是因为
r1
是最后一个指向该资源的
shared_ptr
。 循环引用(Circular References)是如何导致
shared_ptr
内存泄漏的,以及
weak_ptr
如何解决?

循环引用是

shared_ptr
在使用中最常见且最隐蔽的陷阱之一,它会导致内存泄漏。简单来说,当两个或多个对象通过
shared_ptr
互相持有对方的引用时,就会形成一个循环。在这种情况下,即使外部已经没有
shared_ptr
指向这个循环中的任何一个对象,它们的强引用计数也永远不会降到零,因为它们内部的
shared_ptr
还在互相引用。结果就是,这些对象及其占用的内存将永远不会被释放。

想象一下一个父子关系:一个

Parent
对象持有其
Child
对象的
shared_ptr
,而
Child
对象又持有其
Parent
对象的
shared_ptr
#include <iostream>
#include <memory>

class Child; // 前向声明

class Parent {
public:
    std::shared_ptr<Child> child;
    int id;
    Parent(int i) : id(i) { std::cout << "Parent " << id << " created." << std::endl; }
    ~Parent() { std::cout << "Parent " << id << " destroyed." << std::endl; }
};

class Child {
public:
    std::shared_ptr<Parent> parent; // 这里是导致循环引用的点
    int id;
    Child(int i) : id(i) { std::cout << "Child " << id << " created." << std::endl; }
    ~Child() { std::cout << "Child " << id << " destroyed." << std::endl; }
};

void create_circular_ref() {
    std::shared_ptr<Parent> p = std::make_shared<Parent>(1);
    std::shared_ptr<Child> c = std::make_shared<Child>(2);

    p->child = c; // Parent持有Child,c的强引用计数变为2
    c->parent = p; // Child持有Parent,p的强引用计数变为2

    std::cout << "Parent use_count: " << p.use_count() << std::endl; // 2
    std::cout << "Child use_count: " << c.use_count() << std::endl;   // 2

} // p和c离开作用域,各自的强引用计数减1,但仍为1,对象不会被销毁

create_circular_ref
函数结束后,
p
c
的强引用计数都变成了1,而不是0。这意味着
Parent 1
Child 2
的析构函数永远不会被调用,导致内存泄漏。

weak_ptr
的解决方案
weak_ptr
正是为了解决
shared_ptr
的循环引用问题而设计的。它是一种“弱引用”或“非拥有性引用”。
weak_ptr
可以指向一个由
shared_ptr
管理的对象,但它本身不增加对象的强引用计数。这意味着
weak_ptr
不会阻止被管理对象的销毁。

当需要访问

weak_ptr
所指向的对象时,必须先通过其
lock()
方法尝试获取一个
shared_ptr
。如果对象仍然存在(即强引用计数大于0),
lock()
会返回一个有效的
shared_ptr
;否则,它会返回一个空的
shared_ptr

使用

weak_ptr
来打破循环引用的策略是:在循环中的一侧(通常是“子”或“从属”的一方)使用
weak_ptr
来引用另一方(“父”或“主”的一方)。
#include <iostream>
#include <memory>

class Child_Fixed; // 前向声明

class Parent_Fixed {
public:
    std::shared_ptr<Child_Fixed> child;
    int id;
    Parent_Fixed(int i) : id(i) { std::cout << "Parent_Fixed " << id << " created." << std::endl; }
    ~Parent_Fixed() { std::cout << "Parent_Fixed " << id << " destroyed." << std::endl; }
};

class Child_Fixed {
public:
    std::weak_ptr<Parent_Fixed> parent; // 使用weak_ptr打破循环
    int id;
    Child_Fixed(int i) : id(i) { std::cout << "Child_Fixed " << id << " created." << std::endl; }
    ~Child_Fixed() { std::cout << "Child_Fixed " << id << " destroyed." << std::endl; }

    void access_parent() {
        if (auto p = parent.lock()) { // 尝试获取shared_ptr
            std::cout << "Child_Fixed " << id << " accessing Parent_Fixed " << p->id << std::endl;
        } else {
            std::cout << "Child_Fixed " << id << ": Parent_Fixed no longer exists." << std::endl;
        }
    }
};

void create_fixed_ref() {
    std::shared_ptr<Parent_Fixed> p = std::make_shared<Parent_Fixed>(1);
    std::shared_ptr<Child_Fixed> c = std::make_shared<Child_Fixed>(2);

    p->child = c; // Parent持有Child,c的强引用计数变为2
    c->parent = p; // Child弱引用Parent,p的强引用计数仍为1

    std::cout << "Parent_Fixed use_count: " << p.use_count() << std::endl; // 1
    std::cout << "Child_Fixed use_count: " << c.use_count() << std::endl;   // 2

    c->access_parent(); // 此时Parent_Fixed仍然存在

} // p和c离开作用域,强引用计数归零,对象被销毁

现在,当

create_fixed_ref
函数结束时,
p
的强引用计数会降为0(因为
c->parent
weak_ptr
,不增加计数),
Parent_Fixed 1
会被销毁。接着,
p
被销毁导致
p->child
(一个
shared_ptr<Child_Fixed>
)也被销毁,
c
的强引用计数降为1。最后,
c
本身离开作用域,强引用计数降为0,
Child_Fixed 2
也被销毁。这样,内存泄漏问题就解决了。在我看来,理解
weak_ptr
的“观察者”角色,而不是“拥有者”角色,是正确使用它的关键。
shared_ptr
的自定义删除器(Custom Deleter)有哪些应用场景,以及如何影响对象销毁?

shared_ptr
的自定义删除器是一个非常强大的特性,它允许你指定当对象强引用计数归零时,
shared_ptr
应该如何释放资源,而不是简单地调用
delete
操作符。这极大地扩展了
shared_ptr
能够管理资源的类型,使其不仅仅局限于
new
分配的内存。

应用场景:

  1. 管理C风格数组:
    std::shared_ptr<T>
    默认使用
    delete obj;
    来释放资源,但这对于
    new T[size];
    分配的数组是不正确的,应该使用
    delete[] obj;
    。自定义删除器可以解决这个问题。
  2. 管理文件句柄:例如
    FILE*
    ,需要用
    fclose()
    来关闭。
  3. 管理网络套接字、数据库连接等:这些资源通常有特定的关闭函数(如
    closesocket()
    ,
    sqlite3_close()
    )。
  4. 管理第三方库分配的内存:如果某个库提供了自己的内存分配和释放函数对(如
    lib_malloc()
    ,
    lib_free()
    ),你可以通过自定义删除器来确保使用正确的释放函数。
  5. 资源池管理:当对象被“销毁”时,你可能不想真正释放它,而是将其归还到资源池中,以供后续重用。
  6. 日志记录或清理操作:在资源被释放前执行一些额外的清理或日志记录。

如何影响对象销毁: 当

shared_ptr
的强引用计数降为零时,它会调用你提供的自定义删除器来释放资源,而不是默认的
delete
操作符。这个删除器会作为控制块的一部分被存储起来。这意味着,即使你将
shared_ptr
赋值给另一个类型,只要它们共享同一个控制块,它们就会共享同一个删除器。
#include <iostream>
#include <memory>
#include <cstdio> // For FILE operations

// 1. 管理C风格数组
void array_deleter(int* p) {
    std::cout << "Custom array deleter called for " << p << std::endl;
    delete[] p;
}

// 2. 管理文件句柄
void file_closer(FILE* f) {
    if (f) {
        std::cout << "Custom file closer called for " << f << std::endl;
        fclose(f);
    }
}

int main() {
    std::cout << "--- Custom Deleter Examples ---" << std::endl;

    // 示例1:管理C风格数组
    std::shared_ptr<int> intArray(new int[5], array_deleter);
    for (int i = 0; i < 5; ++i) {
        intArray.get()[i] = i * 10;
    }
    std::cout << "intArray use_count: " << intArray.use_count() << std::endl;
    // 当intArray离开作用域时,array_deleter会被调用

    // 示例2:管理文件句柄
    FILE* file = fopen("test.txt", "w");
    if (file) {
        std::shared_ptr<FILE> filePtr(file, file_closer);
        fprintf(filePtr.get(), "Hello from shared_ptr!\n");
        std::cout << "filePtr use_count: " << filePtr.use_count() << std::endl;
    } else {
        std::cerr << "Failed to open file." << std::endl;
    }
    // 当filePtr离开作用域时,file_closer会被调用

    // 示例3:使用Lambda表达式作为删除器 (更常见和简洁)
    auto customDeleter = [](std::string* s) {
        std::cout << "Lambda deleter called for string: " << *s << std::endl;
        delete s;
    };
    std::shared_ptr<std::string> myString(new std::string("Hello Lambda"), customDeleter);
    std::cout << "myString use_count: " << myString.use_count() << std::endl;

    std::cout << "--- End Custom Deleter Examples ---" << std::endl;
    return 0;
}

在我看来,自定义删除器是

shared_ptr
能够成为通用资源管理工具的关键。它使得
shared_ptr
不仅仅是一个内存管理工具,更是一个RAII容器,能够优雅地管理各种非内存资源。这在编写与C库交互的代码,或者处理系统级资源时,特别有用。它避免了手动调用
close()
free()
等函数可能带来的遗漏和错误,将资源管理责任完美地封装在智能指针中。 为什么说
shared_ptr
是线程安全的,但在多线程环境中

以上就是C++shared_ptr对象销毁顺序与内存管理的详细内容,更多请关注知识资源分享宝库其它相关文章!

相关标签: access 工具 ai c++ ios 作用域 为什么 red Resource count 封装 析构函数 fclose 循环 指针 数据结构 线程 多线程 空指针 delete 对象 作用域 数据库 大家都在看: C++如何检查文件存在 access函数替代方案 使用vcpkg为C++项目管理依赖库的具体步骤是什么 CLion IDE中配置C++工具链和CMake环境的指南 C++制作温度转换小工具方法 C++环境搭建需要安装哪些必要工具

标签:  销毁 顺序 内存管理 

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。