C++ Primer - 表达式

《C++ Primer》第五版引入了11标准相关内容,我早年在初学C++时还只有第四版,近来想对C++做一个整体的复习+版本升级,借此机会过一遍第五版。本文是阅读第四章“表达式”时所做的笔记。

C++ Primer - 表达式

基础

表达式(expression)由一个或多个运算对象(operand)组成,对表达式求值将得到一个结果(result)。字面值和变量是最简单的表达式,其结果就是字面值和变量的值。把一个运算符和一个或多个运算对象组合起来可以生成较复杂的表达式。

C++有多个一元运算符、二元运算符以及一个三元运算符。函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。

有些符号意义不单一,比如*,既可以作为一元运算符执行解引用,也可以作为二元运算符执行乘法运算。符号的作用取决于上下文。

优先级和结合性

表达式的求值结果依赖于运算符的优先级和结合性以及运算对象的求值顺序。C中老生长谈的东西了。

###运算对象转换

表达式求值经常伴随着运算对象类型转换。

特别的事,表达式求值过程中,小整数类型(如bool、char、short等)通常会被提升(promoted)为较大的整数类型,主要是int。

###重载运算符

C++定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。当运算符作用于类类型的运算对象时,用户可以自定义其含义,这被称作运算符重载。

重载运算符可以重新定义对象类型和返回值类型,但运算符的优先级和结合性无法改变。

左值和右值

简单来说,左值可以放在赋值语句左侧,右值不行。

左值表达式的结果是一个对象或函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。此外,虽然某些表达式求值结果是对象,但它们是右值而非左值。

当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值时,用的是对象的地址。需要右值的地方可以用左值代替,反之则不行。

几种需要左值的情景:

  • 赋值运算符需要一个非常量左值作为其左侧运算对象,返回结果也是一个左值。
  • 取地址符作用于左值运算对象,返回指向该运算对象的指针,该指针是一个右值。
  • 内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符都返回左值。
  • 内置类型和迭代器的递增递减运算符作用于左值运算对象。前置版本返回左值,后置版本返回右值。

左值和右值对decltype有所影响:

如果表达式求值结果是左值,decltype作用于该表达式得到一个引用类型。比如p是int*,那么decltype(*p)得到的是int&。而如果生成右值,则decltype(&p)结果是int**,是一个指向整型指针的指针。

算术运算符

运算符 功能 用法
+ 一元正号 + expr
- 一元负号 - expr
* 乘法 expr * expr
/ 除法 expr / expr
% 求余 expr % expr
+ 加法 expr + expr
- 减法 expr - expr

在除法运算中,C++语言的早期版本允许结果为负数的商向上或向下取整,C++11新标准则规定商一律向0取整(即直接切除小数部分)。

逻辑和关系运算符

结合律 运算符 功能 用法
! 逻辑非 !expr
< 小于 expr < expr
<= 小于等于 expr <= expr
> 大于 expr > expr
>= 大于等于 expr >= expr
== 相等 expr == expr
!= 不相等 expr != expr
&& 逻辑与 expr && expr
\ \ 逻辑或 expr \ \ expr

&&和||都是短路运算符。

进行比较运算时,除非比较的对象是布尔类型,否则不要使用布尔字面值true和false作为运算对象。

赋值运算符

赋值运算符=的左侧运算对象必须是一个可修改的左值。

1
2
3
4
5
6
int i = 0, j = 0, k = 0;	//初始化而非赋值
const int ci = i; //初始化而非赋值

1024 = k; //错误:字面值是右值
i + j = k; //错误:算术表达式是右值
ci = k; //错误:ci是常量左值,不可修改

C++11新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象。

1
2
3
k = {3.14};					//错误:窄化转换
vector<int> vi; //初始为空
vi = {0,1,2,3,4,5,6,7,8,9}; //vi现在有10个元素了

因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号。

复合赋值运算符包括+=-=*=/=%=<<=>>=&=^=|=。任意一种复合运算都完全等价于a = a op b

递增和递减运算符

递增和递减运算符是为对象加1或减1的简洁书写形式。很多不支持算术运算的迭代器可以使用递增和递减运算符。

递增和递减运算符分为前置版本和后置版本:

  • 前置版本首先将运算对象加1(或减1),然后将改变后的对象作为求值结果。
  • 后置版本也会将运算对象加1(或减1),但求值结果是运算对象改变前的值的副本。

除非必须,否则不应该使用递增或递减运算符的后置版本。后置版本需要将原始值存储下来以便于返回修改前的内容,如果我们不需要这个值,那么后置版本的操作就是一种浪费。

