智能指针shared_ptr、unique_ptr、weak_ptr

简介: 智能指针shared_ptr、unique_ptr、weak_ptr


智能指针解决的问题

智能指针主要解决以下两个问题:

  • 避免内存泄露。一般采用malloc、new在堆上分配内存需要使用free、delete手动释放;使用智能指针可以自动释放内存。
  • 共享所有权指针的传播和释放,例如在多线程项目中可以很好地处理不同线程的同一个对象的析构问题。

智能指针分类

C++98有一个智能指针auto_ptr(目前已弃用),C++11有3个常用的指针shared_ptr,unique_ptr,weak_ptr

  • shared_ptr:共享对象的所有权,性能略差。
  • unique_ptr:独占对象的所有权,由于没有引用计数,性能较好于shared_ptr。
  • weak_ptr:该智能指针通常用来配合shared_ptr,解决循环引用的问题。

shared_ptr

内存模型图

  • shared_ptr是一个模板类。
  • shared_ptr内部有两个指针,一个ptr指向对象,一个ptr指向控制块。控制块里面包含着引用计数(reference count,或者叫use_count)和一个弱计数(weak_count,一般在weak_ptr的配合下才起作用)。

shared_ptr示例

//普通的内存分配
Buffer buf = new Buffer("auto free memory");
delete buf;//需要配合delete使用
//智能指针指向分配内存的对象
shared_ptr<buffer> buf = make_shared<Buffer>("auto free memory");
//在作用域外,自动释放内存

shared_ptr含义

shared_ptr使用引用计数记录对象被引用的次数。每一个shared_ptr的拷贝都指向相同的内存,当最后一个shared_ptr析构时,也就是use_count为0后,内存才会释放。

shared_ptr基本用法及常用函数

常用函数

  • s.get():返回shared_ptr中保存的裸指针。
  • s.reset(…):重置shared_ptr,将指针s释放并置空,引用计数减一。如果reset带了输入参数(一个对象),则指针释放并重新指向该对象,原对象对应的引用计数减一。
  • s.use_count():返回shared_ptr的引用计数。
  • s.unique():如果use_count为1,则返回true,否之返回false。

智能指针的构造,初始化-make_shared

一般优先使用make_shared来构造智能指针,更加高效。

auto sp1 = make_shared<int>(100);
//或者
shared_ptr<int> sp1 = make_shared<int>(100);

以下方法也可以初始化shared_ptr,但效率不如make_shared

std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3;
p3.reset(new int(1));

效率不同的原因:因为shared_ptr构造函数会执行两次内存分配,一次给int对象,一次给引用计数(可能还是两个内存区域)。而make_shared只会执行一次内存分配,将两个数据一次性分配完(一个内存区域)。

explicit

shared_ptr是有explicit定义的,所以不能将一个原始指针直接赋值给一个智能指针,如下的操作是错误的:

std::shared_ptr<int> p = new int(1);//该操作会隐式调用拷贝构造函数,不满足explicit定义

获取原始指针get

当需要获取原始指针时,可以通过get方法来返回原始指针,代码如下所示:

std::shared_ptr<int> ptr(new int(1));
int *p = ptr.get(); //
//不小心 delete p,会导致该内存delete两次

谨慎使用p.get()的返回值。p.get()的返回值就相当于一个裸指针的值,不合适地使用这个值,上述陷阱的所有错误都有可能发生。如下:

  • 不要保存p.get()的返回值 ,无论是保存为裸指针还是shared_ptr都是错误的
  • 保存为裸指针不知什么时候就会变成悬空指针,保存为shared_ptr则产生了独立指针
  • 不要delete p.get()的返回值 ,会导致对一块内存delete两次的错误

指定删除器

shared_ptr可以指定删除器。

示例代码如下:

#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);
  return 0;
}

删除动态数组

因为shared_ptr的默认删除器不支持数组对象,所以当使用shared_ptr管理动态数组时,这时候指定删除器就起大作用了。

代码如下所示:

#include <iostream>
#include <memory>
using namespace std;
void DeleteIntPtr(int *p) {
  cout << "call DeleteIntPtr" << endl;
  delete p;
}
int main()
{
  std::shared_ptr<int> p3(new int[10], [](int *p) { delete [] p;});//该指定删除器是以匿名函数lambda表达式呈现
  return 0;
}

shared_ptr的注意问题

1.一个原始指针初始化多个shared_ptr,会造成二次释放同一内存空间。

int *ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr); // 错误

因为p1,p2两个指针之间无关联关系,(每个指针的强引用计数都是1),所以在释放ptr所指向的内存时,p1和p2都会释放这个内存空间,显然有问题(一个内存空间被释放了两次)。

2.不要在函数实参中创建shared_ptr。

如下:

function(shared_ptr<int>(new int), g()); //有缺陷

因为C++的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的,一般是从右到左,但也可能从左到右,所以,可能的过程是先new int,然后调用g(),如果恰好g()发生异常,而shared_ptr还没有创建, 则int内存泄漏了,正确的写法应该是先创建智能指针,代码如下:

shared_ptr<int> p(new int);
function(p, g());

3.避免循环引用。

#include <iostream>
#include <memory>
using namespace std;
class A {
public:
  std::shared_ptr<B> bptr;
  ~A() {
    cout << "A is deleted" << endl;
  }
};
class B {
public:
  std::shared_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,在离开作用域之后,ap和bp的引用计数减为1,而没有减为0,导致两个指针都不会被析构,产生内存泄漏。

解决的办法是把A和B任何一个成员变量改为weak_ptr

比如:

class A {
public:
  std::weak_ptr<B> bptr; // 修改为weak_ptr
  ~A() {
    cout << "A is deleted" << endl;
  }
};

unique_ptr

unique_ptr含义

