std::shared_ptr通过引用计数实现共享所有权的自动内存管理,避免内存泄漏和悬空指针;推荐使用std::make_shared创建,注意循环引用等陷阱。

std::shared_ptr在C++中提供了一种智能、自动管理动态内存的方式,它允许我们以共享所有权(shared ownership)的模式来管理堆上的对象。简单来说,它就像一个智能的引用计数器,当指向同一对象的最后一个shared_ptr被销毁时,它会自动释放所管理的对象内存,有效避免了内存泄漏和悬空指针的问题。
使用std::shared_ptr,最直接的便是创建并让它管理一个对象。通常,我们推荐使用std::make_shared来创建shared_ptr,这不仅效率更高(单次内存分配),也能避免一些潜在的内存泄漏问题。
#include <iostream>
#include <memory> // 包含 shared_ptr
class MyClass {
public:
MyClass() { std::cout << "MyClass 构造\n"; }
~MyClass() { std::cout << "MyClass 析构\n"; }
void doSomething() { std::cout << "MyClass 正在工作...\n"; }
};
int main() {
// 推荐方式:使用 std::make_shared
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
ptr1->doSomething();
std::cout << "ptr1 的引用计数: " << ptr1.use_count() << std::endl; // 通常是1
// 复制 shared_ptr,共享所有权
std::shared_ptr<MyClass> ptr2 = ptr1;
std::cout << "ptr1 的引用计数: " << ptr1.use_count() << std::endl; // 此时是2
std::cout << "ptr2 的引用计数: " << ptr2.use_count() << std::endl; // 此时是2
// 另一个 shared_ptr 也指向同一个对象
{
std::shared_ptr<MyClass> ptr3(ptr1); // 或 std::shared_ptr<MyClass> ptr3 = ptr1;
std::cout << "ptr1 的引用计数: " << ptr1.use_count() << std::endl; // 此时是3
} // ptr3 在这里离开作用域,引用计数减1
std::cout << "ptr1 的引用计数 (ptr3 离开作用域后): " << ptr1.use_count() << std::endl; // 此时是2
// 可以通过 get() 获取原始指针,但要小心使用,不要手动删除
MyClass* rawPtr = ptr1.get();
if (rawPtr) {
rawPtr->doSomething();
}
// 重置 shared_ptr,使其不再管理当前对象
ptr1.reset(); // ptr1 现在为空,引用计数减1
std::cout << "ptr1 重置后,ptr2 的引用计数: " << ptr2.use_count() << std::endl; // 此时是1
// 当最后一个 shared_ptr (ptr2) 离开作用域时,MyClass 对象将被析构
return 0;
}这段代码展示了shared_ptr的核心机制:创建、复制、引用计数的变化以及最终的自动释放。通过->和*运算符可以像使用普通指针一样访问其管理的对象。use_count()方法则可以查看当前有多少个shared_ptr实例共享同一个对象。
说实话,刚接触C++时,指针这东西确实让人又爱又恨。它强大,能直接操作内存,但同时也带来了无数的陷阱:内存泄漏、野指针、重复释放等等。std::shared_ptr的出现,在我看来,就是为了解决这些“人祸”而生。它和普通指针最大的不同,在于它拥有所有权,并且这种所有权是共享的。
立即学习“C++免费学习笔记(深入)”;
普通指针,说白了,就是个地址,它不关心它指向的内存是谁分配的,谁应该释放。你手动new一个对象,就得手动delete它,否则就是泄漏。如果你不小心delete了两次,程序就崩溃了。更别提多个指针指向同一块内存,你不知道什么时候这块内存被其他指针释放了,你的指针就成了“悬空指针”,访问它就是未定义行为。
而std::shared_ptr则不然,它内部有一个引用计数器。当你创建一个shared_ptr并让它指向一个对象时,引用计数器就初始化为1。每当你复制这个shared_ptr(比如std::shared_ptr<T> p2 = p1;),引用计数器就加1。当一个shared_ptr离开作用域或被重置时,引用计数器就减1。只有当引用计数器归零时,也就是没有shared_ptr再指向这个对象时,它才会自动调用对象的析构函数并释放内存。这简直是“懒人福音”,把内存管理的重担从程序员肩上卸了下来。
选择std::shared_ptr的理由很直接:
shared_ptr是理想选择。比如一个资源被多个模块使用,只要有一个模块还在用,资源就不会被释放。shared_ptr管理的对象被释放后,所有指向它的shared_ptr都会变成空(虽然它们本身并不会自动感知,但use_count为0后,访问行为会更可控,至少不会访问到已释放的内存)。更重要的是,它保证了对象在还有“主人”时不会被提前释放。// 示例:普通指针可能带来的问题
void process_raw_ptr(int* data) {
if (data) {
// ... 使用 data
delete data; // 假设这里要释放
}
}
int main_raw() {
int* my_data = new int(100);
process_raw_ptr(my_data);
// 此时 my_data 已经是一个悬空指针,再次访问或 delete 会出问题
// std::cout << *my_data << std::endl; // 未定义行为
// delete my_data; // 双重释放
// 使用 shared_ptr 则不会有这些烦恼
std::shared_ptr<int> shared_data = std::make_shared<int>(100);
std::shared_ptr<int> shared_data_copy = shared_data;
// 无论哪个 shared_ptr 离开作用域,只要还有其他 shared_ptr 引用,内存就不会被释放
// 只有当 shared_data 和 shared_data_copy 都失效后,int(100) 才会析构
return 0;
}这种由shared_ptr提供的“智能”和“安全”,在现代C++编程中是极其宝贵的。
虽然shared_ptr极大地简化了内存管理,但它也不是万能的,甚至可以说,如果用得不好,它也会引入一些新的复杂性。
常见陷阱:
循环引用 (Circular References):这是shared_ptr最臭名昭著的陷阱。当两个或多个对象相互持有对方的shared_ptr时,它们会形成一个引用环。即使外部没有其他shared_ptr指向它们,它们的引用计数也永远不会降到零,导致这组对象永远不会被释放,造成内存泄漏。
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A 析构\n"; }
};
class B {
public:
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B 析构\n"; }
};
void test_circular_ref() {
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,导致 A 和 B 都不会被析构
} // A 和 B 都没有析构,内存泄漏!解决循环引用通常需要引入std::weak_ptr。weak_ptr是一种不增加引用计数的智能指针,它“观察”shared_ptr管理的对象,但不拥有它。
从裸指针多次构造shared_ptr:如果你有一个裸指针,然后用它创建了多个独立的shared_ptr,而不是通过复制一个已存在的shared_ptr,那么每个shared_ptr都会有自己的引用计数器,并认为自己是该对象的唯一所有者。当它们各自的引用计数归零时,就会尝试多次释放同一块内存,导致双重释放错误。
int* raw_ptr = new int(42); std::shared_ptr<int> p1(raw_ptr); // p1 认为自己是所有者 // std::shared_ptr<int> p2(raw_ptr); // 错误!p2 也会认为自己是所有者,导致双重释放 // 正确的做法是: // std::shared_ptr<int> p2 = p1;
从this指针创建shared_ptr:在类的成员函数中,如果你需要返回当前对象的shared_ptr,直接使用std::shared_ptr<MyClass>(this)也是错误的,因为它同样会创建一个独立的shared_ptr,导致与外部已有的shared_ptr冲突。正确的方法是让类继承std::enable_shared_from_this<MyClass>,并通过shared_from_this()方法获取。
管理非堆内存或数组:shared_ptr默认调用delete来释放内存。如果它管理的是一个数组(new T[N]),则需要提供一个自定义的deleter来调用delete[]。对于非堆内存(如栈上的对象),shared_ptr也会尝试delete,导致未定义行为。
最佳实践:
优先使用std::make_shared:这是最重要的一条。std::make_shared在一个内存块中同时分配对象和其控制块(包含引用计数等信息),减少了一次内存分配,提高了效率,并且避免了在构造对象失败时,控制块仍然被分配的潜在内存泄漏。
// 推荐 std::shared_ptr<MyClass> p = std::make_shared<MyClass>(); // 不推荐,虽然功能上没问题,但效率较低,且可能出现异常安全问题 // std::shared_ptr<MyClass> p(new MyClass());
使用std::weak_ptr解决循环引用:当两个对象需要相互引用,但又不希望形成强所有权循环时,让其中一方持有weak_ptr。
class B_fixed;
class A_fixed {
public:
std::shared_ptr<B_fixed> b_ptr;
~A_fixed() { std::cout << "A_fixed 析构\n"; }
};
class B_fixed {
public:
std::weak_ptr<A_fixed> a_ptr; // 使用 weak_ptr
~B_fixed() { std::cout << "B_fixed 析构\n"; }
};
void test_no_circular_ref() {
std::shared_ptr<A_fixed> a = std::make_shared<A_fixed>();
std::shared_ptr<B_fixed> b = std::make_shared<B_fixed>();
a->b_ptr = b;
b->a_ptr = a; // 这里不会增加 a 的引用计数
} // a 和 b 都会正常析构理解所有权语义:shared_ptr意味着共享所有权。如果一个对象应该只有一个所有者,或者其生命周期由其创建者严格控制,那么std::unique_ptr可能是更好的选择。
自定义deleter:如果shared_ptr需要管理非标准方式分配的内存(如malloc、new[])或者需要执行特殊的清理操作,可以提供一个自定义的deleter。
// 管理数组
std::shared_ptr<int[]> arr_ptr(new int[10], std::default_delete<int[]>());
// 或使用 lambda 作为 deleter
std::shared_ptr<FILE> file_ptr(fopen("test.txt", "w"), [](FILE* f) {
if (f) { fclose(f); std::cout << "文件关闭\n"; }
});避免在shared_ptr管理的对象内部持有其自身的shared_ptr:这通常会导致循环引用或不必要的复杂性。如果需要,请使用std::enable_shared_from_this。
std::shared_ptr在实际项目中有着非常广泛的应用,尤其是在需要对象生命周期管理复杂、多模块共享资源、或者难以明确单一所有者的场景。
应用场景:
shared_ptr。这样,工厂本身不需要关心对象的生命周期,调用者可以安全地使用,并在不再需要时自动释放。// 假设有一个接口 Product // std::shared_ptr<Product> createProduct(ProductType type);
shared_ptr可以确保只要有任何一个消费者还在使用某个数据项,该数据项就不会被从缓存中移除或销毁。当所有消费者都释放了对它的shared_ptr时,缓存系统可以安全地将其清除。shared_ptr可以管理这些资源的生命周期,确保资源在所有使用它的线程都结束后才被释放。当然,shared_ptr本身是线程安全的(引用计数的增减是原子操作),但它所管理的对象的数据访问仍需要额外的同步机制(如互斥锁)。shared_ptr在这里可以有效地管理这些资源的生命周期,避免重复加载和提前释放。shared_ptr的形式注册到事件源。这样,只要有观察者存在,事件源就可以安全地向它们发送通知。性能考量:
shared_ptr的便利性并非没有代价,它相比于裸指针和unique_ptr,会引入一些性能开销:
shared_ptr不仅仅是一个指针。它需要一个额外的“控制块”来存储引用计数(强引用和弱引用)、自定义deleter等信息。这个控制块通常与对象本身分开分配(除非使用make_shared),因此每个shared_ptr实例会占用更多的内存。shared_ptr的复制、赋值、销毁都会涉及到引用计数的原子增减操作。原子操作虽然比非原子操作慢,但在多线程环境下是必要的。shared_ptr的构造和析构比裸指针或unique_ptr更复杂,因为它需要管理控制块和引用计数。make_shared的优势:如前所述,std::make_shared能够将对象和控制块分配在同一块内存上,这不仅减少了内存碎片,也提升了缓存局部性,从而在一定程度上缓解了内存开销和部分运行时开销。何时选择shared_ptr,何时选择unique_ptr?
这是一个很实际的问题。我的经验是,如果一个对象只有一个明确的、排他的所有者,并且所有权可以转移,那么std::unique_ptr是首选。它零运行时开销,内存开销也最小(只比裸指针多一个指针大小)。它强调“独占所有权”,语义更清晰。
只有当一个对象确实需要被多个部分共享,并且其生命周期难以由单一所有者决定时,才考虑使用std::shared_ptr。比如上面提到的缓存、多线程共享资源等场景。
总结来说,shared_ptr是一个强大的工具,它在C++中提供了自动的内存管理和共享所有权语义。理解其工作原理、优缺点以及潜在陷阱,并结合实际需求选择合适的智能指针,是编写高效、健壮现代C++代码的关键。
以上就是如何在C++中使用std::shared_ptr_C++共享指针shared_ptr使用指南的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号