成员访问运算符

点运算符和箭头运算符都可用于访问成员,点获取类对象的成员,箭头运算符则通过指针间接获取,ptr->mem等价于(*ptr).mem

加括号是因为解引用优先级低于点运算符。

条件运算符

唯一的一个三目运算符——cond ? expr1 : expr2

cond为true则对expr1求值并返回该值,否则对expr2求值并返回。

条件运算符可以嵌套。

位运算符

运算符 功能 用法
~ 位求反 ~expr
<< 左移 expr1 << expr2
>> 右移 expr1 >> expr2
按位与 expr & expr
^ 异或 expr ^ expr
按位或 expr \ expr

在位运算中符号位如何处理并没有明确的规定,所以建议仅将位运算符用于无符号类型的处理。

左移运算符<<在运算对象右侧插入值为0的二进制位。右移运算符>>的行为依赖于其左侧运算对象的类型:如果该运算对象是无符号类型,在其左侧插入值为0的二进制位;如果是带符号类型,在其左侧插入符号位的副本或者值为0的二进制位,如何选择视具体环境而定。

sizeof运算符

sizeof运算符返回一个表达式或一个类型名字所占的字节数,返回值是size_t类型。

在sizeof的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用。

sizeof运算符的结果部分依赖于其作用的类型:

  • 对char或者类型为char的表达式执行sizeof运算,返回值为1。
  • 对引用类型执行sizeof运算得到被引用对象所占空间的大小。
  • 对指针执行sizeof运算得到指针本身所占空间的大小。
  • 对解引用指针执行sizeof运算得到指针指向的对象所占空间的大小,指针不需要有效。
  • 对数组执行sizeof运算得到整个数组所占空间的大小。
  • 对string或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中元素所占空间的大小。

逗号运算符

多个运算对象以逗号隔开,最终返回最右侧表达式的值。常见于for循环。

类型转换

###隐式转换

自动执行的类型转换叫隐式转换。

何时发生隐式转换?

  • 大多数表达式中,比int类型小的整型值首先提升为较大的整型值,主要是int。
  • 条件中,非布尔值换成布尔类型。
  • 初始化过程中,初始值转换成变量类型;赋值语句中,右侧运算对象转换成左侧运算对象的类型。
  • 如果算术运算或关系运算的运算对象有多种类型,需要转换成一种类型。
  • 函数调用时也会发生类型转换。
  • 数组转换成指针,大多数用到数组的表达式中,数组自动转成指向数组首元素的指针。
  • 指针的转换,指向任意非常量的指针可以转成void ,指向任意对象的指针能转成const void 。继承也会涉及到指针转换。
  • 转换成常量,允许指向非常量类型的指针和引用转成指向常量类型的指针和引用。
  • 类类型定义的转换。

###显式转换

程序员也可以进行显式转换。

显式类型转换也叫做强制类型转换(cast)。虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的。建议尽量避免强制类型转换。

命名的强制类型转换(named cast)形式如下:cast-name<type>(expression);

其中type是转换的目标类型,expression是要转换的值。如果type是引用类型,则转换结果是左值。cast-namestatic_castdynamic_castconst_castreinterpret_cast中的一种,用来指定转换的方式。

  • dynamic_cast支持运行时类型识别。
  • 任何具有明确定义的类型转换,只要不包含底层const,都能使用static_cast。
  • const_cast只能改变运算对象的底层const,不能改变表达式的类型。同时也只有const_cast能改变表达式的常量属性。const_cast常常用于函数重载。
  • reinterpret_cast通常为运算对象的位模式提供底层上的重新解释。这个东西非常危险。

尽量避免强制类型转换,尽量限制类型转换值的作用域,很容易自掘坟墓的语言特性。

早期版本的C++语言中,显式类型转换包含两种形式:

1
2
type (expression);    // 函数形式的强制类型转换
(type) expression; // C风格的强制类型转换

操作符优先级

参考网页:https://en.cppreference.com/w/cpp/language/operator_precedence

文章目录
  1. 1. C++ Primer - 表达式
    1. 1.1. 基础
      1. 1.1.1. 优先级和结合性
      2. 1.1.2. 左值和右值
    2. 1.2. 算术运算符
    3. 1.3. 逻辑和关系运算符
    4. 1.4. 赋值运算符
    5. 1.5. 递增和递减运算符
    6. 1.6. 成员访问运算符
    7. 1.7. 条件运算符
    8. 1.8. 位运算符
    9. 1.9. sizeof运算符
    10. 1.10. 逗号运算符
    11. 1.11. 类型转换
    12. 1.12. 操作符优先级
,