首页 > 编程语言 >【C++ Primer Plus】C++11 深入理解右值、右值引用和完美转发

【C++ Primer Plus】C++11 深入理解右值、右值引用和完美转发

时间:2023-12-02 14:33:05浏览次数:26  
标签:11 右值 int 左值 C++ Useless 构造函数 引用

1. 右值引用和移动语义

1.1 左值和右值

  • 左值 local value:存储在内存中、有明确存储地址(可寻址)的数据(x、y、z)
  • 右值 read value:不一定可以寻址,例如存储于寄存器中的数据;通常字面量都是右值,除了字符串常量(1、3)
int x = 1;
int y = 3;
int z = x + y;

  对于x++和++x虽然都是自增操作,但x++编译器首先生成一份临时值,然后对x自增,最后返回临时内容,所以x++是右值;++x是对x递增后返回自身,所以++x是左值

x++;    // 右值引用
++x;    // 左值引用
int *p1 = &x++;    // 右值引用所以编译失败
int *p2 = &++x;    // 左值引用可以编译成功

1.2 左值引用和右值引用

  • 左值引用:必须引用一个左值。
  • 常量左值引用:可以引用左值或右值。因为右值的生命周期被延长了,但这种引用存在一个问题,常量左值引用导致无法修改对象内容。 
int &x1 = 7;         // 非常量左值引用 编译错误
const int &x = 11;   // 语句结束后,11的生命周期被延长了
const int x = 11;    // 语句结束后,11立刻被销毁
  • 右值引用:引用右值且只能引用右值的方法
int &&k = 11;    // 右值引用(延长右值的生命周期)

   对于数字的表示可能不太清晰,因为数字本身就有些虚无缥缈,下面用一个类的例子来更好解释右值引用的优势,可以减少复制构造来优化性能(但实际上编译器会帮我们优化)

  • 优化前:MyClass&& mc = make_myclass(); 调用后
    • ctor: 调用mc的构造函数
    • copy ctor:返回值时调用mc的拷贝函数
    • dtor: 返回值后将mc销毁调用析构函数(注意这里的复制构造函数会存在问题:如果是浅复制,此时销毁的对象会把堆区内存销毁导致新的对象空引用,所以还是强调必须重写复制构造函数)
    • 由于右值引用,延长了右值的生命周期
    • dtor: main函数结束再调用一次析构函数

  • 编译器优化后:MyClass&& mc = make_myclass(); 调用后
    • ctor: 调用构造函数
    • dtor: 调用析构函数

  • MyClass mc = make_myclass(); 该函数调用后
    • ctor: 调用mc的构造函数
    • copy ctor: 返回值 调用mc的拷贝函数
    • dtor: 返回值后销毁mc 调用析构函数
    • copy ctor: 为了构造mc2 调用构造函数
    • dtor: 销毁返回值临时变量 调用析构函数
    • dtor: main函数结束后再调用一次析构函数
#include <iostream>
using namespace std;
class MyClass {
public:
    char* pc;
    MyClass();
    MyClass(const MyClass& myclass);
    ~MyClass() { cout << "dtor" << endl; delete pc; }
    void show() { cout << "Show: " << pc << endl; }
};

MyClass::MyClass() {
    cout << "ctor" << endl;
    pc = new char[10];
    for (int i = 0; i < 10; i++)
        pc[i] = 'c';
}

MyClass::MyClass(const MyClass& myclass) {
    cout << "copy ctor" << endl;
    pc = myclass.pc;
    for (int i = 0; i < 10; i++)
        pc[i] = myclass.pc[i];
}

MyClass make_myclass() {
    MyClass mc;    // 1.构造函数     3.析构函数(此时如果是浅复制,新的对象指向的内存也将为空
    return mc;     // 2.拷贝函数
}

int main() {
    MyClass&& mc = make_myclass();    // 返回值的生命周期被延长
    mc.show();
    cout << endl;
    MyClass mc2 = make_myclass();    // 再次调用拷贝构造函数
    mc2.show();
}

