6.21 移动语义与智能指针

    //先构造,再拷贝构造
    //利用"hello"这个字符串创建了一个临时对象
    //并复制给了s3
    //这一步实际上new了两次
    String s3 = "hello";

背景需求: 这个隐式创建的字符串出了该行就直接销毁掉,效率比较低

可以让_pstr指向这个空间,可以让匿名对象的指针指向nullptr,这样析构函数判断是nullptr对于匿名对象就不会处理。

但是不能直接在拷贝构造函数中直接进行操作,因为可能有的并不是匿名对象的情况,直接将原本的销毁不合适

能对表达式取地址的,称为左值;不能取地址的,称为右值。

&1;//error取不到地址

&"hello"; //文字常量区可以取地址(补充如果文字是相同的会存在一个地方)

&(a + b);//error
&String("hello"); // error

右值存储的位置:

右值可以存储在内存中,比较复杂的或者放内存中性能更佳的时候。

右值也可以存在寄存器中,简单的变量往往放在寄存器中。

区分左右值不能通过存储位置和等号左右进行判断

非const左值引用只能绑定到左值,不能绑定到右值,也就是非const左值引用只能识别出左值。

const左值引用既可以绑定到左值,也可以绑定到右值,也就是表明const左值引用不能区分是左值还是右值。

所以接着上面就是我们需要确定只要是右值引用就可以使用特别的右值拷贝构造,而左值就是普通的拷贝构造函数。

右值引用:

右值引用不能绑定到左值,但是可以绑定到右值,也就是右值引用可以识别出右值

 String(String && rhs)
    : _pstr(rhs._pstr)//浅拷贝
    {
        cout << "String(String&&)" << endl;
        rhs._pstr = nullptr;
    }

也就是说在有移动构造的时候,对于临时变量就不再调用以前的复制运算符函数

加上编译器的去优化参数 -fno-elide-constructors

发现没有再调用拷贝构造函数,而是调用了移动构造函数。

如果还没有就再加上 cstd = c++11

移动构造函数的特点:

1. 四大金刚只要是显示定义一个的时候,编译器就不再提供移动构造函数

2. 显示定义了拷贝构造没有移动构造,就会调用拷贝构造(临时)

3. 显示定义了移动构造,右值复制使用移动构造。

总结:移动构造函数优先级高于拷贝构造函数。也就是会尝试移动函数不行再常规

移动赋值函数

String & operator=(String && rhs){
  if(this != &rhs){
      delete [] _pstr;
      //浅拷贝
      _pstr = rhs._pstr;
      rhs._pstr = nullptr;
      cout << "String& operator=(String&&)" << endl;
  }
  return *this;
}

其实这地方还有有点容易混淆的,因为

String s = "hello"; 移动构造函数

String s = String("hello");移动构造函数 

s = String("xixi")        移动赋值运算符函数

移动赋值运算符函数特点和移动拷贝构造函数相同。

总结:

拷贝构造和赋值运算函数具有复制控制语义的函数

移动构造和移动复制有移动语句的函数移交控制权(移交控制权)

移动语义函数优于控制语义函数

    String s1("hello");
    //右值复制给左值,肯定不是同一个对象
    s1 = String("world");
    //创建了两个内容相同的临时对象,也不是同一对象
    String("wangdao") = String("wangdao");

std::move函数

背景需求:有时候可能必须需要用到右值,所以需要将左值转换为右值

   int a = 1;
   &(std::move(a)); //error,左值转成了右值
   int && r = std::move(a);

【注意】

值显式转换为右值后,原来的左值对象就无法正常工作了,必须要重新赋值才可以继续使用。

但是通过输出流运算符输出s1的 _pstr依然造成了程序的中断,所以说明对std::move(s1)的内容进行修改,会导致s1的内容也被修改。

std::move的本质是在底层做了强制转换(并不是像名字表面的意思一样做了移动)

这就更是说明了move只是欺骗系统说这是一个右值,让系统当作一个右值处理,并且将控制权也都交了出去,对于这个‘右值’的处理就是对于本身的处理。

【注意】在这个时候可能在移动赋值函数中,如果是使用move导致的(如果去掉自复制判断)自复制情况下就会出现问题,所以必须加上自复制判断。

