【C++随笔02】左值和右值
- 一、左值和右值
- 1、字面理解——左值、右值
- 2、字面理解的问题
- 3、左值、右值
- 4、左值的特征
- 5、 右值的特征
- 6、x++和++x是左值还是右值
- 7、复合例子
- 8、通常字面量都是一个右值,除字符串字面量以外:
- 二、左值引用和右值引用
- 三、左值引用
- 1、常量左值引用
- 2、制构造函数和复制赋值运算符函数——左值引用
- 四、右值引用
- 1、移动语义和完美转发
- 2、移动语义
- 3、完美转发
- 3.1、简介、demo
- 3.2、问题:c++中的完美转发(std::forward)存在的意义?
- 简单总结
一、左值和右值
1、字面理解——左值、右值
最简单的字面理解,表达式等号左边的值为左值,等号右边的值为右值,比如
int x = 1;
int y = 3;
int z = x + y;
以上面的代码为例,
- x是左值,1是右值;
- y是左值,3是右值;
- z是左值,x+y的结果是右值。(注意,是结果)
2、字面理解的问题
在第一行代码中我们判断a是一个左值,它却在第二行变成了右值,所以这就是问题,要准确地区分左值和右值还是应该理解其内在含义。
3、左值、右值
在C++中
- 左值一般是指一个指向特定内存的具有名称的值(具名对象),它有一个相对稳定的内存地址,并且有一段较长的生命周期。
- 右值则是不指向稳定内存地址的匿名值(不具名对象)
简单来说,左值是一个有内存地址的表达式,而右值是一个没有内存地址的表达式。
基于这一特征,我们可以用取地址符&来判断左值和右值,能取到内存地址的值为左值,否则为右值。还是以上面的代码为例,因为&a和&b都是符合语法规则的,所以a和b都是左值。
4、左值的特征
左值具有以下特征:
- 左值在内存中有唯一的地址。
- 左值可以出现在赋值操作符的左边。
- 左值可以被取地址操作符(&)获取其内存地址。
- 左值可以作为函数参数或返回值。
int x = 10; // x是一个左值
int* ptr = &x; // 获取x的地址并赋给指针ptr
5、 右值的特征
右值具有以下特征:
- 右值没有内存地址。
- 右值不能作为赋值操作符的左边。
- 右值不能被取地址操作符获取其内存地址。
int y = 20; // 20是一个右值
int z = x + y; // 表达式x + y的结果是一个右值
6、x++和++x是左值还是右值
int *p = &x++; // 编译失败
int *q = &++x; // 编译成功
- x++是右值,因为在后置++操作中编译器首先会生成一份x值的临时复制,然后才对x递增,最后返回临时复制内容。
- ++x则不同,它是直接对x递增后马上返回其自身,所以++x是一个左值。
7、复合例子
int x = 1;
int get_val()
{
return x;
}
void set_val(int val)
{
x = val;
}
int main()
{
x++;
++x;
int y = get_val();
set_val(6);
}
- get_val函数,该函数返回了一个全局变量x,虽然很明显变量x是一个左值,但是它经过函数返回以后变成了一个右值。
原因和x++类似,在函数返回的时候编译器并不会返回x本身,而是返回x的临时复制,所以int * p = &get_val();也会编译失败。
- set_val函数,该函数接受一个参数并且将参数的值赋值到x中。在main函数中set_val(6);实参6是一个右值,但是进入函数之后形参val却变成了一个左值。
我们可以对val使用取地址符.
- 左值到右值的隐式转换
左值可以被隐式地转换为右值,例如在某些表达式中,需要右值而传入一个左值参数时,编译器会自动进行转换。
void printValue(int value)
{
cout << value << endl;
}
int x = 10;
printValue(x); // 编译器将x隐式地转换为右值传递给函数
8、通常字面量都是一个右值,除字符串字面量以外:
int x = 1;
set_val(6);
auto p = &“hello world”;
编译器会将字符串字面量存储到程序的数据段中,程序加载的时候也会为其开辟内存空间(rodata),所以我们可以使用取地址符&来获取字符串字面量的内存地址。
- 给大家写一个有编译错误的例子,大家调调
#include <iostream>
int add1(int& x)
{
return x+1;
}
int main() {
int a = add1(1);
std::cout << a << std::endl;
return 0;
}
二、左值引用和右值引用
void processValue(int& value) {
// 对左值进行处理
}
void processValue(int&& value) {
// 对右值进行处理
}
-
左值引用和右值引用在C++11中,引入了右值引用的概念。左值引用(L-value references)用于绑定到左值,右值引用(R-value references)用于绑定到右值。
-
左值引用的声明使用单个&符号:
int x = 10;
int& lvalueRef = x; // 左值引用绑定到左值x
- 右值引用的声明使用两个&&符号:
int y = 20;
int&& rvalueRef = y + 30; // 右值引用绑定到右值表达式的结果
引用可以方便地对变量进行修改或者将其传递给函数。
右值引用在移动语义(Move Semantics)和完美转发(Perfect Forwarding)中具有重要的作用,可以提高性能和代码效率。
三、左值引用
1、常量左值引用
常量左值引用的特性显得更加有趣,它除了能引用左值,还能够引用右值,比如:
int &x1 = 7; // 编译错误
const int &x = 11; // 编译成功
在上面的代码中,第一行代码会编译报错,因为int&无法绑定一个int类型的右值,但是第二行代码却可以编译成功。请注意,虽然在结果上const int &x = 11和const int x = 11是一样的,但是从语法上来说,前者是被引用了,所以语句结束后11的生命周期被延长,而后者当语句结束后右值11应该被销毁。虽然常量左值引用可以引用右值的这个特性在赋值表达式中看不出什么实用价值,但是在函数形参列表中却有着巨大的作用。一个典型的例子就是复制构造函数和复制赋值运算符函数,
2、制构造函数和复制赋值运算符函数——左值引用
当我们使用左值引用时,通常会涉及到复制构造函数和复制赋值运算符函数。复制构造函数用于创建一个新对象,并将其初始化为已存在的对象的副本。复制赋值运算符函数用于将一个已存在的对象的值复制给另一个已存在的对象。
以下是一个简单的示例程序,演示了左值引用、复制构造函数和复制赋值运算符函数的使用:
#include <iostream>
class MyObject {
private:
int data;
public:
MyObject(int d) : data(d) {
std::cout << "Constructor called with value: " << d << std::endl;
}
MyObject(const MyObject& other) : data(other.data) {
std::cout << "Copy constructor called. Copied value: " << data << std::endl;
}
MyObject& operator=(const MyObject& other) {
if (this != &other) {
data = other.data;
std::cout << "Copy assignment operator called. Copied value: " << data << std::endl;
}
return *this;
}
void printData() const {
std::cout << "Data: " << data << std::endl;
}
};
int main() {
MyObject obj1(42); // 调用构造函数
MyObject obj2(obj1); // 调用复制构造函数
MyObject obj3 = obj1; // 调用复制构造函数
MyObject obj4(55); // 调用构造函数
obj4 = obj1; // 调用复制赋值运算符函数
obj1.printData(); // 输出: Data: 42
obj2.printData(); // 输出: Data: 42
obj3.printData(); // 输出: Data: 42
obj4.printData(); // 输出: Data: 42
return 0;
}
输出
Constructor called with value: 42
Copy constructor called. Copied value: 42
Copy constructor called. Copied value: 42
Constructor called with value: 55
Copy assignment operator called. Copied value: 42
Data: 42
Data: 42
Data: 42
Data: 42
在上述示例中,我们首先定义了一个名为 MyObject 的类,该类具有一个带有整数参数的构造函数、一个复制构造函数和一个复制赋值运算符函数。然后我们创建了几个 MyObject 类型的对象,并通过不同方式进行初始化和赋值。
在 main() 函数中,我们创建了 obj1,并使用拷贝构造函数将其值分别复制给 obj2 和 obj3。接下来,我们创建了 obj4,然后使用赋值运算符将 obj1 的值复制给 obj4。
最后,我们调用各个对象的成员函数 printData() 来打印它们的数据值。你可以看到,obj2、obj3 和 obj4 的数据值都与 obj1 相同,这表明复制构造函数和复制赋值运算符函数成功地将一个对象的值复制给了另一个对象。
四、右值引用
顾名思义,右值引用是一种引用右值且只能引用右值的方法。在语法方面右值引用可以对比左值引用,在左值引用声明中,需要在类型后添加&,而右值引用则是在类型后添加&&,例如:
int i = 0;
int &j = i; // 左值引用
int &&k = 11; // 右值引用
在上面的代码中,k是一个右值引用,如果试图用k引用变量i,则会引起编译错误。右值引用的特点之一是可以延长右值的生命周期。
1、移动语义和完美转发
右值引用在移动语义和完美转发中起着重要的作用。
-
移动语义:移动语义是指通过右值引用将资源(如动态分配的内存、文件句柄等)从一个对象转移到另一个对象,而不是进行深拷贝。这样可以避免不必要的内存分配和释放,提高程序性能。
-
完美转发:完美转发是指将一个函数中的参数以原样传递给另一个函数,包括参数的左值或右值属性信息。通过使用右值引用和模板,可以实现完美转发,避免了不必要的拷贝。
2、移动语义
当涉及到移动语义和完美转发时,我们需要先了解一些基本概念和问题。在C++中,对象的拷贝构造函数(Copy Constructor)和拷贝赋值运算符(Copy Assignment Operator)会对资源进行拷贝操作,这可能导致内存分配和释放的开销。在某些情况下,我们希望能够高效地转移资源的所有权而不是进行深拷贝,这就引入了移动语义和完美转发。
移动语义(Move Semantics)
移动语义是指通过右值引用将资源(如动态分配的内存、文件句柄等)从一个对象转移到另一个对象,而不是进行深拷贝。移动语义可以大大提高程序性能,因为它避免了不必要的内存分配和释放。
移动语义的实现依赖于右值引用。右值引用(R-value Reference)通过双个&&符号进行声明,并且可以绑定到右值。通过移动构造函数(Move Constructor)和移动赋值运算符(Move Assignment Operator),可以使用移动语义来实现资源的高效转移。
简单来说,移动语义允许我们从一个临时的右值对象或者一个将要被销毁的对象中“窃取”资源,然后将其传递给新对象,而无需进行资源的复制操作。
示例代码:
#include <iostream>
class MyObject {
private:
int data;
public:
MyObject(int d = 0) : data(d) {
std::cout << "Constructor called with value: " << d << std::endl;
}
MyObject(const MyObject& other) : data(other.data) {
std::cout << "Copy constructor called. Copied value: " << data << std::endl;
}
MyObject& operator=(const MyObject& other) {
if (this != &other) {
data = other.data;
std::cout << "Copy assignment operator called. Copied value: " << data << std::endl;
}
return *this;
}
MyObject(MyObject&& other) noexcept : data(other.data) {
std::cout << "Move constructor called. Moved value: " << data << std::endl;
other.data = 0; // 清空原对象的值
}
MyObject& operator=(MyObject&& other) noexcept {
if (this != &other) {
data = other.data;
std::cout << "Move assignment operator called. Moved value: " << data << std::endl;
other.data = 0; // 清空原对象的值
}
return *this;
}
void printData() const {
std::cout << "Data: " << data << std::endl;
}
};
int main() {
MyObject obj1(42); // 调用构造函数
MyObject obj2(obj1); // 调用复制构造函数
MyObject obj3 = obj1; // 调用复制构造函数
MyObject obj4(55); // 调用构造函数
obj4 = obj1; // 调用复制赋值运算符函数
MyObject obj5(std::move(obj1)); // 调用移动构造函数
MyObject obj6 = std::move(obj2); // 调用移动赋值构造函数
obj1.printData(); // 输出: Data: 42
obj2.printData(); // 输出: Data: 0
obj3.printData(); // 输出: Data: 42
obj4.printData(); // 输出: Data: 42
obj5.printData(); // 输出: Data: 42
obj6.printData(); // 输出: Data: 42
return 0;
}
Constructor called with value: 42
Copy constructor called. Copied value: 42
Copy constructor called. Copied value: 42
Constructor called with value: 55
Copy assignment operator called. Copied value: 42
Move constructor called. Moved value: 42
Move constructor called. Moved value: 42
Data: 0
Data: 0
Data: 42
Data: 42
Data: 42
Data: 42
- obj1和obj2为0,这证明了移动构造函数被调用,并且资源成功被转移。
3、完美转发
3.1、简介、demo
完美转发(perfect forwarding)是C++11引入的概念,用于在函数模板中将参数按原样传递给另一个函数,同时保留其值类别(lvalue或rvalue)。它可以在保持精确性的同时避免不必要的复制或移动操作,提高代码的效率。
完美转发通常与转发引用类型参数(如模板中的万能引用)一起使用,以实现泛型编程中的参数传递。在函数模板中,我们可以使用 std::forward 函数来进行完美转发。
下面是一个使用完美转发的示例:
#include <iostream>
#include <utility>
// 定义一个接收传入参数的函数
void process(int& i) {
std::cout << "Lvalue: " << i << std::endl;
i = 100; // 修改传入的左值参数
}
void process(int&& i) {
std::cout << "Rvalue: " << i << std::endl;
}
// 将参数转发到 process 函数
template <typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
int main() {
int a = 42;
wrapper(a); // 传递左值
wrapper(123); // 传递右值
std::cout << "a: " << a << std::endl;
int b = 42;
process(b);
process(123);
std::cout << "b: " << a << std::endl;
return 0;
}
输出
Lvalue: 42
Rvalue: 123
a: 100
Lvalue: 42
Rvalue: 123
b: 100
在上述示例中,我们定义了两个重载的 process 函数,一个接受左值引用参数,一个接受右值引用参数。然后,我们创建了一个模板函数 wrapper,该函数接受一个通用引用类型的参数 T&& arg。
在 wrapper 函数内部,我们通过调用 std::forward 来进行完美转发,将 arg 参数原封不动地传递给 process 函数。std::forward 根据参数的值类别(lvalue还是rvalue)将参数作为对应类型的引用进行转发。
在 main 函数中,我们先传递了一个左值 x 给 wrapper 函数,然后传递了一个右值 123。程序会根据参数的值类别,选择调用合适的 process 函数,并输出相应的结果。
使用完美转发可以避免不必要的拷贝和移动操作,提高代码的效率和性能。
3.2、问题:c++中的完美转发(std::forward)存在的意义?
-
我们从上面的demo中可以看到,既然不使用forward也可以达到类似的效果,那为何还要使用完美转发(std::forward)呢?
-
说这个问题前,我们先把上面的一个demo改下
#include <iostream>
class MyObject {
private:
int data;
public:
MyObject(int d = 0) : data(d) {
std::cout << "Constructor called with value: " << d << std::endl;
}
MyObject(const MyObject& other) : data(other.data) {
std::cout << "Copy constructor called. Copied value: " << data << std::endl;
}
MyObject& operator=(const MyObject& other) {
if (this != &other) {
data = other.data;
std::cout << "Copy assignment operator called. Copied value: " << data << std::endl;
}
return *this;
}
MyObject(MyObject&& other) noexcept : data(other.data) {
std::cout << "Move constructor called. Moved value: " << data << std::endl;
other.data = 0; // 清空原对象的值
}
MyObject& operator=(MyObject&& other) noexcept {
if (this != &other) {
data = other.data;
std::cout << "Move assignment operator called. Moved value: " << data << std::endl;
other.data = 0; // 清空原对象的值
}
return *this;
}
void printData() const {
std::cout << "Data: " << data << std::endl;
}
};
template<typename T>
T* createObject(T&& t)
{
return new MyObject(t);
}
int main() {
MyObject* newMyObject = createObject(std::move(MyObject(110)));
return 0;
}
- 大家猜猜看,上面应该输出什么?应该调用移动构造函数的,对吧,实际情况输出如下,调用的却是拷贝构造函数。
Constructor called with value: 110
Copy constructor called. Copied value: 110
- 是不是很好奇,为啥调用的是拷贝构造函数,而非我们之前想象的移动构造函数呢?
我们把createObject做下简单的更改,增加std::forward,
template<typename T>
T* createObject(T&& t)
{
return new MyObject(std::forward<T>(t));
}
- 结果输出如下
Constructor called with value: 110
Move constructor called. Moved value: 110
这时,才真正的如我们所想,真的调用了移动构造函数。
- 分析:经历了模版参数t这一次转发,t右值的属性被改变为了左值。
简单总结
- 左值引用用于引用左值并允许修改,
- 右值引用用于引用右值并具有移动语义和完美转发的特性。