C++学习之移动语义与智能指针详解

 更新时间:2021年05月30日 12:52:27   作者:谢白羽  
智能指针和移动语义是迄今为止,最难理解的两个概念,下面这篇文章主要给大家介绍了关于C++学习之移动语义与智能指针的相关资料,需要的朋友可以参考下

移动语义

1.几个基本概念的理解

(1)可以取地址的是左值,不能取地址的就是右值,右值可能存在寄存器,也可能存在于栈上(短暂存在栈)上

(2)右值包括:临时对象、匿名对象、字面值常量

(3)const 左值引用可以绑定到左值与右值上面,称为万能引用。正因如此,也就无法区分传进来的参数是左值还是右值。

const int &ref = a;//const左值引用可以绑定到左值
const int &ref1 = 10;//const左值引用可以绑定到右值

(4)右值引用:只能绑定到右值不能绑定到左值

2.移动构造函数

注意:

移动函数(移动构造函数和移动赋值运算符函数)优先于复制函数(拷贝构造函数和赋值运算符函数)的执行;具有移动语义的函数(移动构造函数和移动赋值运算符函数)优先于具有复制控制语义函数(拷贝构造函数和赋值运算符函数)的执行

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

3.移动赋值函数

String &operator=(String &&rhs)
{
    cout << "String &operator=(String &&)" << endl;
    if(this != &rhs)//1、自移动
    {
        delete [] _pstr;//2、释放左操作数
        _pstr = nullptr;
        _pstr = rhs._pstr;//3、浅拷贝
        rhs._pstr = nullptr;
    }
    
    return *this;//4、返回*this
}

4.std::move函数

std::move:

原理:将左值转换为右值,在内部其实上是做了一个强制转换,static_cast<T &&>(lvaule)。将左值转换为右值后,左值就不能直接使用了,如果还想继续使用,必须重新赋值。std::move()作用于内置类型没有任何作用,内置类型本身是左值还是右值,经过std::move()后不会改变。

5.面试题,关于实现String

#include <iostream>
#include <string>

using std::string;
using std::cout;
using std::endl;

class String
{
public:
    //(当传递右值的时候)具有移动语义的函数优先于具有复制控制语义的函数
    //移动构造函数(只针对右值)
    String(String &&rhs)
    : _pstr(rhs._pstr)
    {
        cout << "String(String &&)" << endl;
        rhs._pstr = nullptr;
    }

    //移动赋值运算符函数(传入右值)
    String &operator=(String &&rhs)
    {
        cout << "String &operator=(String &&)" << endl;
        if(this!=&rhs){     //不能自复制
            delete [] _pstr;//释放左操作数
            _pstr = nullptr;

            _pstr=rhs._pstr;//转移右操作的资源
            rhs._pstr=nullptr;//释放右值
        }
        return *this;
    }

private:
    char* _pstr;
};

资源管理和智能指针

一、C语言中的问题

C语言在对资源管理的时候,比如文件指针,由于分支较多,或者由于写代码的人与维护的人不一致,导致分支没有写的那么完善,从而导致文件指针没有释放,所以可以使用C++的方式管理文件指针。。。

class SafeFile
{
public:
    //在构造的时候托管资源(fp)
    SafeFile(FILE *fp)
    : _fp(fp)
    {
        cout << "SafeFile(FILE *)" << endl;
        if(nullptr==fp)
        {
            cout << "nullptr == _fp " << endl;
        }
    }

    //提供若干访问资源的方法
    void write(const string &msg)
    {
        fwrite(msg.c_str(),1,msg.size(),_fp);  //调用c语言的函数往_fp输入数据
    }

    //在销毁(析构)时候释放资源(fp)
    ~SafeFile()
    {
        cout << "~SafeFile()" << endl;
        if(_fp)
        {
            fclose(_fp);
            cout << "fclose(_fp)" << endl;
        }
    }
private:
    FILE *_fp;
};
void test()
{
    string s1 = "hello,world\n";
    SafeFile sf(fopen("text.txt","a+"));
    sf.write(s1);
}

二、C++的解决办法(RAII技术)

