c++11新特性——智能指针详解

简介: c++11新特性——智能指针详解

智能指针:

一、解决了什么问题
  1. 内存泄漏:在未使用智能指针时,我们在堆上malloc申请一段内存或者new一个对象,如果忘记释放就会造成内存泄漏;
  2. 指针共享所有权的传递和释放,比如:多线程同时使用同一个对象时的析构问题。
  3. 使用普通指针,容易造成内存泄露(忘记释放)、二次释放、程序发生异常时内存泄露等问题等。
二、C++11 智能指针

std::auto_ptr : 已被c++11废弃

std::unique_ptr :独占资源所有权的指针。

std::shared_ptr :共享资源所有权的指针。

std::weak_ptr :共享资源的观察者,需要和 std::shared_ptr 一起使用,不影响资源的生命周期。

三、线程安全问题

对于shared_ptr来说,引用计数是线程安全的,但是数据是线程不安全的,需要内部对数据进行加锁。

四、指针指针使用范例
4.1 unique_ptr

当我们想独占资源的所有权时,可以使用std::unique_ptr对资源进行管理,离开该std::unique_ptr对象的作用域时,会自动释放资源。这是很基本的RAII思想(RAII,Resource Acquisition Is Initialization,由c++之父Bjarne Stroustrup提出,即:资源获取即初始化,他说:使用局部对象来管理资源的技术称之为“资源获取即初始化”)。

4.1.1 使用裸指针,需要手动释放

{
    int* p = new int(10);
    // ...
    delete p;  // 要记得释放内存
}

4.1.2 使用unique_ptr,自动释放内存

{
  std::unique_ptr<int> u_ptr = std::make_unique<int>(10);
  // ...
  // 离开该作用域会自动释放内存
}

4.1.3 unique_ptr所有权的转移,只支持move转移

{
  std::unique_ptr<int> u_ptr = std::make_unique<int>(10);
  std::unique_ptr<int> u_ptr1 = u_ptr;  // 编译报错, unique_ptr只支持move转移所有权
  std::shared_ptr<int> s_ptr = u_ptr;   // 编译报错,不支持与shared_ptr混用
  std::unique_ptr<int> u_ptr2 = std::move(u_ptr);
}

4.1.4 unique_ptr 还可以指向数组

{
    std::unique_ptr<int[]> u_ptr = std::make_unique<int[]>(10);
    for (int i = 0; i < 10; i++) {
        u_ptr [i] = i;
    }   
    for (int i = 0; i < 10; i++) {
        std::cout << u_ptr [i] << std::endl;
    }   
}
4.2 shared_ptr

本质是对资源做引用计数,当引用计数为0时,释放该资源。

4.2.1 使用shared_ptr,自动释放内存

{
  std::shared_ptr<int> p = new int(1); // 错误,不能将裸指针直接赋值为智能指针
  std::shared_ptr<int> s_ptr = std::make_shared<int>(10);
  assert(s_ptr.use_count() == 1); // 此时,s_ptr的引用计数为1
  {
    std::shared_ptr<int> s_ptr1 = s_ptr;
    assert(s_ptr.use_count() == 1); // 此时,s_ptr的引用计数为2
  }
  assert(s_ptr.use_count() == 1); // 离开s_ptr1 的作用域,引用计数减1
}
// 引用计数为0,释放该资源

4.2.2 shared_ptr也可以指向数组

{
  std::shared_ptr<int[]> s_ptr(new int[10]);
  // std::shared_ptr<int[]> u_ptr = std::make_shared<int[]>(10);  c++20才支持
    for (int i = 0; i < 10; i++) {
        s_ptr[i] = i;
    } 
    for (int i = 0; i < 10; i++) {
        std::cout << s_ptr[i] << std::endl;
    } 
}

4.2.3 当需要获取原始指针时,可以通过get方法。不过,谨慎使用get方法。

{
  int main()
  {
    int *ptmp = new int(10);
    std::shared_ptr<int> s_ptr(ptmp);  // 裸指针委托智能指针进行管理
    //std::shared_ptr<int> s_ptr = std::make_shared<int>(10);
    int* ptr = s_ptr.get(); 
    std::cout << ptmp << ", " << ptr << std::endl;
    return 0;
  }
}

4.2.4 通过 shared_from_this() 返回 this 指针

不要将this指针作为shared_ptr 返回出来,因为 this 指针本质上市裸指针,可能会导致多次析构。

#include <iostream> 
#include <memory> 
using namespace std;
class A {
public:
  shared_ptr<A> GetSelf(){
    return shared_ptr<A>(this);
  }
  ~A(){
    cout << "Destructor A" << endl;
  }
}
int main()
{
  shared_ptr<A> s_ptrA(new A);
  shared_ptr<B> s_ptrB = s_ptrA->GetSelf();
  return 0;
}

