IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++——右值引用、移动构造函数、move函数、完美转发 -> 正文阅读

[C++知识库]C++——右值引用、移动构造函数、move函数、完美转发

左值引用vs右值引用

左值引用:&

int num = 10;
int &b = num; //num为左值 正确
int &c = 10; //错误,左值引用不能初始化为右值

右值引用:&&
和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:

int num = 10;
int && a = num;  //错误,右值引用不能初始化为左值
int && a = 10;   //正确

移动构造函数

在 C++ 11 标准之前,如果想用其它对象初始化一个同类的新对象,只能借助类中的拷贝构造函数。当类中拥有指针类型的成员变量时,拷贝构造函数中需要以深拷贝的方式复制该指针成员。

看下面的例子:

#include <iostream>
using namespace std;
class demo{
public:
   demo():num(new int(0)){
      cout<<"construct!"<<endl;
   }
   //拷贝构造函数
   demo(const demo &d):num(new int(*d.num)){
      cout<<"copy construct!"<<endl;
   }
   ~demo(){
      cout<<"class destruct!"<<endl;
   }
private:
   int *num;
};
demo get_demo(){
    return demo();
}
int main(){
    demo a = get_demo();
    return 0;
}

如上所示,我们为 demo 类自定义了一个拷贝构造函数。该函数在拷贝 d.num 指针成员时,必须采用深拷贝的方式,即拷贝该指针成员本身的同时,还要拷贝指针指向的内存资源。否则一旦多个对象中的指针成员指向同一块堆空间,这些对象析构时就会对该空间释放多次,这是不允许的。

执行结果如下:

construct!                <-- 执行 demo()
copy construct!       <-- 执行 return demo()
class destruct!         <-- 销毁 demo() 产生的匿名对象
copy construct!       <-- 执行 a = get_demo()
class destruct!         <-- 销毁 get_demo() 返回的临时对象
class destruct!         <-- 销毁 a

由结果可以得出,利用拷贝构造函数实现对 a 对象的初始化,底层实际上进行了 2 次深拷贝操作。

怎样才能避免深拷贝导致的效率问题呢?——移动语义

什么是移动语义?
所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。而移动构造函数就是现实移动语义的具体方式

以前面程序中的 demo 类为例,该类的成员都包含一个整形的指针成员,其默认指向的是容纳一个整形变量的堆空间。当使用 get_demo() 函数返回的临时对象初始化 a 时,我们只需要将临时对象的 num 指针直接浅拷贝给 a.num,然后修改该临时对象中 num 指针的指向(通常另其指向 NULL),这样就完成了 a.num 的初始化。

下面程序对 demo 类进行了修改:

#include <iostream>
using namespace std;
class demo{
public:
    demo():num(new int(0)){
        cout<<"construct!"<<endl;
    }
    demo(const demo &d):num(new int(*d.num)){
        cout<<"copy construct!"<<endl;
    }
    //借助右值引用添加移动构造函数
    demo(demo &&d):num(d.num){
        d.num = NULL;
        cout<<"move construct!"<<endl;
    }
    ~demo(){
        cout<<"class destruct!"<<endl;
    }
private:
    int *num;
};
demo get_demo(){
    return demo();
}
int main(){
    demo a = get_demo();
    return 0;
}

可以看到,在之前 demo 类的基础上,我们又手动为其添加了一个构造函数。和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 d.num,有效避免了“同一块对空间被释放多次”情况的发生。

输出结果为:

construct!
move construct!
class destruct!
move construct!
class destruct!
class destruct!

通过执行结果我们不难得知,当为 demo 类添加移动构造函数之后,使用临时对象初始化 a 对象过程中产生的 2 次拷贝操作,都转由移动构造函数完成。

临时对象既无名称也无法获取其存储地址,所以属于右值,可以初始化右值引用

结论:
当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数

我们在实际开发中,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。

move函数

上面讲到,当使用类的右值对象(可以理解为临时对象)初始化同类对象时,编译器会优先选择移动构造函数
那么,用当前类的左值对象(有名称,能获取其存储地址的实例对象)初始化同类对象时,是否就无法调用移动构造函数了呢?当然不是,C++11 标准中已经给出了解决方案,即调用 move() 函数。

move 的功能很简单,就是将某个左值强制转化为右值。

#include <iostream>
using namespace std;
class movedemo{
public:
    movedemo():num(new int(0)){
        cout<<"construct!"<<endl;
    }
    //拷贝构造函数
    movedemo(const movedemo &d):num(new int(*d.num)){
        cout<<"copy construct!"<<endl;
    }
    //移动构造函数
    movedemo(movedemo &&d):num(d.num){
        d.num = NULL;
        cout<<"move construct!"<<endl;
    }
public:     //这里应该是 private,使用 public 是为了更方便说明问题
    int *num;
};
int main(){
    movedemo demo;
    cout << "demo2:\n";
    movedemo demo2 = demo;
    //cout << *demo2.num << endl;   //可以执行
    cout << "demo3:\n";
    movedemo demo3 = std::move(demo);
    //此时 demo.num = NULL,因此下面代码会报运行时错误
    //cout << *demo.num << endl;
    return 0;
}

程序执行结果为:

construct!
demo2:
copy construct!
demo3:
move construct!

通过观察程序的输出结果,以及对比 demo2 和 demo3 初始化操作不难得知,demo 对象作为左值,直接用于初始化 demo2 对象,其底层调用的是拷贝构造函数;而通过调用 move() 函数可以得到 demo 对象的右值形式,用其初始化 demo3 对象,编译器会优先调用移动构造函数。

完美转发

什么是完美转发?
它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。

举个不完美转发的例子:

template<typename T>
void function(T t) {
    otherdef(t);
}

显然,function() 函数模板并没有实现完美转发。一方面,参数 t 为非引用类型,这意味着在调用 function() 函数时,实参将值传递给形参的过程就需要额外进行一次拷贝操作;另一方面,无论调用 function() 函数模板时传递给参数 t 的是左值还是右值,对于函数内部的参数 t 来说,它有自己的名称,也可以获取它的存储地址,因此它永远都是左值,也就是说,传递给 otherdef() 函数的参数 t 永远都是左值。总之,无论从那个角度看,function() 函数的定义都不“完美”。

怎么才能让上面的function() 函数模板实现完美转发呢?
C++11 标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)。
借助右值引用,在 C++11 标准中实现完美转发,只需要编写如下一个模板函数即可:

template <typename T>
void function(T&& t) { //参数 t 既可以接收左值,也可以接收右值。
    otherdef(t);
}

通过将函数模板的形参类型设置为 T&&,我们可以很好地解决接收左、右值的问题。
但除此之外,还需要解决一个问题,即无论传入的形参是左值还是右值,对于函数模板内部来说,形参既有名称又能寻址,因此它都是左值。那么如何才能将函数模板接收到的形参连同其左、右值属性,一起传递给被调用的函数呢?

C++11 标准的开发者已经帮我们想好的解决方案,该新标准还引入了一个模板函数 forword(),我们只需要调用该函数,就可以很方便地解决此问题。

实现完美转发的函数模板:

template <typename T>
void function(T&& t) {
    otherdef(forward<T>(t));
}

此 function() 模板函数才是实现完美转发的最终版本。可以看到,forword() 函数模板用于修饰被调用函数中需要维持参数左、右值属性的参数。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-06-29 18:48:02  更:2022-06-29 18:48:20 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 17:13:19-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码