1)概念:资源管理 RAII 技术,利用对象的生命周期管理程序资源(包括内存、文件句柄、锁等)的技术,因为对象在离开作用域的时候,会自动调用析构函数

2)关键:要保证资源的释放顺序与获取顺序严格相反。。正好是析构函数与构造函数的作用

3)RAII常见特征

1、在构造时初始化资源,或者托管资源。
2、析构时释放资源。
3、一般不允许复制或者赋值(值语义-对象语义)
4、提供若干访问资源的方法。

4)区分:值语义:可以进行复制与赋值。

5)对象语义:不能进行复制与赋值,一般使用两种方法达到要求:

(1)、将拷贝构造函数和赋值运算符函数设置为私有的就 ok 。

(2)、将拷贝构造函数和赋值运算符函数使用=delete.

6)RAII技术代码

template <typename T>
class RAII
{
public:
    //通过构造函数托管资源
    RAII(T *data)
    : _data(data)
    {
        std::cout<< "RAII(T *)" << std::endl;
    }

    //访问资源的方法
    T *operator->()
    {
        return _data;
    }
    T &operator*()
    {
        return *_data;
    }
    T *get()const
    {
        return _data;
    }
    void reset(T *data)
    {
        if(_data)
        {
            delete _data;
            _data = nullptr;
        }
        _data = data;
    }

    //不能赋值和复制
    RAII(const RAII&rhs) = delete;
    RAII&operator=(const RAII&rhs)=delete;

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


private:
    T *_data;
};
void test3()
{
    //这里没给出Point类的实现方式
    RAII<Point> ppt(new Point(1,2));
    cout<<"ppt = ";
    ppt->print();
    cout<<endl;
}

三、四种智能指针

RAII的对象ppt就有智能指针的雏形。

1、auto_ptr.cc

最简单的智能指针,使用上存在缺陷,所以被弃用。。。(C++17已经将其删除了)

2、unique_ptr

比auto_ptr安全多了,明确表明是独享所有权的智能指针,所以不能进行复制与赋值。

    unique_ptr<int> up(new int(10));
    cout<<"*up="<<*up<<endl;              //打印10
    cout<<"up.get() = "<<up.get()<<endl;  //获取托管的指针的值,也就是10的地址

    cout << endl << endl;
    /* unique_ptr<int> up2(up);//error,独享资源的所有权,不能进行复制 */

    unique_ptr<int> up4(std::move(up));  //通过移动语义转移up的所有权
    cout<<"*up="<<*up4<<endl;
    cout<<"up.get() = "<<up4.get()<<endl;

    unique_ptr<Point> up5(new Point(3,4));//通过移动语义转移up的所有权
    vector<unique_ptr<Point>> numbers;
    numbers.push_back(unique_ptr<Point>(new Point(1,2)));
    numbers.push_back(std::move(up5));

3、shared_ptr

    shared_ptr<int> sp(new int(10));
    cout << "*sp = " << *sp << endl;        //打印10
    cout << "sp.get() = " << sp.get() << endl;  //地址
    cout << "sp.use_count() = " << sp.use_count() << endl;  //引用次数为1

    cout<<endl<<endl;
    //提前结束栈对象
    {
        shared_ptr<int> sp2(sp);//共享所有权,使用浅拷贝
        cout << "*sp = " << *sp << endl;
        cout << "sp.get() = " << sp.get() << endl;
        cout << "sp.use_count() = " << sp.use_count() << endl;
        cout << "*sp2 = " << *sp2 << endl;
        cout << "sp2.get() = " << sp2.get() << endl;              //地址都一样
        cout << "sp2.use_count() = " << sp2.use_count() << endl;  //引用次数加1,变为2了
    }

    cout << "sp.use_count() = " << sp.use_count() << endl;  //又变为1了

    cout << endl << endl;
    shared_ptr<Point> sp4(new Point(3.4));//通过移动语义转移sp的所有权
    vector<shared_ptr<Point>> numbers;
    numbers.push_back(shared_ptr<Point> (new Point(1,2)));
    numbers.push_back(sp4);
    numbers[0]->print();
    numbers[1]->print();

3.1、循环引用