String & operator=(String && rhs){
  if(this != &rhs){
      delete [] _pstr;
      //浅拷贝
      _pstr = rhs._pstr;
      rhs._pstr = nullptr;
      cout << "String& operator=(String&&)" << endl;
  }
  return *this;
}

右值引用本身的性质

int && func(){
    return 10;
}

void test1(){
    // &func();  //无法取址,说明返回的右值引用本身也是一个右值
    int && ref = func();
    &ref;  //可以取址,此时ref是一个右值引用,其本身是左值
}

注意区分下列的情况。

第一二种情况,返回的都是副本临时值都是不能取地址的,第三种情况是全局变量的引用可以取地址,第四种情况是返回一个右值引用是不可以取地址的,但是如果是是在程序中定义的一个右值引用的话是可以进行取地址操作的,原因就是下述所讲有名字的右值引用是左值,无名字是右值。

有名字右值引用是左值,没有名字还是左值

String str2("wangdao");
String func2(){
    String str1("wangdao");
	str1.print();
    return str1;    //对于一个将亡的对象而言是调用移动构造
    //return str2;//长期存在的就调用拷贝构造函数
}

void test2(){
    func2();
    //&func2(); //error,右值
   	String && ref = func2();
    &ref;  //右值引用本身为左值
}

在上述代码中return语句是调用移动构造语句,而不是拷贝构造语句

return

总结:返回将亡值对象使用移动构造,否则调用拷贝构造

资源管理

背景需求:程序的出口比较多,如果在某一个出口忘记将资源释放的时候,不能很好的将资源释放

所以说想到可以通过一个类进行封装保护实现,就像是运算符重载一章中的middleLayer

class SafeFile
{
public:
    //在构造函数中初始化资源(托管资源)
    SafeFile(FILE * fp)
    : _fp(fp)
    {
        cout << "SafeFile(FILE*) " << endl;
    }
    //提供方法访问资源
    void write(const string & msg){
        fwrite(msg.c_str(),1,msg.size(),_fp);
    }
    //利用析构函数释放资源
    ~SafeFile(){
        cout << "~SafeFile()" << endl;
        if(_fp){
            fclose(_fp); 
            cout << "fclose(_fp)" << endl;
        }
    }
private:
    FILE * _fp;
};

void test0(){
    string msg = "hello,world";
    SafeFile sf(fopen("wd.txt","a+"));
    sf.write(msg);
}

同时也像是middleLayer中的担心会出现多次释放的情况所以也是直接在创建对象的时候就是用fopen而不是再赋给一个有名的指针。

RAII技术

RAII类的常见特征

RAII常见的特征:

1.构造函数中托管资源,析构中释放资源

2.不允许复制和赋值(对象语义)

3.提供若干访问方法 (->*读写)

值语义:可以进行复制或赋值(两个变量的值可以相同)

对象语义:不允许复制或者赋值

【联系】上面刚说了移动语义和赋值控制语义

常用手段:

  1. 将拷贝构造函数与赋值运算符函数设置为私有的
  2. 将拷贝构造函数与赋值运算符函数=delete
  3. 使用继承的思想,将基类的拷贝构造函数与赋值运算符函数删除(或设为私有),让派生类继承基类。(原理:因为我们知道当派生类进行拷贝的时候,其中的基类子对象的部分也需要进行拷贝,就用到了基类的拷贝构造函数,如果说基类中的拷贝构造函数删除以后那么其中的基类子对象就不能进行拷贝构造)

 使用最保险的就是将函数删除的方法

RAII类的模拟

借助于上面对于智能指针思想的描述的实现。 

template <class T>
class RAII
{
public:
    //1.在构造函数中初始化资源(托管资源)
    RAII(T * data)
    : _data(data)
    {
        cout << "RAII(T*)" << endl;
    }

    //2.在析构函数中释放资源
    ~RAII(){
        cout << "~RAII()" << endl;
        if(_data){
            delete _data;
            _data = nullptr;
        }
    }

    //3.提供若干访问资源的方法
    T * operator->(){
        return _data;
    }
    
    T & operator*(){
        return *_data;
    }

    T * get() const{
        return _data;
    }

    void set(T * data){
        if(_data){
            delete _data;
            _data = nullptr;
        }
        _data = data;
    }