1.3 移动语义

  上面其实已经用到了移动语义,移动语义主要就是解决C++复制构造对性能的影响。但也存在问题,例如移动构造函数运行过程中发生了异常,这会造成源对象和目标对象都不完整。这里再用一个例子说明,该Useless类内有一个元素个数为 n 的 char 数组,静态变量 ct 记录了对象个数。

  • Useless one(20, 'o'); 调用 int char 构造函数
  • Useless one(20, 'c'); 调用 int char 构造函数
  • Useless three(one + two);
    • one + two 调用 operator+ 运算符重载,在内部调用 int 构造函数构造了对象 temp
    • 返回值后调用移动语义构造函数,夺走 temp 里指针指向的内容并把它的指针设置为空,这样它在销毁时不会把堆区内存清空
    • 临时对象 temp 被销毁
class Useless {
public:
    int n;            // 元素个数
    char* pc;        // 数据指针
    static int ct;    // 对象个数
    void ShowObject()const;
    Useless(int k);
    Useless(int k, char ch);
    Useless(Useless&& f);        // 移动构造
    ~Useless();
    Useless operator+(const Useless& f)const;
    void ShowData() const;
};
int Useless::ct = 0;
Useless::Useless(int k) :n(k) {
    printf("int 参数的构造函数; 对象个数为: %d\n", ++ct);
    pc = new char[n];
    ShowObject();
}
Useless::Useless(int k, char ch) :n(k) {
    printf("int char参数的构造函数; 对象个数为: %d\n", ++ct);
    pc = new char[n];
    for (int i = 0; i < n; i++){
        pc[i] = ch;
    }
    ShowObject();
}
Useless::Useless(Useless&& f) :n(f.n) {
    printf("移动构造函数; 对象个数为: %d\n", ++ct);
    pc = f.pc;
    f.pc = nullptr;
    f.n = 0;
    ShowObject();
}

Useless::~Useless() {
    printf("析构函数调用; 元素个数为: %d\n", --ct);
    ShowObject();
    delete[] pc;
}

Useless Useless::operator+(const Useless& f)const {
    printf("进入 operator+\n");
    Useless temp = Useless(n + f.n);
    for (int i = 0; i < n; i++)
        temp.pc[i] = pc[i];
    for (int i = n; i < temp.n; i++)
        temp.pc[i] = f.pc[i - n];
    printf("离开 operator+\n");
    return temp;
}

void Useless::ShowObject() const {
    printf("元素个数: %d, 数据地址: %x\n", n, (void*)pc);
}

void Useless::ShowData()const {
    if (n == 0)
        printf("元素个数为空\n");
    else
        for (int i = 0; i < n; i++)
            printf("%c ", pc[i]);
    printf("\n");
}

int main()
{
    Useless one(20, 'o');    // int char 构造函数 对象个数1
    printf("\n");
    Useless two(20, 'c');    // int char 构造函数 对象个数2
    printf("\n");
    Useless three(one + two);    // 1. operator+ 调用 int 构造函数 对象个数3;  2. operator+ 返回右值, 调用移动构造函数(减少了复制的次数) 对象个数4;  3.临时对象被销毁 对象个数3
    printf("\n");
    printf("object one: \n");
    one.ShowData();
    printf("object two: \n");
    two.ShowData();
    printf("object three: \n");
    three.ShowData();
    printf("\n");
}

1.4 强制移动

  移动构造函数和移动赋值运算符都必须使用右值,但如果让他们使用左值就需要一些特殊处理

Useless choices[10];
Useless best;
int pick = 5;
best = chioces[pick];    // 由于这里是左值,所以会调用普通的复制赋值运算符

  可以使用C++11头文件utility中提供的move函数来实现将左值转换为右值,但是注意右值的字段会被夺走,并且必须定义了移动赋值运算符或移动构造函数