该智能指针在使用的时候,会使得引用计数增加,从而会出现循环引用的问题,两个shared_ptr智能指针互指,导致引用计数增加,不能靠对象的销毁使得引用计数变为0,从而导致内存泄漏。。

class Child;
class Parent
{
public:
  Parent()
 {
    cout << "Parent()" << endl;
 }
  ~Parent()
 {
    cout << "~Parent()" << endl;
 }
  shared_ptr<Child> pParent;
};
class Child
{
public:
  Child()
 {
    cout << "Child()" << endl;
 }
  ~Child()
 {
    cout << "~Child()" << endl;
 }
  shared_ptr<Parent> pChild;
};
void test()
{
  //循环引用可能导致内存泄漏
  shared_ptr<Parent> parentPtr(new Parent());
  shared_ptr<Child> childPtr(new Child());
  cout << "parentPtr.use_count() = " << parentPtr.use_count() << endl;
  cout << "childPtr.use_count() = " << childPtr.use_count() << endl;
 
  cout << endl << endl;
  parentPtr->pParent = childPtr;//sp = sp
  childPtr->pChild = parentPtr;
  cout << "parentPtr.use_count() = " << parentPtr.use_count() << endl;
  cout << "childPtr.use_count() = " << childPtr.use_count() << endl;
}

1.解决循环引用的办法是使得其中一个改为weak_ptr,不会增加引用计数,这样可以使用对象的销毁而打破引用计数减为0的问题。。

2.修改: shared_ptr pChild;改为 weak_ptr pChild;即可解决循环引用的问题。。

parentPtr->pParent = childPtr;//sp = sp
childPtr->pChild = parentPtr;//wp = sp,weak_ptr不会导致引用计数加1

4、weak_ptr

与shared_ptr相比,称为弱引用的智能指针,shared_ptr是强引用的智能指针。weak_ptr不会导致引用计数增加,但是它不能直接获取资源,必须通过lock函数从wp提升为sp,从而判断共享的资源是否已经销毁

    weak_ptr<Point> wp
    {
        shared_ptr<Point> sp(new Point(1,2));
        wp = sp;
        cout << "wp.use_count = " << wp.use_count() << endl;
        cout << "sp.use_count = " << sp.use_count() << endl;

        cout<<"wp.expired = "<<wp.expired()<<endl;//此方法等同于use_count()==0?
        //等于0表示false,空间还存在
        //不等0表示true,空间已经不存在了
        //expired = use_count
        shared_ptr<Point> sp2 = wp.lock();//判断共享的资源是否已经销毁的方式就是从wp提升为sp
        if(sp2)
        {
            cout << "提升成功" << endl;
        }
        else
        {
            cout << "提升失败" << endl;
        }
     }

四、为智能指针定制删除器

1)很多时候我们都用new来申请空间,用delete来释放。库中实现的各种智能指针,默认也都是用delete来释放空间,但是若我们采用malloc申请的空间或是用fopen打开的文件,这时我们的智能指针就无法来处理,因此我们需要为智能指针定制删除器,提供一个可以自由选择析构的接口,这样,我们的智能指针就可以处理不同形式开辟的空间以及可以管理文件指针。

2)自定义智能指针的方式有两种:

(1)函数指针

(2)仿函数(函数对象)

函数指针的形式:

template<class T>
void Free(T* p)
{
  if (p)
    free(p);
}
template<class T>
void Del(T* p)
{
  if (p)
    delete p;
}
void FClose(FILE* pf)
{
  if (pf)
    fclose(pf);
}
//定义函数指针的类型
typedef void(*DP)(void*);
template<class T>
class SharedPtr
{
public:
  SharedPtr(T* ptr = NULL ,DP dp=Del)
 :_ptr(ptr)
 , _pCount(NULL)
 , _dp(dp)
 {
    if (_ptr != NULL)
   {
      _pCount = new int(1);
   }
 }
private:
  void Release()
 {
    if (_ptr&&0==--GetRef())
   {
      //delete _ptr;
      _dp(_ptr); 
      delete _pCount;
   }
 }
  int& GetRef()
  {
    return *_pCount;
 }
private:
  T* _ptr;
  int* _pCount;
  DP _dp;
};