    //4.不允许复制或赋值
    RAII(const RAII & rhs) = delete;
    RAII& operator=(const RAII & rhs) = delete;
private:
    T * _data;
};

智能指针

//<memory>头文件中

//std::auto_ptr         c++0x 不安全在c++17中已经丢弃

//std::unique_ptr    c++11

//std::shared_ptr     c++11

//std::weak_ptr        c++11

auto_ptr

在其中auto_ptr的赋值运算符函数和拷贝构造函数是可以使用的。

auto_ptr的拷贝构造实际上却是移动复制

template <class _Tp> 
class auto_ptr {
public:
    //拷贝构造
   auto_ptr(auto_ptr& __a) __STL_NOTHROW 
   //ap2的_M_ptr 被赋值为 ap调用release函数的返回值
   : _M_ptr(__a.release()) 
   {}

    //ap调用release函数
   _Tp* release() __STL_NOTHROW 
   {
     //用局部的指针__tmp接管ap的指针所指向的资源
    _Tp* __tmp = _M_ptr;
    _M_ptr = nullptr; //将ap底层的指针设为空指针
    return __tmp;//返回的就是原本ap管理的资源的地址
  }
    
private:
  _Tp* _M_ptr;
};
    auto_ptr<int> ap2(ap);
    cout << "*ap2:" << *ap2 << endl; //ok
    cout << "*ap:" << *ap << endl;  //error,因为底层是移动构造

该拷贝出现隐患被丢弃

unique_ptr(重要)

1. 独享所有权的指针 不允许进行复制或者赋值(对象语义),但是具有移动语义

2. 作为容器元素

当作为容器的元素传入的时候如果是直接传入一个左值的话,会调用复制运算符函数但是函数已经被删除,但是移动复制没有删除,所以说使用移动复制函数移交管理权。

就是使用匿名对象(move函数但是实际上的所有权被移交了,所以原来的指针失效,不用)

vector<unique_ptr<Point>> vec;
    unique_ptr<Point> up4(new Point(10,20));
    //up4是一个左值
    //将up4这个对象作为参数传给了push_back函数,会调用拷贝构造
    //但是unique_ptr的拷贝构造已经删除了
    //所以这样写会报错
    vec.push_back(up4);  //error
    
    vec.push_back(std::move(up4));  //ok,但是不用还是移交了管理权!
    vec.push_back(unique_ptr<Point>(new Point(1,3))); //ok

share_ptr(重要)

背景需求: unique_ptr是独占所有权的智能指针,有时候需要共享,有了share_ptr

原理:类似cowstring的原理,就是有一个引用计数

特征:

1. 共享所有权(可以复制或者赋值),但是具有移动语义(有移动构造和移动复制)

2. 可以作为容器元素

如果是把左值属性的指针传进去,会出现复制操作。use_count会加1

但是会出现循环引用的问题

class Child;

class Parent
{
public:
	Parent()
	{ cout << "Parent()" << endl; }
	~Parent()
	{ cout << "~Parent()" << endl; }
	//只需要Child类型的指针,不需要类的完整定义
	shared_ptr<Child> _spChild;
};

class Child
{
public:
	Child()
	{ cout << "child()" << endl; }
	~Child()
	{ cout << "~child()" << endl; }
	shared_ptr<Parent> _spParent;
};
shared_ptr<Parent> parentPtr(new Parent());
shared_ptr<Child> childPtr(new Child());
//获取到的引用计数都是1
cout << "parentPtr.use_count():" << parentPtr.use_count() << endl;
cout << "childPtr.use_count():" << childPtr.use_count() << endl;

parentPtr->_spChild = childPtr;
childPtr->spParent = parentPtr;
//获取到的引用计数都是2
cout << "parentPtr.use_count():" << parentPtr.use_count() << endl;
cout << "childPtr.use_count():" << childPtr.use_count() << endl;

当栈上的指针删除的时候还会有引用计数都是1 

解决这个问题引入弱引用指针。,因为上述的环在有栈上的指针的时候只要是有一个引用计数不为2就可以解除这个环。因为当栈上指针不再指向的时候就会变成0,销毁后另外一个引用也变为0

week_ptr弱引用的指针

不会增加引用计数

week_ptr是一个弱引用的指针,不会增加引用计数。

对于上述问题的解决办法:将Parent类中的shared_ptr类型指针换成weak_ptr

