【C++】表达式

时间:2024-10-23 16:06:39

表达式

这部分内容对应于 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-namestatic_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无可厚非,但是在其它情况下不建议使用强制类型转换。