C++中的值类型
C++中表达式的结果有两个属性:类型和值类别(value categories),类型影响值的表示范围和所占的存储空间,值类别则影响
- 能否取地址和修改
- 引用的绑定规则
- 生命周期
等特性。分为以下几类
1. 左值 lvalue
具有存储位置的对象,可以被修改和获取,且在表达式结束后依然存在。简单地说,能够用&取地址的就是左值。包括
- 函数名
当我们将一个函数名作为值来使用时,它会自动转换成指向对应函数的指针 - 具名的变量 例如
std::cin,std::endl - 返回左值引用的函数调用
- 前置自增/自减运算符的表达式 例如
++i,--i - 由赋值运算符或复合赋值运算符连接的表达式 例如
a=b、a+=b、a%=b - 解引用表达式
- 字符串字面值 例如
"abac"
2. 纯右值 prvalue
不与对象的存储位置直接关联,无法获取地址且没有标识符(也就是不具名)的临时对象,包括
- 除了字符串字面量以外的字面量 例如3,
false - 返回非引用类型的函数调用
- 后置自增/自减运算符的表达式 例如
i++,i-- - 算术表达式
a+b,a&b - 逻辑表达
a&&b,a||n,~a - 比较表达式
a==b - 取址表达式
&a等
3. 将亡值 xvalue
在C++11之前的右值和C++11后的纯右值是等价的。C++11中的将亡值是随着右值引用T&&的引入而引入的。将亡值表达式就是下列表达式:
- 返回右值引用的函数的调用表达式
- 转换为右值引用的转换函数的调用表达式
之所以要引入右值引用和将亡值,是因为在C++中,当我们使用一个左值去初始化一个新对象或者给已有的对象赋值时,会调用拷贝构造函数或赋值运算符来拷贝资源。在C++11中,引入了移动构造函数和移动赋值运算符,当用右值来来初始化或赋值的话,这两个函数来实现,从而避免拷贝,提高效率。这个右值完成为左值初始化或赋值的任务后,它的资源已经被移动给了这个左值,其本身马上就会被析构。也就说,这个右值在被定义时,它就是“将亡”的了。
4. 广义左值和右值
广义左值包含左值和将亡值,右值包含纯右值和将亡值。
值类型的辨析
1. 字符串字面量是左值
字符串字面值是所有字面值中唯一的左值,而其他的字面值都是右值,这是因为早期间C++将字符串字面值实现为char类型的数组,为每个字符都分配了空间并允许进行操作。
1 | |
例如上面的代码,字符串字面量"abc"可以直接用来取地址和给指针赋值,p_char的值就是字符串首字母'a'的地址
2. 具名的右值引用是左值,不具名的右值引用是右值
1 | |
3. ++i和i++
++i是先把i加1再赋值给i,表达式返回的值就是i,因此它的结果是具名的,i在表达式结束后依然存在,因此++i是左值i++则是先对i进行拷贝,将拷贝的副本返回后再对i加1,由于返回的结果是i的拷贝,因此是不具名的,是纯右值
左值与右值的转换
可以使用std::move将左值转换成右值
1 | |
万能引用
当
T是一个模板参数,且需要进行类型推导,或者- 使用
auto &&声明变量
时,T&&或auto &&既可以绑定到左值也可以绑定到右值,称之为万能引用。
为了达成T&&或auto &&的上述功能,C++规定,当应用的引用出现时,会通过引用折叠简化为单一引用
& &→&& &&→&&& &→&&& &&→&&
例如
1 | |
完美转发
通过std::forward<>模板,我们能够实现完美转发,即在参数传递时保留其原始的值类型
1 | |
- 万能引用是参数绑定的入口,负责接收任意的值类别。
- 而
std::forward是转发的出口,负责恢复原始值的类别。以上面的代码为例,wrapper中的t无论接受的是左值还是右值,在wrapper内部都是左值,因为它是具名的,且在栈上分配了空间。而std::forward<T>(t)则能根据T是int(t传入的是右值)还是int &(t传入的是左值)来将t恢复成它原来的值类型。 - 之所以需要万能转发,是因为不管你传入的是右值还是左值,在函数内部,参数都是具名的,也就是说,在函数内部参数都是左值。如果这个函数需要调用其他的函数,而那个被调用的函数根据参数是左值还是右值,会有不同的行为的话,这个时候就需要用完美转发来恢复参数的值类型。