week_ptr指向不会增加引用计数

可能出现的问题就是引用计数 减到0 _wpChild指针还指向这个位置,会不会出现访问错误问题

实际上week_ptr这个指针不能访问,因为是弱引用指针不能访问

weak_ptr知道所托管的对象是否还存活,如果存活,必须要提升为shared_ptr才能对资源进行访问,不能直接访问。

use_count()只能访问share_ptr的指针指向的个数,也就是知道week_ptr有没有指向空间

week_ptr的初始化的方式

weak_ptr<int> wp;//无参的方式创建weak_ptr

//也可以利用shared_ptr创建weak_ptr 
weak_ptr<int> wp2(sp);

可能不成功当没有空间可以管理的时候,否则有另外一个shared_ptr指针指向。

shared_ptr<int> sp2 = wp.lock();
if(sp2){
cout << "创建shared成功" << endl;
cout << *sp2 << endl;
}else{
cout << "创建shared失败,托管的空间已经被销毁或者没有资源可以管理" << endl;
}

expired函数

bool flag = wp.expired();
if(flag){
cout << "托管的空间已经被销毁" << endl;
}else{
cout << "托管的空间还在" << endl;
}

删除器

unique指针的(模板参数)

背景需求: 默认的删除器使用的是delete,不能回收fopen打开的文件,因为是delete而不能是fclose。

问题描述:如果是使用原本的删除器继续使用delete,不close写的内容可能不能刷新缓冲区,如果再手动close会导致double free问题,因为delete底层本身就是free函数。

所以说需要自定义删除器将删除器中的处理设置为fclose。

struct FILECloser{
void operator()(FILE * fp){
  if(fp){
      fclose(fp);
      cout << "fclose(fp)" << endl;
  }
}
};
void test1(){
string msg = "hello,world\n";
unique_ptr<FILE,FILECloser> up(fopen("res2.txt","a+"));
//get函数可以从智能指针中获取到裸指针
fwrite(msg.c_str(),1,msg.size(),up.get());
}

shared_ptr(作为构造函数参数)

智能指针误用

将一个资源(原生指针)交给两个指针指针管理

void test0(){
//需要人为注意避免
Point * pt = new Point(1,2);
unique_ptr<Point> up(pt);
unique_ptr<Point> up2(pt);//error,出现double free
}

//unique是独占的因此不能共同管理一个对象。

void test2(){
Point * pt = new Point(10,20);
shared_ptr<Point> sp(pt);
shared_ptr<Point> sp2(pt);//error
}

//虽然说shared_ptr是可以共享的,但是也必须是使用赋值和复制的行为才是可以的,否则不可以

//下面这个情况也是同时托管一个原生指针

unique_ptr<int> uq(new int(10));
unique_ptr<int> uq2(new int(10));
uq.reset(uq2.get());
//会发生错误因为两个指针同时托管了一个原生指针,
//shared_ptr 同理                               

 这个地方addPoint如果是放回的是Point*就会出现sp3和sp管理一个Point*原生指针,

如果是返回的是shared_ptr<Point>(this)这种情况也是sp和匿名对象共用this这个point对象指针

这个地方就是用到动态内存管理中的辅助类(enable_shared_from_this)中的成员函数(shared_from_this)来实现功能。就是使用继承的方式在该类中可以使用类的成员函数。

#include <iostream>
#include <memory>
using namespace std;

class Point
:public enable_shared_from_this<Point>
{
public:
    Point(int ix, int iy)
    :_ix(ix)
    ,_iy(iy)
    {}

    shared_ptr<Point> addPoint(Point * pt){
        _ix += pt->_ix;
        _iy += pt->_iy;
        return shared_from_this();
    }
    void print(){
        cout << "(" << _ix << "," << _iy << ")" << endl;
    }
private:
    int _ix;
    int _iy;
};


void test2(){
    shared_ptr<Point> sp(new Point(1,2)); 
    shared_ptr<Point> sp1(new Point(1,2)); 
    shared_ptr<Point> sp2(sp->addPoint(sp1.get()));
    //这个地方只能是使用sp1的get,因为在shared_就是从一个指针中得到另外一个,
    //不能直接使用new不然会出现内存泄漏问题 
}


int main()
{
    test2();
    return 0;
}

  • 27
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值