C++11—右值引用

news/2025/2/3 11:34:35/

目录

简介 

左值和右值

左值

右值

右值引用

生命周期

引用折叠

实际应用

移动语义

移动构造函数

移动赋值运算符

完美转发


简介 

     之前我们曾学习过引用叫左值引用,但那是C++98的,在C++11中新增了一种引用叫右值引用。右值引用主要用于支持移动语义和完美转发。从而可以更高效地管理资源,减少不必要的复制操作。

左值和右值

左值

     左值是指可以取地址的表达式。它通常出现在赋值操作符左侧。

下面是它的几个特性:

  1. 可寻址性:左值在内存中有明确的地址,即可以取地址。
  2. 持久性:左值通常指向内存中的持久对象,这些对象在表达式结束后仍然存在。
  3. 可变性:左值可以被重新赋值,即可以改变其存储的值。

     例:

int a = 0;
int* p = new int[10];

     上面的a就是左值 ,因为它有持久的内存地址,并且可以被后续的代码访问和修改。*p同理。

右值

     右值是指不可以取地址的临时对象,它通常出现在表达式的结果。

下面是它的几个特性:

  1. 不可寻址性:右值通常没有持久的内存地址,即不可以取地址。
  2. 短暂性:右值通常表示的是一些临时的值,这些值在表达式结束后就会被销毁。

      例:

int a = 0;
int b = 1; 
int c = a + b;

     上面的0,1就是右值 ,因为它是一个字面常量,没有持久的内存地址。a + b也是右值,因为它是表达式的结果,没有持久的内存地址。

常见的右值有:

  1. 字面常量,如整数常量(如42)、字符常量('a')等
  2. 无引用的函数返回值。
  3. 表达式的结果,如算术表达式(如1+1)、逻辑表达式(如a||b)、关系表达式(如a>b)。

     想区分它们也很简单只要能取地址就是左值,不能取地址就是右值。 

右值引用

     右值引用的表示如下:

int&& a = 1;

     右值引用和左值引用没有什么区别都是起别名,从汇编层来看它们都是用指针来实现的。只不过一个是给右值起别名一个给左值。

     虽然说右值引用不能引用左值,但是可以通过move函数来实现。该函数可以让左值变右值,本质上是通过强制类型转换(使用该函数要小心原对象会变成不确定状态,原因一会儿在解答)。

int a = 1;
int&& ra = move(a);

同样的左值引用也可以引用右值在前面加const,即const左值引用。当const左值引用绑定右值时,编译器通常会创建一个临时对象,并将右值的内容复制到该临时对象中。又因为临时对象有只读性,被const修饰后的对象也只具有只读性。这是权限的平移,所以可以这样是OK的。

const int& a = 1;

     注意:一个右值被右值引用绑定后,右值引用变量表达式的属性是左值!就像上面的ra虽然它是一个右值引用,但它自身作为一个变量是左值。

生命周期

     大家可能发现了当一个临时的或匿名的对象被右值引用后,它好像不仅仅可以出现在该行,其它行好像也可以出现。事实上当一个对象被右值引用后它的生命周期就被延长了。不仅仅是右值引用,const左值引用的对象也是可以实现的只不过内容无法修改。例如:

int main()
{string&& str1 = string("ha");str1 += "ha";cout << "str1: " << str1 << endl;const string& str2 = string("xi");cout << "str2: " << str2 << endl;return 0;
}

引用折叠

     它的规则是:右值引用的右值引用折叠成右值引用,其他组合均折叠成左值引用。简单来说如果是两个都是右值引用那就折叠成右值引用,其他就都是左值引用。这主要是为了实现万能引用。示例:

template<class T>
void Test(T&& x)
{//...
}

     注意上面的参数虽然是通过右值引用传递的,因为它是一个模板所以这是一个万能引用。不太懂的可以先往下看。当用下面代码调用时:

Test(10);

      由于Test函数模板的参数是T&&,这里发生了模板参数推导即转发引用。简单来说当传递一个左值给接受T&&参数的函数模板时,编译器会推导T为左值引用类型;右值即非引用类型。

     在这种情况下,编译器会T推导为int(因为10是int类型),并且由于10是右值,T&&在这里实际上变成了int&&,即一个对int类型的右值引用。这其实并没有用引用折叠。

     用下面的代码调用时:

int a = 10;
Test(a);

    调用 Test 函数并将a作为参数传递进去。由于传递的是左值, T会被推导为int&,这就变为了int& && 然后根据引用折叠规则最后就变为了int&,即一个对int类型的左值引用。

     这样一份代码就可以实现左值引用和右值引用,是不是方便了不少。

实际应用

移动语义

     移动语义指的是通过移动构造函数和移动赋值运算符,将资源的所有权从一个对象转移到另一个对象,即新对象直接管理原对象的内存资源,原对象仍然处于有效状态,但不再持有任何资源。它们的主要目的是优化资源管理和提升程序性能。

移动构造函数

     可以简单认为移动构造函数的实现是通过下面的方式:

void swap(string& s)
{std::swap(_str, s._str);//首元素地址std::swap(_size, s._size);//字符串长度std::swap(_capacity, s._capacity);//容量
}
// 移动构造 
string(string&& rs)
{swap(rs);
}

     rs是右值引用但它的属性是左值,所以在swap()里用的是左值引用。

     示例: 

string Test()
{string str("Happy New Year");//...return str;
}int main()
{string ret = Test();return 0;
}

     不需要关心上面的函数是用来做什么的,这里我就只拿关键的部分。之前要想把str的值传给ret就要先创建一个临时对象,然后把str里的值拷贝构造给临时对象,然后临时对象在拷贝构造给ret。

     使用两次拷贝构造这代价可不小,要是使用移动语义的话可就不一样了。移动语义相当于“掠夺”并不进行拷贝,这代价就很小了。