运行结果会调用两次析构函数。因为用一个this指针构造了两个指针指针,而这两个智能指针是没有关系的。所以,在程序结束时,都会调用各自的析构函数,导致重复析构。

正确的做法是:让需要返回的类共有继承 std::enable_shared_from_this 类,然后使用基类的成员函数shared_from_this() 来返回目标类 this 的 shared_ptr。

#include <iostream> 
#include <memory> 
using namespace std;
class A : public std::enable_shared_from_this<A> {
public:
  shared_ptr<A> GetSelf(){
    return shared_from_this();
  }
  ~A(){
    cout << "Destructor A" << endl;
  }
}
int main()
{
  shared_ptr<A> s_ptrA(new A);
  shared_ptr<B> s_ptrB = s_ptrA->GetSelf();
  return 0;
}

4.2.5 循环引用,导致内存泄漏

#include <iostream> 
#include <memory> 
using namespace std; 
class A; 
class B; 
class A { 
public: std::shared_ptr<B> bptr; 
  ~A() {
    cout << "A is deleted" << endl; 
  } 
};
class B { 
public: std::shared_ptr<A> aptr; 
// public: std::weak_ptr<A> aptr; // 解决循环引用问题
  ~B() {cout << "B is deleted" << endl; } 
};
int main() { 
  { 
    std::shared_ptr<A> ap(new A); 
    std::shared_ptr<B> bp(new B); 
    ap->bptr = bp; 
    bp->aptr = ap; 
  }
  cout<< "main leave" << endl; // 循环引用导致ap bp退出了作用域都没有析构
  return 0; 
}

说明:循环引用导致ap和bp的引用技术都是2,在离开作用域后,各自的引用记数减1,并没有减为0。导致两个智能指针都没有被析构,导致内存泄漏。

解决方法:把A和B任何一个成员变量改为weak_ptr

五、shared_ptr实现原理

shared_ptr内部有两个指针,一个指向目标对象,另一个指向控制块,控制块包含引用计数、弱计数和删除器和其他数据。

5.1 指定删除器

在使用shared_ptr管理非new对象或者没有析构函数时,需要为其指定删除器。

//1-3-1-delete 
#include <iostream> 
#include <memory> 
using namespace std; 
  void DeleteIntPtr(int *p) { 
  cout << "call DeleteIntPtr" << endl; 
  delete p; 
}
int main() { 
  std::shared_ptr<int> p(new int(1), DeleteIntPtr);
  std::shared_ptr<int> p(new int(1), [](int *p) { delete p; }); //使用lambda表达式
  // 注意 在指定 unique_ptr 的删除器时,需要指明删除器的类型
  std::unique_ptr<int> ptr4(new int(1), [](int *p){delete p;}); // 错误
  std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;}); // 正确
  return 0;
}
六、weak_ptr

share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互

使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从

一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。

6.1 基本用法

int main()
{
  shared_ptr<int> sp(new int(10)); 
  weak_ptr<int> wp(sp); 
  cout << wp.use_count() << endl; //结果讲输出1
  // 通过 expired 方法判断所观察对象是否已经被释放
  if(wp.expired()) 
    cout << "weak_ptr无效,资源已释放"; 
  else
    cout << "weak_ptr有效";
  return 0;
}

6.2 通过lock方法获取监视对象的shared_ptr。

举例:比如在多线程情况下

  1. 线程1正在使用某个shared_ptr,线程2,可能在某个时刻需要释放指针,因为线程1使用了 lock() 方法锁住这个指针,线程2只能等线程1使用完才能释放。
  2. 线程2已经释放了该shared_ptr,线程1会通过expired判断该资源是否还存在,规避了异常发生。
std::weak_ptr<int> gw; // 全局变量
void func()
{
  auto sptr = gw.lock();
  if (gw.expired()){
    cout << "gw无效,资源已释放";
  }else{
    cout << "gw有效, *spt = " << *spt << endl;
  }
}
int main()
{
  {
    auto sp = std::shared_ptr<int>(100);
    gw = sp;
    func(); // 还在shared_ptr作用域内,gw有效
  }
  func(); // 出了作用域,打印:"gw无效,资源已释放";
}

文章参考与<零声教育>的C/C++linux服务期高级架构

相关文章
|
1月前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
108 59
|
12天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
35 4
|
28天前
|
存储 安全 编译器
在 C++中,引用和指针的区别
在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
|
28天前
|
存储 搜索推荐 C语言
如何理解指针作为函数参数的输入和输出特性
指针作为函数参数时,可以实现输入和输出的双重功能。通过指针传递变量的地址,函数可以修改外部变量的值,实现输出;同时,指针本身也可以作为输入,传递初始值或状态。这种方式提高了函数的灵活性和效率。
|
1月前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
38 1
|
1月前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
30 2
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(三)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(二)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(一)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
33 0