博客园  :: 首页  :: 新随笔  :: 管理

1.3.1 智能指针与右值引用

Posted on 2023-03-27 20:58  wsg_blog  阅读(96)  评论(0编辑  收藏  举报

Linux C/C++服务器

C++智能指针与右值引用

智能指针、右值引用、STL

编译

版本比较低的编译器要指明c++11或c++14

g++ -o main main.cpp -std=c++11
g++ -o main main.cpp -std=c++14

1. 智能指针

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

  • 内存泄漏:内存手动释放,使用智能指针可以自动释放
  • 共享所有权指针的传播和释放,比如多线程使用同一个对象时析构问题

C++里面的四个智能指针:auto_ptr,shared_ptr,unique_ptr,weak_ptr其中后三个是C++11支持,并且第一个已经被C++11弃用。

  • unique_ptr独占对象所有权,由于没有引用计数,因此性能较好。
  • shared_ptr共享对象所有权,但性能略差。
  • weak_ptr配合shared_ptr,解决循环引用的问题。

1.1 shared_ptr共享智能指针

1.1.1 内存模型


shared_ptr内部包含两个指针,一个指向对象,另一个指向控制块(control block),控制块包含一个引用计数(reference count),一个弱计数(weak count)和一些其他数据。

share_ptr的实现包含了两部分,

  • 一个是指向堆上创建的对象裸指针,Ptr to T
  • 一个指向内部隐藏的、共享的控制块,Control Block,Reference Count指当前这个堆上对象被多少对象引用了,也被称为引用计数,当销毁一个引用对象引用计数就减一,当引用计数为0后,堆上数据T自动销毁。

1.1.2 基本用法

  • 初始化make_shared/reset
std::shared_ptr<int> p1 = make_shared<int>(1);
std::shared_ptr<int> p2(new int(1));
std::shared_ptr<int> p3 = p2;
std::shared_ptr<int> p4;
p4.reset(new int(1));

我们优先使用make_shared来构建智能指针,因为它更高效
不能将一个原始指针直接赋值给一个智能指针

std::shared_ptr<int> p = new int(1);  //错误赋值
  • 指针获取/指定删除器
//如果用share_ptr管理非new对象或是没有析构函数的类时。应为其传递合适的删除器;
void DeleteIntPtr(int *p){
    delete p;
}
int main(){
    std::shared_ptr<int> p(new int(1), DeletIntPtr);
}
//当p的引用计数为0时,自动调用DeletIntPtr来释放对象的内存。删除器可以是一个lambda表达式,上面的写法可以改写为:
std::shared_ptr<int> p(new int(1), [](int *p) {delete p;});
//当我们使用shared_ptr管理动态数组时,需指定删除器,**因为shared_ptr的默认删除器不支持数组对象**
std::shared_ptr<int> p(new int[10], [](int *p) {delete [] p;});

虽然可以通过get()来获取原始指针,但最好不要用p.get()这种方法,很容易不小心delte p;如果你不知道get()的危险性则永远不要使用get()函数

1.1.3 需要注意的问题

  • 不要用一个原始指针初始化多个shared_ptr
int *ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr);  //逻辑错误
  • 不要在函数实参中创建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());
  • 通过shared_form_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); // 不要这么做
    return shared_from_this();  // 正确做法
  }
  ~A()
  {
    cout << "Destructor A" << endl;
  }
};
int main()
{
  shared_ptr<A> sp1(new A);
  shared_ptr<A> sp2 = sp1->GetSelf();
  return 0;
}
  • 避免循环引用
    循环引用造成内存泄漏
#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;
  ~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,具体方法见weak_ptr章节

1.2 unique_ptr独占智能指针

1.2.1 基本用法

  • unique_ptr是一个独占的智能指针,不能将其赋值给另一个unique_ptr
    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); // 正确
unique_ptr<T> ptr = my_ptr;  // 报错,不能复制
  • unique_ptr可以指向一个数组
std::unique_ptr<int []> ptr(new int[10]);
ptr[9] = 9;
std::shared_ptr<int []> ptr2(new int[10]); // 这个是不合法的,必须要有数组删除器
  • unique_ptr需要确定删除器的类型
    unique_ptr指定删除器和shared_ptr有区别