移动赋值运算符

     它的简单实现和移动构造函数可以说是一样的。

// 移动赋值 
string& operator=(string&& s)
{swap(s);return *this;
}

     示例:

string Test()
{string str("Happy New Year");//...return str;
}int main()
{string ret;ret = Test();return 0;
}

     之前要想把str的值传给ret就要先创建一个临时对象,然后把str里的值拷贝构造给临时对象,然后临时对象在拷贝赋值给ret。 

     当有了移动语义后就只是进行简单交换。

     上面我所介绍的str值传给ret的过程,是编译器没有进行任何优化。我在这里就不介绍编译器具体怎么优化它们了,大家感兴趣可以自己上网查查。 

     现在我解答一下为什么使用move()后,原对象通常处于不确定状态。这是因为当move()把对象转换为右值后,会自动调用移动语义而造成的。例如:

int main()
{string s("haha");cout << "s:" << s << endl;string rs = move(s);cout << "s:" << s << endl;return 0;
}

完美转发

     完美转发在C++编程中具有重要意义,它提高了代码的复用性、性能和可维护性。先看如下代码:

void Fun(int&& x)
{cout << "右值引用" << endl;
}void Fun(int& x)
{cout << "左值引用" << endl;
}template<class T>
void Test(T&& x)
{Fun(x);
}int main()
{int a = 1;Test(a);Test(1);return 0;
}

     这是运行结果: 

     为什么第二个也是左值引用?我在上面提到过右值引用它自身作为一个变量是左值,所以这里就是左值引用。但是这样会降低运行效率,其解决方案就是用完美转发forward函数,它能够保持参数的左、右值属性不变,并将其转发给目标函数。具体使用方法如下:

template<class T>
void Test(T&& x)
{Fun(forward<T>(x));
}

     修改后的结果: 

     完美转发forward本质是⼀个函数模板,他主要还是通过引用折叠的方式实现,上面示例中传递给Func的实参是右值,T被推导为int,没有折叠,forward内部 x 被强转为右值引用返回。当传递给Func的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部 x 被强转为左值引用返回。


http://www.ppmy.cn/news/1568939.html

相关文章

LeetCode:474.一和零

跟着carl学算法&#xff0c;本系列博客仅做个人记录&#xff0c;建议大家都去看carl本人的博客&#xff0c;写的真的很好的&#xff01; 代码随想录 LeetCode&#xff1a;474.一和零 给你一个二进制字符串数组 strs 和两个整数 m 和 n 。 请你找出并返回 strs 的最大子集的长度…

【赵渝强老师】K8s中Pod探针的TCPSocketAction

在K8s集群中&#xff0c;当Pod处于运行状态时&#xff0c;kubelet通过使用探针&#xff08;Probe&#xff09;对容器的健康状态执行检查和诊断。K8s支持三种不同类型的探针&#xff0c;分别是&#xff1a;livenessProbe&#xff08;存活探针&#xff09;、readinessProbe&#…

《LLM大语言模型+RAG实战+Langchain+ChatGLM-4+Transformer》

文章目录 Langchain的定义Langchain的组成三个核心组件实现整个核心组成部分 为什么要使用LangchainLangchain的底层原理Langchain实战操作LangSmithLangChain调用LLM安装openAI库-国内镜像源代码运行结果小结 使用Langchain的提示模板部署Langchain程序安装langserve代码请求格…

【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】2.4 索引优化:避免意外复制的高效技巧

2.4 索引优化&#xff1a;避免意外复制的高效技巧 目录/提纲 #mermaid-svg-hmMAIqF8kFh46fbH {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-hmMAIqF8kFh46fbH .error-icon{fill:#552222;}#mermaid-svg-hmMAIqF8kF…

深度学习查漏补缺:2. 三个指标和注意力机制

一、bachsize, num_epochs, dataset 在训练卷积神经网络&#xff08;CNN&#xff09;或任何其他深度学习模型时&#xff0c;有几个关键参数和概念需要了解&#xff1a;batch size、num epochs 和 dataset。下面是对它们的详细解释&#xff1a; Batch Size&#xff08;批量大小&…

设计模式Python版 抽象工厂模式

文章目录 前言一、抽象工厂模式二、抽象工厂模式示例三、抽象工厂模式在Django框架中的应用 前言 GOF设计模式分三大类&#xff1a; 创建型模式&#xff1a;关注对象的创建过程&#xff0c;包括单例模式、简单工厂模式、工厂方法模式、抽象工厂模式、原型模式和建造者模式。结…

NLP自然语言处理通识

目录 ELMO 一、ELMo的核心设计理念 1. 静态词向量的局限性 2. 动态上下文嵌入的核心思想 3. 层次化特征提取 二、ELMo的模型结构与技术逻辑 1. 双向语言模型&#xff08;BiLM&#xff09; 2. 多层LSTM的层次化表示 三、ELMo的运行过程 1. 预训练阶段 2. 下游任务微调 四、ELMo的…

Microsoft Power BI:融合 AI 的文本分析

Microsoft Power BI 是微软推出的一款功能强大的商业智能工具&#xff0c;旨在帮助用户从各种数据源中提取、分析和可视化数据&#xff0c;以支持业务决策和洞察。以下是关于 Power BI 的深度介绍&#xff1a; 1. 核心功能与特点 Power BI 提供了全面的数据分析和可视化功能&…