自学内容网 自学内容网

C++ 智能指针

智能指针是C++11引入的新特性,用于方便内存管理

智能指针是一种自动管理动态分配(使用new操作符)的内存的机制,是对裸指针的封装,初衷是让程序员无需手动释放内存,来避免内存泄漏。它们在对象不再使用时自动释放内存,从而帮助防止内存泄漏。C++标准库提供了几种智能指针,其中最常见的是unique_ptrshared_ptrweak_ptr

auto_ptr

auto_ptr是C++早期标准库中的智能指针,它曾经用于自动管理动态分配的内存。然而,auto_ptr存在一些缺点,导致它在C++11中被弃用,并在C++17中被移除。以下是auto_ptr的一些主要缺点:

  1. 自动转移所有权auto_ptr的一个核心特性是它在被复制时会自动转移所有权,这意味着复制一个auto_ptr会导致原始的auto_ptr变为空。这违反了大多数程序员对复制操作的预期,并且很容易导致错误。

  2. 不安全的复制行为:由于auto_ptr在复制时转移所有权,这使得通过值传递auto_ptr参数给函数变得不安全,因为函数内部的复制操作会导致调用者手中的auto_ptr失效。

  3. 缺乏移动语义:在C++11之前,C++没有引入移动语义的概念,auto_ptr也没有提供移动构造函数和移动赋值操作符。这意味着即使在C++11之前,auto_ptr也无法充分利用资源的移动,而不是复制。

  4. 没有标准保证的异常安全性auto_ptr的复制操作可能抛出异常(尽管标准库实现通常避免了这一点),但它没有提供强异常安全性保证,这意味着在复制过程中可能会发生异常,导致资源泄漏。

  5. 不支持自定义删除器auto_ptr只支持使用delete作为删除器,不支持自定义删除器。这限制了它的灵活性,特别是在需要使用特定删除逻辑的场景中。

  6. 与其他智能指针的兼容性问题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_ptrweak_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_ptrweak_ptr在引用计数操作上是线程安全的,但是在多线程环境中使用智能指针时,仍然需要注意以下几点:

  1. 避免循环引用:即使shared_ptr是线程安全的,循环引用也会导致引用计数永远不会达到零,从而造成内存泄漏。使用weak_ptr可以打破循环引用。
  2. 避免数据竞争:虽然引用计数是线程安全的,但是智能指针指向的对象本身并不是线程安全的。如果多个线程需要访问同一个对象,需要确保适当的同步机制,如互斥锁。
  3. 性能考虑:虽然原子操作通常很快,但在高竞争环境下,频繁的原子操作可能会成为性能瓶颈。在这种情况下,可能需要考虑其他同步机制或设计模式来减少竞争。

循环引用

在两个或多个对象相互引用,或者一些复杂的数据结构,如图、双向链表中,存在多个引用路径等情况下,可能会存在循环引用问题,导致资源无法被释放掉,这个时候就需要使用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,无法释放

原文地址:https://blog.csdn.net/weixin_44601183/article/details/143013199

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!