std::shared_ptr<int> ptr3(new int(1), [](int *p){deletep;}); // 正确
std::unique_ptr<int> ptr4(new int(1), [](int *p){deletep;}); // 错误
std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;}); //需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器

关于shared_ptr和unique_ptr的使用场景是要根据实际应用需求来选择。
如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。

1.3 weak_ptr弱引用智能指针

share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也会被释放。

1.3.1 基本用法

  • use_count()、expired()方法
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
cout << wp.use_count() << endl;  //1.通过use_count()方法获得当前观察资源的引用计数  结果输出1 wp(sp)并不会增加引用计数!
if(wp.expired())                 //2.通过expired()方法判断所观察资源是否已经释放
  cout << "无效,资源已释放";
  • 通过lock方法获取监视的shared_ptr
std::weak_ptr<int> gw;
void f()
{
  cout << "lock\n";
  auto spt = gw.lock();  //锁好资源再去判断是否有效
  std::this_thread::sleep_for(std::chrono::seconds(2));
  if(gw.expired()) {
  cout << "gw Invalid, resource released\n";
  }
  else {
    cout << "gw Valid, *spt = " << *spt << endl;
  }
}
int main()
{
  {
    auto sp = std::make_shared<int>(42);
    gw = sp;
    std::thread([&](){
              std::this_thread::sleep_for(std::chrono::seconds(1));
              cout << "sp reset\n";
              sp.reset();  //释放sp资源
           }).detach();
    f();  //gw任然有效,因为lock
  }
  f();
  return 0;
}
  • weak_ptr解决循环引用问题
    在shared_ptr章节提到智能指针循环引用的问题,因为智能指针的循环引用会导致内存泄漏,可以通过weak_ptr解决该问题,只要将A或B的任意一个成员变量改为weak_ptr
#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::weak_ptr<A> aptr;  // 修改为weak_ptr
  ~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;
}

这样在对B的成员赋值时,即执行bp->aptr=ap;时,由于aptr是weak_ptr,它并不会增加引用计数,所以ap的引用计数仍然会是1,在离开作用域之后,ap的引用计数为减为0,ap会被析构,析构后其内部的bptr绑定的bp的引用计数会由2减为1,然后在离开作用域后bp引用计数又从1减为0,B对象也被析构,不会发生内存泄漏。



2. 右值引用与移动语义

C++11中引用了右值引用和移动语义,可以避免无谓的复制,提高了程序性能。

什么是左值、右值?

  • 左值可以取地址,位于等号左边;
  • 右值没法取地址,位于等号右边,比如常量、表达式、返回右值函数等;
int a = 6;
  • a可以通过 & 取地址,位于等号左边,所以a是左值。
  • 6位于等号右边,6没法通过 & 取地址,所以6是个右值。

2.1 左值引用

int a = 5;
int &ref_a = a; // 左值引用指向左值,编译通过
int &ref_a = 5; // 左值引用指向了右值,会编译失败

引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值,但是const左值引用可以指向右值

const int &ref_a = 5;  // 编译通过
void push_back (const value_type& val);  //函数传常数 vec.push_back(5) 就是左值引用的问题,我们平时可能都在用只是不知道叫法

2.2 右值引用

右值,就是在内存没有确定存储地址、没有变量名,表达式结束就会销毁的值。
右值引用,就是绑定到右值的引用,通过&&来获得右值引用。
可以将右值引用归纳为:非常量右值引用只能绑定到非常量右值上;常量右值引用可以绑定到非常量右值、常量右值上。

int a=10;             //非常量左值(有确定存储地址,也有变量名)
const int a1=20;      //常量左值(有确定存储地址,也有变量名)
const int a2=20;      //常量左值(有确定存储地址,也有变量名)
//非常量右值引用
int &&b1=a;            //错误,a是一个非常量左值,不可以被非常量右值引用绑定
int &&b2=a1;           //错误,a1是一个常量左值,不可以被非常量右值引用绑定
int &&b3=10;           //正确,10是一个非常量右值,可以被非常量右值引用绑定
int &&b4=a1+a2;        //错误,(a1+a2)是一个常量右值,不可以被非常量右值引用绑定
//常量右值引用
const int &&c1=a;      //错误,a是一个非常量左值,不可以被常量右值引用绑定
const int &&c2=a1;     //错误,a1是一个常量左值,不可以被常量右值引用绑定
const int &&c3=a+a1;   //正确,(a+a1)是一个非常量右值,可以被常量右值引用绑定
const int &&c4=a1+a2;  //正确,(a1+a2)是一个常量右值,不可以被常量右值引用绑定

