shared_ptr在C++中主要通过引用计数(reference counting)机制来管理对象的生命周期。当一个对象的引用计数归零时,
shared_ptr会自动调用其析构函数并释放内存。这意味着对象的销毁是确定性的,且发生在最后一个
shared_ptr实例被销毁或重置的那一刻。 解决方案
shared_ptr是C++11引入的智能指针,旨在解决动态内存管理中常见的资源泄漏和悬空指针问题,尤其是在多所有权场景下。它的核心思想是“共享所有权”:多个
shared_ptr实例可以共同拥有同一个对象。
每个
shared_ptr内部都维护着一个控制块(control block),这个控制块通常存储着两个计数器:
-
强引用计数(strong count):记录有多少个
shared_ptr
实例正在管理这个对象。 -
弱引用计数(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()方法)或被赋值为另一个对象时,强引用计数就会原子性地递减。
对象的实际销毁,即调用其析构函数并释放内存,发生在强引用计数降至零的那一刻。这一机制确保了:
-
资源不被过早释放:只要至少有一个
shared_ptr
实例指向该对象,对象就不会被销毁。 -
资源及时释放:一旦最后一个
shared_ptr
实例不再指向该对象,对象就会立即被销毁,避免了资源长期占用。
这种“即时且延迟”的销毁策略,完美契合了RAII(Resource Acquisition Is Initialization)原则,将资源的生命周期与管理它们的
shared_ptr对象的生命周期绑定。从我的经验来看,这比手动管理内存要安全得多,尤其是当对象的所有权在多个模块或函数之间传递时。你不再需要去思考“谁来
delete这个指针?”的问题,因为
shared_ptr会帮你搞定。

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


#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分配的内存。
应用场景:
-
管理C风格数组:
std::shared_ptr<T>
默认使用delete obj;
来释放资源,但这对于new T[size];
分配的数组是不正确的,应该使用delete[] obj;
。自定义删除器可以解决这个问题。 -
管理文件句柄:例如
FILE*
,需要用fclose()
来关闭。 -
管理网络套接字、数据库连接等:这些资源通常有特定的关闭函数(如
closesocket()
,sqlite3_close()
)。 -
管理第三方库分配的内存:如果某个库提供了自己的内存分配和释放函数对(如
lib_malloc()
,lib_free()
),你可以通过自定义删除器来确保使用正确的释放函数。 - 资源池管理:当对象被“销毁”时,你可能不想真正释放它,而是将其归还到资源池中,以供后续重用。
- 日志记录或清理操作:在资源被释放前执行一些额外的清理或日志记录。
如何影响对象销毁: 当
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++环境搭建需要安装哪些必要工具
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。