表达式
这部分内容对应于 C++ Primer 第五版的整个第四章。表达式应该是非常简单的内容了,但是在 C++ 的表达式当中有许多需要明确的细节,因此非常有必要进行学习记录。
基础
左值和右值
C++ 的表达式要么是左值(lvalue)要么是右值(rvalue)。可以做一个简单的归纳:当一个对象被用作右值的时候,用的是对象的值(内容);而当对象被用作左值的时候,使用的是它的身份(即对象在内存当中的地址)。
- 赋值运算符需要一个左值作为左侧运算的对象,所得到的结果仍然是左值;
- 取地址符作用于一个左值对象,返回一个指向对象地址的指针,这个指针是一个右值;
- 内置解引用符、下标运算符、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值;
- 内置类型和迭代器的递增递减运算符作用于左值对象(这是显而易见的,因为改变的是内置类型或迭代器的地址,这样才能实现迭代);
使用关键字decltype
的时候,左值和右值也是不同的。如果表达式的求值结果是左值(即得到一个对象的地址),decltype
作用于该表达式的时候得到的是一个引用类型。
例如,对于类型为int*
的变量p
,*p
是一个解引用操作,返回的是p
所指的对象,因此是一个左值,所以decltype(*p)
的结果是int&
。此外,由于取地址运算符生成的是右值,所以decltype(&p)
的结果是int**
,即一个指向整型指针的指针。
如何区分左值和右值呢?在这里我结合自己对这一部分的理解谈一谈我自身的感受。C++ Primer当中已经进行了归纳,左值指的是对象的身份,而右值指的是对象的内容,一个对象本身是一个左值,而它的值以及它地址的值则是右值,因此对于方才所说的情况,*p
是对整型指针使用解引用运算符,得到的是指针所指的对象,而不加解引用运算符的地址本身存储的才是指针所指对象的地址,因此*p
返回的是左值,即指针所指的对象本身;而&p
是对指针对象进行取地址操作,它返回的是一个右值,即指向对象p
的指针的地址,它是一个具有明确的类型的结果,因此使用decltype
可以直接推导出这个右值的类型,即指向整型指针的指针。
递增和递减运算符
在C语言的学习过程中我们就已经接触过了递增和递减运算符++/--
,它分为前置版本和后置版本,二者的区别如下:
- 递增/递减运算符的后置版本首先返回当前变量的值,再执行递增/递减操作;
- 递增/递减运算符的前置版本首先执行递增/递减操作,再返回当前变量的值。
C++ Primer 当中的建议是除非必须,否则不要使用递增/递减运算符的后置版本。
在一条语句当中混用解引用符和递增运算符
当我们想在复合表达式当中既将变量加1
或减1
又能使用它原来的值,此时就可以使用递增/递减运算符的后置版本。
以下是一个例子,在此例当中我们试图寻找vector
对象当中的第一个负值:
auto pbeg = v.begin();
while(pbeg != v.end() && *pbeg >= 0) {
cout << *pbeg ++ << endl; // 输出当前值并将迭代器 pbeg 向右移动一个元素
}
后置运算符的优先级高于解引用运算符,因此*pbeg++
等价于*(pbeg++)
。这条语句的含义是首先输出pbeg
指针所指向元素的值,再将迭代器pbeg
加一,等价于向右移动一个元素。
显式类型转换
有时我们希望显式地将对象强制地转换成另一种类型。在学习 C 语言的过程中我们已经知道,如果想要强制将整型t
转为double
类型,可以使用(double)t
来完成,在 C++ 当中出现了一些新方法。
命名强制类型转换
一个命名的强制类型转换具有如下形式:
const-name<type>(expression);
其中type
是转换的目标而expression
是要转换的值。如果type
是引用类型,则结果是左值。cast-name
是static_cast/dynamic_cast/const_cast/reinterpret_cast
当中的一种。dynamic_cast
支持运行时类型识别。
static_cast
任何具有明确定义的类型转换,只要不包含底层const
,都可以使用static_cast
。例如,通过将一个运算对象强制转换为double
类型就可以使表达式执行浮点数除法:
double slope = static_cast<double>(j) / i;
当需要把一个较大的算是类型赋值给较小的类型时,static_cast
非常有用。一般来说,如果编译器发现一个较大的算术类型试图赋值给较小的类型时,会发出警告,但是当我们执行了显式的类型转换之后,警告信息会关闭。
static_cast
对于编译器无法自动执行的类型转换也非常有用。例如,可以使用static_cast
找回存在于void*
当中的指针的值:
void *p = &d; // 正确: 任何非常量对象的地址都可以放入 void*
double *dp = static_cast<double*>(p);
const_cast
const_cast
只能改变对象的底层 const:
const char *pc; // 指向字符常量的指针 pc
char *p = const_cast<char*>(pc); // 通过 const_cast 将 pc 转为指向可变字符的指针
只有const_cast
可以改变表达式的常量属性,不能使用const_cast
改变表达式的类型:
const char *cp;
char *q = static_cast<char*> cp; // 错误, static_cast 不可以改变表达式的常量属性
static_cast<string> cp; // 正确: 将字符串的字面值转换为 string 类型
const_cast<string> cp; // 错误: const_cast 不可以改变表达式的类型, 只能改变其常量属性
const_cast
常常用于有函数重载的上下文。
reinterpret_cast
reinterpret_cast
通常为运算对象的位模式提供较低层次上的重新解释。它本质上依赖于机器。想要安全地使用reinterpret_cast
必须对涉及的类型和编译器实现转换的过程都非常了解。
对于上述的强制类型转换部分的内容,C++ Primer 给出的建议是尽可能地避免在程序中使用强制类型转换。在函数重载的上下文中使用const_cast
无可厚非,但是在其它情况下不建议使用强制类型转换。