从上述可以发现,常量左值引用可以绑定到右值上,但右值引用不能绑定任何类型的左值,若想利用右值引用绑定左值该怎么办呢?

2.3 move移动语义

C++11中提供了一个标准库move函数获得绑定到左值上的右值引用,即直接调用std::move告诉编译器将左值像对待同类型右值一样处理,但是被调用后的左值将不能再被使用。

int a=10;                 //非常量左值(有确定存储地址,也有变量名)
const int a1=20;          //常量左值(有确定存储地址,也有变量名)
//非常量右值引用
int &&d1=std::move(a);    //正确,将非常量左值a转换为非常量右值,可以被非常量右值引用绑定
int &&d2=std::move(a1);    //错误,将常量左值a1转换为常量右值,不可以被非常量右值引用绑定
//常量右值引用
const int &&c1=std::move(a);      //正确,将非常量左值a转换为非常量右值,可以被常量右值引用绑定
const int &&c2=std::move(a1);     //正确,将常量左值a1转换为常量右值,可以被常量右值引用绑定

可以发现,编译器利用std::move将左值强制转换为相同类型的右值之后,引用情况跟右值是一模一样的。

  1. 左值引用绑定到有确定存储空间以及变量名的对象上,表达式结束后对象依然存在;右值引用绑定到要求转换的表达式、字面常量、返回右值的表达式等临时对象上,赋值表达式结束后就对象就会被销毁。
  2. 左值引用后可以利用别名修改左值对象;右值引用绑定的值不能修改。

2.4 引入右值引用有什么意义?

  1. 减少内存拷贝,提高效率,某些情况下,需要拷贝一个对象然后将其销毁,如:临时类对象的拷贝就要先将旧内存的资源拷贝到新内存,然后释放旧内存,引入右值引用后,就可以让新对象直接使用旧内存并且销毁原对象(右值引用也是引用),这样就减少了内存和运算资源的使用,从而提高了运行效率;
  2. 移动含有不能共享资源的类对象,像IO、unique_ptr这样的类包含不能被共享的资源(如:IO缓冲、指针),因此,这些类对象不能拷贝但可以移动。这种情况,需要先调用std::move将左值强制转换为右值,再进行右值引用。

右值引用与左值引用

2.5 forward完美转发

forward 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。
现存在一个函数

Template<class T>
void func(T &&val);

根据前面所描述的,这种引用类型既可以对左值引用,亦可以对右值引用。
但要注意,引用以后,这个val值它本质上是一个左值!
看下面例子

int &&a = 10;
int &&b = a; //错误

注意这里,a是一个右值引用,但其本身a也有内存名字,所以a本身是一个左值,再用右值引用引用a这是不对的。
因此我们有了std::forward()完美转发,这种T &&val中的val是左值,但如果我们用std::forward (val),就会按照参数原来的类型转发;

int &&a = 10;
int &&b = std::forward<int>(a);

3. STL容器

容器、迭代器

  • 标准容器类:
    array(固定大小数组)、deque(队列)、forward_list(单链表)、list(双向链表)、vector(变长数组)
  • 有序关联容器(红黑树实现):
    set:快速查找,无重复元素
    multiset:快速查找,可有重复元素
    map:一对一映射,无重复元素,基于键快速查找
    multimap:一对一映射,可有重复元素,基于键快速查找
  • 无序容器(hash):
    unordered_set:快速查找,无重复元素
    unordered_multiset:快速查找,可有重复元素
    unordered_map:一对一映射,无重复元素,基于键快速查找
    unordered_multimap:一对一映射,可有重复元素,基于键快速查找
  • 标准库容器类:
    stack:后进先出(LIFO)
    queue:先进先出(FIFO)
    priority_queue:优先级最高的元素先出,堆排序