仿函数(函数对象)

关于删除器的使用

五、智能指针的误用

1、同一个裸指针被不同的智能指针托管,导致被析构两次。

1.1、直接使用

1.2、间接使用

2、还是裸指针被智能指针托管形式,但是比较隐蔽。。。

class Point
: public std::enable_shared_from_this<Point>
{
public:
  Point(int ix = 0, int iy = 0)
 : _ix(ix)
 , _iy(iy)
 {
    cout << "Point(int = 0, int = 0)" << endl;
 }
  void print() const
 {
    cout << "(" <<_ix
       << ","  << _iy
       << ")" << endl;
 }
  /* Point *addPoint(Point *pt) */
  shared_ptr<Point> addPoint(Point *pt)
 {
    _ix += pt->_ix;
    _iy += pt->_iy;
    //this指针是一个裸指针
    /* return shared_ptr<Point>(this); */
    return shared_from_this();
 }
  ~Point()
 {
    cout << "~Point()" << endl;
 }
private:
  int _ix;
  int _iy;
};
void test3()
{
  shared_ptr<Point> sp1(new Point(1, 2));
  cout << "sp1 = ";
  sp1->print();
  cout << endl;
  shared_ptr<Point> sp2(new Point(3, 4));
  cout << "sp2 = ";
  sp2->print();
  cout << endl;
  shared_ptr<Point> sp3(sp1->addPoint(sp2.get()));
  cout << "sp3 = ";
  sp3->print();
}

总结

到此这篇关于C++学习之移动语义与智能指针的文章就介绍到这了,更多相关C++移动语义与智能指针内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 浅谈C/C++中的static与extern关键字的使用详解

    浅谈C/C++中的static与extern关键字的使用详解

    本篇文章是对C/C++中的static与extern关键字的使用进行了详细的分析介绍,需要的朋友参考下
    2013-05-05
  • C++ 构造函数中使用new时注意事项

    C++ 构造函数中使用new时注意事项

    本文主要介绍了C++ 构造函数中使用new时注意事项。具有很好的参考价值,下面跟着小编一起来看下吧
    2017-02-02
  • 堆基本操作实现最大堆

    堆基本操作实现最大堆

    这篇文章主要介绍了堆基本操作实现最大堆,需要的朋友可以参考下
    2014-02-02
  • 详细了解C语言二叉树的建立与遍历

    详细了解C语言二叉树的建立与遍历

    这篇文章主要介绍了C中二叉树的建立和各种遍历实例代码,涉及树节点的定义,后序遍历,层序遍历,深度优先和广度优先等相关内容,具有一定借鉴价值,需要的朋友可以参考下
    2021-07-07
  • ipv6实现tcp编程示例

    ipv6实现tcp编程示例

    这篇文章主要介绍了ipv6实现tcp编程示例,需要的朋友可以参考下
    2014-03-03
  • C++ 学习之旅二 说一说C++头文件

    C++ 学习之旅二 说一说C++头文件

    作为一个二手的.net程序员,你看到了C++头文件一定就犯迷糊了,这到底是个啥玩意。再我纠结了24个小时, google20次,度娘10下,看过10来骗文章以后,我可能稍微开窍了。我对C++头文件总结,与.net比较如下
    2012-11-11
  • linux 匿名管道实例详解

    linux 匿名管道实例详解

    这篇文章主要介绍了linux 匿名管道实例详解的相关资料,需要的朋友可以参考下
    2017-06-06
  • 引用numpy出错详解及解决方法

    引用numpy出错详解及解决方法

    这篇文章主要介绍了引用numpy出错 解决方法的相关资料,需要的朋友可以参考下
    2017-02-02
  • C++ leetcode之删除并获得点数的示例代码

    C++ leetcode之删除并获得点数的示例代码

    这篇文章主要介绍了C++ leetcode之删除并获得点数的示例代码,本文给大家分享问题解析及解决方案,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-05-05
  • Qt实现苹果状态切换按钮

    Qt实现苹果状态切换按钮

    这篇文章主要为大家详细介绍了Qt实现苹果状态切换按钮,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-08-08

最新评论