智能指针是C++11引入的新特性,用于方便内存管理
智能指针是一种自动管理动态分配(使用new
操作符)的内存的机制,是对裸指针的封装,初衷是让程序员无需手动释放内存,来避免内存泄漏。它们在对象不再使用时自动释放内存,从而帮助防止内存泄漏。C++标准库提供了几种智能指针,其中最常见的是unique_ptr
、shared_ptr
和weak_ptr
。
auto_ptr
auto_ptr
是C++早期标准库中的智能指针,它曾经用于自动管理动态分配的内存。然而,auto_ptr
存在一些缺点,导致它在C++11中被弃用,并在C++17中被移除。以下是auto_ptr
的一些主要缺点:
-
自动转移所有权:
auto_ptr
的一个核心特性是它在被复制时会自动转移所有权,这意味着复制一个auto_ptr
会导致原始的auto_ptr
变为空。这违反了大多数程序员对复制操作的预期,并且很容易导致错误。 -
不安全的复制行为:由于
auto_ptr
在复制时转移所有权,这使得通过值传递auto_ptr
参数给函数变得不安全,因为函数内部的复制操作会导致调用者手中的auto_ptr
失效。 -
缺乏移动语义:在C++11之前,C++没有引入移动语义的概念,
auto_ptr
也没有提供移动构造函数和移动赋值操作符。这意味着即使在C++11之前,auto_ptr
也无法充分利用资源的移动,而不是复制。 -
没有标准保证的异常安全性:
auto_ptr
的复制操作可能抛出异常(尽管标准库实现通常避免了这一点),但它没有提供强异常安全性保证,这意味着在复制过程中可能会发生异常,导致资源泄漏。 -
不支持自定义删除器:
auto_ptr
只支持使用delete
作为删除器,不支持自定义删除器。这限制了它的灵活性,特别是在需要使用特定删除逻辑的场景中。 -
与其他智能指针的兼容性问题:
auto_ptr
与其他智能指针类型(如shared_ptr
)之间没有良好的兼容性,这使得在混合使用不同类型智能指针的代码中管理资源变得复杂。
unique_ptr
unique_ptr
代表一个拥有其管理对象的唯一所有权的智能指针。这意味着同一时间只能有一个unique_ptr
指向一个给定的对象。当unique_ptr
被销毁时,它会自动删除其拥有的对象。
实现原理:
unique_ptr
通常包含两个数据成员:一个指向管理对象的指针,和一个指向删除器的指针(默认是delete
操作符)。- 当
unique_ptr
被销毁时,如果它还拥有一个对象,那么它的析构函数会被调用,进而调用删除器来释放内存。 unique_ptr
不支持复制语义,但支持移动语义,这意味着你可以将所有权从一个unique_ptr
转移到另一个,但不可以复制。
shared_ptr
shared_ptr
允许多个智能指针实例共享对同一个对象的所有权。它使用引用计数来跟踪有多少个shared_ptr
实例共享同一个对象。
实现原理:
shared_ptr
包含一个控制块,其中存储了指向管理对象的指针、一个引用计数器以及一个指向删除器的指针。- 当一个
shared_ptr
被创建或复制时,引用计数会增加。 - 当一个
shared_ptr
被销毁或被重新赋值时,引用计数会减少。如果引用计数达到零,控制块会调用删除器来释放对象。 shared_ptr
支持复制和移动语义。
weak_ptr
weak_ptr
是一种不控制对象生命周期的智能指针,它被设计用来解决shared_ptr
可能引起的循环引用问题。
实现原理:
weak_ptr
包含一个指向shared_ptr
控制块的指针,但不会增加引用计数。- 当
weak_ptr
尝试访问其管理的对象时,它会检查对应的shared_ptr
的引用计数是否为零。如果是,表示对象已经被销毁,访问将失败。 weak_ptr
通常用于打破shared_ptr
之间的循环引用,或者在不需要拥有对象时观察对象。
智能指针的实现通常依赖于模板和操作符重载,以提供类似于普通指针的行为,同时自动管理内存。使用智能指针可以减少内存泄漏的风险,但它们也有性能开销,因此在使用时需要权衡。
在多线程环境下,智能指针的线程安全性主要取决于它们如何管理引用计数。对于shared_ptr
和weak_ptr
,引用计数的修改是原子操作,这意味着它们在多线程环境下是线程安全的。
shared_ptr
shared_ptr
的引用计数是线程安全的,因为它使用原子操作来增加和减少计数。这意味着即使多个线程同时修改引用计数,每个线程的修改也会被正确地应用,不会出现竞态条件。
- 创建和复制:当
shared_ptr
被创建或复制时,引用计数的增加是原子操作,确保了多个线程可以安全地共享同一个对象。 - 销毁和重新赋值:当
shared_ptr
被销毁或重新赋值时,引用计数的减少也是原子操作。如果引用计数减少到零,对象会被安全地删除,即使有多个线程同时执行这一操作。
weak_ptr
weak_ptr
本身不修改引用计数,它只是观察shared_ptr
的引用计数。因此,weak_ptr
的创建和销毁是线程安全的,因为它们不涉及修改引用计数。
- 访问对象:当
weak_ptr
尝试访问其管理的对象时,它会检查对应的shared_ptr
的引用计数是否为零。这个检查是安全的,因为引用计数的读取是原子操作。
unique_ptr
unique_ptr
通常不涉及多线程环境,因为它代表对对象的唯一所有权。然而,如果需要在多线程环境中安全地传递unique_ptr
,可以使用std::move
来转移所有权,这是线程安全的。
注意事项
虽然shared_ptr
和weak_ptr
在引用计数操作上是线程安全的,但是在多线程环境中使用智能指针时,仍然需要注意以下几点:
- 避免循环引用:即使
shared_ptr
是线程安全的,循环引用也会导致引用计数永远不会达到零,从而造成内存泄漏。使用weak_ptr
可以打破循环引用。 - 避免数据竞争:虽然引用计数是线程安全的,但是智能指针指向的对象本身并不是线程安全的。如果多个线程需要访问同一个对象,需要确保适当的同步机制,如互斥锁。
- 性能考虑:虽然原子操作通常很快,但在高竞争环境下,频繁的原子操作可能会成为性能瓶颈。在这种情况下,可能需要考虑其他同步机制或设计模式来减少竞争。
循环引用
在两个或多个对象相互引用,或者一些复杂的数据结构,如图、双向链表中,存在多个引用路径等情况下,可能会存在循环引用问题,导致资源无法被释放掉,这个时候就需要使用weak_ptr来打破循环引用。因为使用weak_ptr
指向某一个资源时,它不会增加这个资源的引用计数。
#include <iostream>
#include <memory>
// MyClass就是我们需要调用的资源
class MyClass{
public:
std:shared_ptr<MyClass> other;
// 正确使用如下
// std:weak_ptr<MyClass> other;
MyClass(){
std:cout << "MyClass constructor called" << std::endl;
}
~MyClass(){
std::cout << "MyClass destructor called" << std:endl;
}
};
int main(){
// 指向了new出来的资源MyClass1,引用计数加1
std::shared_ptr<MyClass> ptr1(new MyClass);
// 指向了new出来的资源MyClass2,引用计数加1
std::shared_ptr<MyClass> ptr2(new MyClass);
// 如果没有下列函数,当main函数结束时,ptr1和ptr2的析构函数就会被调用,从而释放资源
// MyClass1里面的shared_ptr other指向了MyClass2的资源,因此资源MyClass2引用计数加1
ptr1->other = ptr2;
ptr2->other = ptr1;
// 语句结束时,资源MyClass1和资源MyClass2的引用计数都为2
return 0;
// return 后两个资源的引用计数都变为1,无法释放