C++值类型
- 纯右值,通常是用于初始化对象使用的字面值,或者是某个运算式的结果,不可以取地址,不可以被赋值,不具备标识符
- 左值,有标识符可以取地址,可以放在赋值号左侧的,需要注意函数在C++中被认为是左值,函数可以被取地址,尽管不能被赋值。而C语言中函数既非左值也非右值
- 将亡值,指一个即将被销毁,但是仍然可以被使用的对象,将亡值一般和std::move的使用和函数右值引用的返回相关,不能取地址,但是有地址
- 广义左值,有地址的函数或变量,要么是左值要么是将亡值
- 右值,所有不是左值的都是右值,即不能取地址,没有标识符,只能出现在赋值号右侧,因此将亡值也算是右值
引用
- 左值引用:
- 本质是一级指针常量。
- 对左值的引用,定义时初始化,不能改变引用方向,同一个变量可以有多个引用,通过引用可以修改值
- 通常用于优化函数参数传递和返回值,注意返回值不能是局部变量(但可以是局部static变量,生存期长于函数),其生存期在函数栈回收后结束,返回对局部变量的引用可能导致严重的问题。
- 只能引用基础类型相同的变量,如int a = 10; int &b = a;
- const引用可以引用非const变量,非const引用不能引用const变量,const引用可以引用字面值,const引用也可以引用基础类型不同的变量,但其实引用的只是一个临时变量,例如double a = 1.2f; const int &b = a,这里编译器会首先对a做强制类型转换得到一个4B的临时整型变量,b引用的实际是这个临时值
- 右值引用:
- 对右值的引用,定义初始化,不能改变方向,引用的对象只能是字面值常量
std::move
- std::move能够将左值转换为右值(将亡值)
- std::move可以触发移动语义,减少拷贝造成的时空消耗
实验
- 了解左值引用的构成
int main() {
int a = 10;
int &ref_a = a;
ref_a = 20;
return 0;
}
1 movl $10, -20(%rbp)
2 leaq -20(%rbp), %rax
3 movq %rax, -16(%rbp)
4 movq -16(%rbp), %rax
5 movl $20, (%rax)
- 对比汇编代码:
- 1 左值a的初始化,a的地址位于栈底指针指向-20B的位置,将整数10放到该位置
- 2,3 左值引用ref_a的初始化,其中2将取出a的地址暂存到rax寄存器,3负责将寄存器值放到栈底指针指向-16的位置,因此左值引用本质是一个一级指针,并且由于左值引用不能修改指向,所以还是一个指针常量
- 了解右值引用的构成
int main() {
int &&rref_ = 30;
rref_ = 40;
std::cout << std::hex << &rref_;
return 0;
}
1 movl $30, -20(%rbp)
2 leaq -20(%rbp), %rax
3 movq %rax, -16(%rbp)
4 movq -16(%rbp), %rax
5 movl $40, (%rax)
- 对比汇编代码:
- 1,2,3共同构成右值引用的初始化过程
- 1将被右值引用的字面常量值(纯右值)移动到栈底指针-20B的位置
- 2,3负责取出地址暂存到rax寄存器然后转到栈底指针-16B的位置
- 因此右值引用实际会先将一个右值存放到栈区的某一个位置,然后将右值引用变量指向该地址实现引用
- 4,5则体现右值引用的值可以被改变,汇编代码显示通过将右值引用存放的值,即字面值30目前所在地址,移动到rax寄存器,然后通过rax寄存器间接访问到30,并替换其为40
- 常量左值引用
- 尝试用claude生成了一个验证代码
- 正常来说左值引用本身是只能引用基础类型相同的变量,不能引用不同基础类型的变量,但常量左值引用可以,这里旨在搞清楚为什么
#include <iostream>
int main() {
double a = 1.0;
// 把 double 绑定到 const int&,隐式类型转换产生一个临时 int,
// 引用实际指向这个临时值,而非 double 变量 a 本身
const int &ref_a = a;
std::cout << "--- 初始状态 ---" << '\n';
std::cout << "a = " << a << '\n';
std::cout << "ref_a = " << ref_a << '\n';
// 类型不同:引用指向的是 int,不是 double
std::cout << "sizeof(a) = " << sizeof(a) << " 字节" << '\n';
std::cout << "sizeof(ref_a) = " << sizeof(ref_a) << " 字节" << '\n';
// 地址不同:&a 是 double 的地址,&ref_a 是临时 int 的地址
std::cout << std::hex;
std::cout << "&a = " << &a << '\n';
std::cout << "&ref_a = " << static_cast<const void*>(&ref_a) << '\n';
std::cout << std::dec;
// 修改 a,ref_a 不变 —— 最直接证据:
// 若 ref_a 引用 a 本身,改 a 后 ref_a 应同步变化;
// 实际仍保持转换时的旧值,说明它引用的是独立临时值
a = 2.0;
std::cout << "--- a = 2.0 之后 ---" << '\n';
std::cout << "a = " << a << '\n';
std::cout << "ref_a = " << ref_a << '\n';
return 0;
}
--- 初始状态 ---
a = 1
ref_a = 1
sizeof(a) = 8 字节
sizeof(ref_a) = 4 字节
&a = 0x7fff75b0dc98
&ref_a = 0x7fff75b0dc94
--- a = 2.0 之后 ---
a = 2
ref_a = 1
- 运行结果说明:
- 常量左值引用所引用的实际上是一个临时值而不是浮点变量本身,这个临时值由浮点值a强制类型转换时在栈区的某个位置存放下来,然后由常量左值引用负责指向该地址
- std::move作用于基础类型
int main() {
int a = 10;
int &&ref_a = std::move(a);
ref_a = 20;
std::cout << a << std::endl;
std::cout << ref_a << std::endl;
return 0;
}
a = 20
ref_a = 20
1 movl $10, -20(%rbp)
2 leaq -20(%rbp), %rax
3 movq %rax, %rdi
4 call _ZSt4moveIRiEONSt16remove_referenceIT_E4typeEOS2_
5 movq %rax, -16(%rbp)
6 movq -16(%rbp), %rax
7 movl $20, (%rax)
- 结果分析:
- 汇编代码中1,2实现的分别是a的初始化,和取a的地址,随后3,4负责调用std::move
- 5将a的地址存放到rbp指针-16B的位置,即对右值引用的初始化
- 6,7负责通过右值引用赋值修改了a的值
- 这说明std::move本身其实并没有修改a的底层,而只是将它转为右值,让编译器认为这是一个右值(否则编译器无法将右值引用绑定到左值上 int &&ref_a = a;是一个语法错误),事实上ref_a绑定的仍然是a的实际地址,最后的输出结果也说明了这一点