  • unique_ptr是一个独占型的智能指针,不能将一个unique_ptr指向的对象赋值给另一个unique_ptr
  • unique_ptr可以指向一个数组
  • unique_ptr需要确定删除器的类型
unique_ptr<T> my_ptr(new T);
unique_ptr<T> my_other_ptr = my_ptr; // 报错,不能复制

move转移

unique_ptr不允许复制,但可以通过函数将所有权转移给其他的unique_ptr,通过std::move来转移到其他的unique_ptr,这样它本身就不再拥有原来指针的所有权了。例如

unique_ptr<T> my_ptr(new T); // 正确
unique_ptr<T> my_other_ptr = std::move(my_ptr); // 正确

make_unique

make_unique是在c++14中加入标准库的。

std::make_unique upw1(std::make_unique<Widget>());
//或者
std::unique_ptr<Widget> upw2(new Widget);

unique_ptr和shared_ptr对比

1.unique_ptr可以指向一个数组,如下:

std::unique_ptr<int []> ptr1(new int[10]);
ptr1[9] = 9;
std::shared_ptr<int []> ptr2(new int[10]); // 这个是不合法的
std::shared_ptr<int> ptr3(new int[10]);//这种是合法的

2.unique_ptr指定删除器和shared_ptr有区别,unique_ptr需要确定删除器的类型,不能像shared_ptr那样直接指定删除器。

std::shared_ptr<int> ptr3(new int(1), [](int *p){delete p;}); // 正确
std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;}); //正确

shared_ptr和unique_ptr使用场景

如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。

weak_ptr

  • 当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
  • weak_ptr是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。
  • weak_ptr设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。
#include <iostream>
#include <memory>
using namespace std;
class A {
public:
  std::weak_ptr<B> bptr; // 修改为weak_ptr
  ~A() {
    cout << "A is deleted" << endl;
  }
};
class B {
public:
  std::shared_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; 
  return 0;
}

智能指针的线程安全

shared_ptr的内部是封装了锁的,因为指向同一个对象的智能指针的引用计数指针是指向同一块资源的。实际上引用计数在shared_ptr底层中是以指针的形式实现的,所有的对象通过指针访问同一块空间,从而实现共享。所有智能指针在对引用计数加减的时候内部是有“加锁”和“释放锁”的操作。

结论:shared_ptr的引用计数时线程安全的(因为是原子操作)。

而shared_ptr管理对象时不一定是线程安全的。

当不同的线程,操控同一个shared_ptr智能指针时,会发生线程不安全的情况。

因为智能指针管理对象需要两步, 首先一个指针指向管理的对象, 然后一个指针控制引用计数。

如果当指针指针的第一步完成了,第二步还没开始的时候CPU被占用,另一个线程也操作这个智能指针时就会出现线程安全问题。

不同的线程,操作不同的shared_ptr智能指针线程安全的哦。=.=

目录
相关文章
|
6月前
|
设计模式 C++ 开发者
C++一分钟之-智能指针:unique_ptr与shared_ptr
【6月更文挑战第24天】C++智能指针`unique_ptr`和`shared_ptr`管理内存,防止泄漏。`unique_ptr`独占资源,离开作用域自动释放;`shared_ptr`通过引用计数共享所有权,最后一个副本销毁时释放资源。常见问题包括`unique_ptr`复制、`shared_ptr`循环引用和裸指针转换。避免这些问题需使用移动语义、`weak_ptr`和明智转换裸指针。示例展示了如何使用它们管理资源。正确使用能提升代码安全性和效率。
126 2
|
7月前
|
安全 Linux 编译器
从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr(下)
从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr
46 3
|
7月前
|
安全 编译器 C语言
从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr(中)
从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr
54 1
|
7月前
|
安全 编译器 C语言
从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr(上)
从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr
40 0
|
1月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
101 13
|
6月前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
2月前
|
C语言
无头链表二级指针方式实现(C语言描述)
本文介绍了如何在C语言中使用二级指针实现无头链表,并提供了创建节点、插入、删除、查找、销毁链表等操作的函数实现,以及一个示例程序来演示这些操作。
36 0
|
3月前
|
存储 人工智能 C语言
C语言程序设计核心详解 第八章 指针超详细讲解_指针变量_二维数组指针_指向字符串指针
本文详细讲解了C语言中的指针,包括指针变量的定义与引用、指向数组及字符串的指针变量等。首先介绍了指针变量的基本概念和定义格式,随后通过多个示例展示了如何使用指针变量来操作普通变量、数组和字符串。文章还深入探讨了指向函数的指针变量以及指针数组的概念,并解释了空指针的意义和使用场景。通过丰富的代码示例和图形化展示,帮助读者更好地理解和掌握C语言中的指针知识。
130 4
|
4月前
|
C语言
【C初阶——指针5】鹏哥C语言系列文章,基本语法知识全面讲解——指针(5)
【C初阶——指针5】鹏哥C语言系列文章,基本语法知识全面讲解——指针(5)
|
4月前
|
C语言
【C初阶——指针4】鹏哥C语言系列文章,基本语法知识全面讲解——指针(4)
【C初阶——指针4】鹏哥C语言系列文章,基本语法知识全面讲解——指针(4)