#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main()
{
    std::string str = "Hello";
    std::vector<std::string> v;
    //调用常规的拷贝构造函数,新建字符数组,拷贝数据
    v.push_back(str);
    std::cout << "After copy, str is \"" << str << "\"\n";
    //调用移动构造函数,掏空str,掏空后,最好不要使用str
    v.push_back(std::move(str));
    std::cout << "After move, str is \"" << str << "\"\n";
    std::cout << v[0] << ", " << v[1] << "\n";
}

2. 万能引用

  很多时候我们希望传递的是一个引用而非通过拷贝构造传递,这可以提高程序效率;但仅仅通过fn(className& c)来传递引用会导致不能传递右值,fn(const className& c)又会导致传递进来的参数不能被修改,所以提出了万能引用的概念

2.1 引用折叠

  万能引用实际上就是发生了类型推导,如果源对象是一个左值,则推导出左值引用;如果源对象是一个右值,则推导出右值引用。

void foo(int &&i) {}    // i为右值引用
template<class T> void bar(T &&t) {}    // t为万能引用
template<class T> void bar(vector<T> &&t) {}    // 非万能引用,必须是直接的T
int get_val() {return 5;}
int &&x = get_val();    // x 为右值引用
auto &&y = get_val();   // y 为万能引用

  C++11 通过一套引用叠加推导的规则来实现万能引用——引用折叠,可以注意到实际类型是左值引用,则最终类型一定是左值引用;只有引用类型是一个非引用类型或者右值引用,最后推导出来的才是一个右值引用

  通过下面几行代码理解引用折叠,首先是C++11规定的展开时的定义

  • 实参类型为T的左值, 则模板T展开为T& int => int&(T)
    • 此时Test形参的类型为 T& &&,经过折叠后为 T& 左值引用
    • 此时static_cast<T&>(t) 将t转为左值引用,所以调用左值引用的函数
  • 实参类型为T的右值, 则模板T展开为T  int => int(T)
    • 此时Test形参的类型为 T &&,所以为右值引用
    • 此时static_cast<T&&>(t) 将t转为右值引用,所以调用右值引用的函数
#include <iostream>
void process(int& i) {
    std::cout << "左值引用" << std::endl;
}
void process(int&& i) {
    std::cout << "右值引用" << std::endl;
}
template<class T>
void Test(T&& t) {
    process(static_cast<T&&>(t));
}
int main() {
    int a = 1;
    Test(a);    // C++11规定 实参类型为T的左值, 则模板T展开为T& int => int&(T)
    Test(1);    // C++11规定 实参类型为T的右值, 则模板T展开为T  int => int(T)
}

2.2 完美转发

  通过 std::forward<T>() 可以实现完美转发,不论左值还是右值都可以通过引用的方式传参,提高程序运行的效率。下面给出了一个完美转发的例子,打印了 T 的实际类型,并通过修改 t 的值实现了修改 a 的值(传入左值即左值引用),同样如果传入类的右值一样是右值引用。

#include <iostream>
template<class T>
void show_Type(T&& t) {
    std::cout << "is int&: " << std::is_same_v<T, int&> << std::endl;
    std::cout << "is int : " << std::is_same_v<T, int> << std::endl;
    t = 10;
}

template<class T>
void perfect_forwarding(T&& t) {
    show_Type(std::forward<T>(t));
}

int main()
{
    int a = 5;
    perfect_forwarding(5);    // 该参数在不同函数间始终以右值引用方式传递
    perfect_forwarding(a);    // 该参数在不同函数间始终以左值引用方式传递
    std::cout << a;           // 由于以引用的方式传递, 这里内存中的数值也一样会修改
}

 

标签:11,右值,int,左值,C++,Useless,构造函数,引用
From: https://www.cnblogs.com/stux/p/17867869.html

相关文章

  • 【运算符和表达式】关系运算符 -C语言-2023/11/29
    //比较后会返回两种结果:用数字0意思为“假”,用数字1意思为“真”。这里强调”意思为“。比如:我写个6>8<9这样一个式子结果显示1意思为真因为这里计算机先执行6>8结果为0,变为0之后再执行0<9变为1意思为真. ......
  • C++多线程编程:利用线程提高程序并发性
    C++多线程编程:利用线程提高程序并发性引言在现代计算机系统中,程序的并发性已经变得越来越重要。多线程编程是一种利用计算机的多核处理器来提高程序性能的方法。C++是一种功能强大的编程语言,提供了丰富的多线程编程支持。本文将介绍如何利用C++多线程编程来提高程序的并发性。什么......
  • [LeetCode Hot 100] LeetCode11. 盛最多的水
    题目描述方法一:暴力,超出时间限制模拟所有情况,记录最大的体积值。体积=Math.min(height[i],height[j])*(j-i)classSolution{publicintmaxArea(int[]height){intres=Integer.MIN_VALUE;for(inti=0;i<height.length;i++){......
  • linux11.29课堂随笔
    第九章文件查找、打包压缩及解压一、文件查找1.echo命令可以查看PATH的值 echo$PATH2.locate命令可以让用户快速查找到所需要的文件或目录,它不搜索全部信息,而是搜索数据库3.find命令搜索速度较慢,并不会索引目录,而是对整个目录进行遍历,会占用很多资源find命令可以根据文件......
  • 2023-2024-1 20232311 《网络空间安全导论》 第四周学习
    教材学习内容总结第四章思维导图教材学习中的问题和解决过程问题1:网络空间生态系统的未来变化趋势有哪些?我国做出哪些应对?问题1解决方案:询问Chatgpt:人工智能和自动化技术会进一步发展,对网络空间的影响将进一步扩大和加深。例如,人工智能对于网络安全和隐私保护的作用将变......
  • 【POJ 1144】Network 题解(Tarjan算法求无向图的割点)
    一家电话线公司(TLC)正在建立一个新的电话电缆网络。它们连接由1到N的整数编号的几个位置。没有两个地方的数字相同。这些线路是双向的,总是连接在两个地方,在每一个地方,线路都以电话交换机结束。每个地方都有一个电话交换机。从每个地方可以通过其他地方的线路到达,但不需要直接连接,可......
  • C++入门:掌握基本语法和面向对象编程
    C++入门:掌握基本语法和面向对象编程C++是一种通用的、高级的编程语言,广泛应用于开发各种应用程序。对于初学者来说,掌握C++的基本语法和面向对象编程是一个重要的起点。本篇博客将介绍C++的基本语法和面向对象编程的基本概念。了解C++的基本语法注释在C++中,你可以使用两种方式添加注......
  • C++聊天集群服务器5
    一、服务器异常处理函数​ 这部分主要处理服务器异常退出时,用户的在线状态还是online不会改变,因此需要修改。由于是需要对用户进行操作,因此我们在user表的数据操作类添加重置用户状态函数。​ 在usermodel.hpp添加后:#ifndefUSERMODEL_#defineUSERMODEL_#include"user.hpp......
  • 20211326学习笔记12
    第十四章数据库系统一、知识点归纳(一)MySQL简介MySQL(MySQL2018)是一个关系数据库系统(Codd1970)c在关系数据库中,数据存储在表中。每个表由多个行和列组成。表中的数据相互关联。表也可能与其他表有关联。关系结构使得可在表上运行查询来检索信息并修改数据库中的数据。关系......
  • Vmware 安装 Windows11 的一些注意事项
    1概述在使用Vmware进行安装Windows11操作系统时,发现没有安装Windows其他版本那么顺利,本文记录一下安装过程中的几个坑。2详情1)Vmware的访问控制默认是不加密的,安装Windows11时,必须为其设置密码进行加密,设置的这个密码需要记住,以后再进入虚机的时候需要输入此密码。......