文章目录

  • 基础
    • 基本概念
      • 组合运算符和运算对象
      • 运算对象转换
      • 重载运算符
      • 左值和右值
    • 优先级与结合律
      • 括号无视优先级与结合律
      • 优先级与结合律有何影响
    • 求值顺序
      • 求值顺序、优先级、结合律
      • 建议:处理复合表达式
  • 算术运算符
    • 一元正负号
    • 加、减、乘、除、求余
    • 提示:溢出和其他算术运算异常
  • 逻辑和关系运算符
    • 逻辑与和逻辑或运算符
    • 逻辑非运算符
    • 关系运算符
    • 相等性测试与布尔字面值
  • 赋值运算符
    • 赋值运算满足右结合律
    • 赋值运算优先级较低
    • 切勿混淆相等运算符和赋值运算符
    • 复合赋值运算符
  • 递增和递减运算符
    • 建议:除非必须,否则不用递增递减运算符的后置版本
    • 在一条语句中混用解引用和递增运算符
    • 建议:简洁可以成为一种美德
    • 运算对象可按任意顺序求值
  • 成员访问运算符
  • 条件运算符
    • 嵌套条件运算符
    • 在输出表达式中使用条件运算符
  • 位运算符
    • 移位运算符
    • 位求反运算符
    • 位与、位或、位异或运算符
    • 使用位运算符
    • 移位运算符(又叫IO运算符)满足左结合律
  • sizeof运算符
  • 逗号运算符
  • 类型转换
    • 何时发生隐式类型转换
    • 算术转换
      • 整型提升
      • 无符号类型的运算对象
      • 理解算术转换
    • 其他隐式显示转换
    • 显示转换
      • 命名的强制类型转换
      • static_cast
      • const_cast
      • reinterpret_cast
      • 建议:避免强制类型转换
      • 旧式的强制类型转换
  • 运算符优先级表

表达式(expression)由一个或多个运算对象(operand)组成,对表达式求值将得到一个结果(result)。

  • 字面值和变量是最简单的表达式,其结果就是字面值和变量的值。

  • 把一个运算符( operator)和一个或多个运算对象组合起来可以生成较复杂的表达式

基础

有几个基础概念对表达式的求值过程有影响,它们涉及大多数(甚至全部)表达式。

基本概念

C++定义了一元运算符(unary operator)和二元运算符(binary operator)。

  • 作用于一个运算对象的运算符是一元运算符,如取地址符(&)和解引用符(*);
  • 作用于两个运算对象的运算符是二元运算符,如相等运算符(==)和乘法运算符(*)。

除此之外,还有一个作用于三个运算对象的三元运算符(?:)。函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。

(一符多意)一些符号既能作为一元运算符也能作为二元运算符。以符号*为例,作为一元运算符时执行解引用操作,作为二元运算符时执行乘法操作。一个符号到底是一元运算符还是二元运算符由它的上下文决定。对于这类符号来说,它的两种用法互不相干,完全可以当成两个不同的符号。

组合运算符和运算对象

对于含有多个运算符的复杂表达式来说,要想理解它的含义首先要理解运算符的:

  • 优先级(precedence)
  • 结合律(associativity)
  • 运算对象的求值顺序(order of evaluation)

例如,下面这条表达式的求值结果依赖于表达式中运算符和运算对象的组合方式:

5 + 10 * 20 / 2;

乘法运算符(*)是一个二元运算符,它的运算对象有4种可能: 10和20、10和20/2、15和20、15和20/2。下一节将介绍如何理解这样一条表达式。

运算对象转换

在表达式求值的过程中,运算对象常常由一种类型转换成另外一种类型。

例如,尽管一般的二元运算符都要求两个运算对象的类型相同,但是很多时候即使运算对象的类型不相同也没有关系,只要它们能被转换成同一种类型即可。

类型转换的规则虽然有点复杂,但大多数都合乎情理、容易理解。

例如,整数能转换成浮点数,浮点数也能转换成整数,但是指针不能转换成浮点数。让人稍微有点意外的是,小整数类型(如bool、char、short等)通常会被提升(promoted)成较大的整数类型,主要是int。接下来将会详细介绍类型转换的细节。

重载运算符

C++语言定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。

当运算符作用于类类型的运算对象时,用户可以自行定义其含义。因为这种自定义的过程事实上是为已存在的运算符赋予了另外一层含义,所以称之为重载运算符( overloaded operator)。IO库的>>和<<运算符以及string对象、vector对象和迭代器使用的运算符都是重载的运算符。

我们使用重载运算符时,其包括运算对象的类型和返回值的类型,都是由该运算符定义的。但是运算对象的个数、运算符的优先级和结合律都是无法改变的。

左值和右值

C++的表达式要不然是右值(rvalue,读作“are-value"),要不然就是左值(lvalue,读作“ell-value”)。这两个名词是从C语言继承过来的,原本是为了帮助记忆:左值可以位于赋值语句的左侧,右值则不能。

在C++语言中,二者的区别就没那么简单了。

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

可以做一个简单的归纳

  • 当一个对象被用作右值的时候,用的是对象的值(内容);
  • 当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。

不同的运算符运算对象的要求各不相同,

  • 有的需要左值运算对象、
  • 有的需要右值运算对象;

返回值也有差异,

  • 有的得到左值结果、
  • 有的得到右值结果。

一个重要的原则是在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。当一个左值被当成右值使用时,实际使用的是它的内容(值)。到目前为止,已经有几种我们熟悉的运算符是要用到左值的。(右能被左替,对换不能替)

  • 赋值运算符需要一个(非常量nonconst)左值作为其左侧运算对象,得到的结果也仍然是一个左值。(a = b = 1)

  • 取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。(int a = 1;int *p = &a;)

  • 内置解引用运算符、下标运算符、迭代器解引用运算符、string和 vector的下标运算符的求值结果都是左值。

  • 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本(本书之前章节所用的形式)所得的结果也是左值。

接下来在介绍运算符的时候,我们将会注明该运算符运算对象是否必须是左值以及其求值结果是否是左值。

使用关键字decltype的时候,左值和右值也有所不同。如果表达式的求值结果是左值,decltype作用于该表达式(不是变量)得到一个引用类型

举个例子,假定p的类型是int*,因为解引用运算符生成左值,所以decltype(p)的结果是int&。另一方面,因为取地址运算符生成右值,所以 decltype (&p)的结果是int*,也就是说,结果是一个指向整型指针的指针。

优先级与结合律

复合表达式(compound expression)是指含有两个或多个运算符的表达式。求复合表达式的值需要首先将运算符和运算对象合理地组合在一起,优先级结合律决定了运算对象组合的方式。也就是说,它们决定了表达式中每个运算符对应的运算对象来自表达式的哪一部分。表达式中的括号无视上述规则,程序员可以使用括号将表达式的某个局部括起来使其得到优先运算。

一般来说,表达式最终的值依赖于其子表达式的组合方式。高优先级运算符的运算对象要比低优先级运算符的运算对象更为紧密地组合在一起。如果优先级相同,则其组合规则由结合律确定。例如,乘法和除法的优先级相同且都高于加法的优先级。因此,乘法和除法的运算对象会首先组合在一起,然后才能轮到加法和减法的运算对象。算术运算符满足左结合律,意味着如果运算符的优先级相同,将按照从左向右的顺序组合运算对象:

  • 根据运算符的优先级,表达式3+4*5的值是23,不是35。

  • 根据运算符的结合律,表达式20-15-3的值是2,不是8。

举一个稍微复杂一点的例子,如果完全按照从左向右的顺序求值,下面的表达式将得到20:

6 + 3 * 4 / 2 + 2

也有一些人会计算得到9、14或者36,然而在C++语言中真实的计算结果应该是14。这是因为这条表达式事实上与下述表达式等价:

// parentheses in this expression match default precedence and associativity
((6 + ((3 * 4) / 2)) + 2)

(Note:如果优先级相同,则其组合规则由结合律确定。

(Note:左结合律 -> 按照从左向右的顺序组合运算对象)

括号无视优先级与结合律

括号无视普通的组合规则,表达式中括号括起来的部分被当成一个单元来求值,然后再与其他部分一起按照优先级组合。例如,对上面这条表达式按照不同方式加上括号就能得到4种不同的结果:

// parentheses result in alternative groupings
cout << (6 + 3) * (4 / 2 + 2) << endl; // prints 36
cout << ((6 + 3) * 4) / 2 + 2 << endl; // prints 20
cout << 6 + 3 * 4 / (2 + 2) << endl; // prints 9

优先级与结合律有何影响

由前面的例子可以看出,优先级会影响程序的正确性,这一点在介绍的解引用和指针运算中也有所体现:

int ia[] = {0,2,4,6,8}; // array with five elements of type int
int last = *(ia + 4); // initializes last to 8, the value of ia [4]
last = *ia + 4; // last = 4, equivalent to ia [0] + 4

如果想访问ia+4位置的元素,那么加法运算两端的括号必不可少。一旦去掉这对括号,*ia就会首先组合在一起,然后4再与*ia的值相加。

结合律对表达式产生影响的一个典型示例是输入输出运算,将要介绍IO相关的运算符满足左结合律。这一规则意味着我们可以把几个IO运算组合在一条表达式当中:

cin >> v1 >> v2; // read into v1 and then into v2

最后部分会罗列出了全部的运算符,并用双横线将它们分割成若干组。

同一组内的运算符优先级相同,组的位置越靠前组内的运算符优先级越高。例如,前置递增运算符和解引用运算符的优先级相同并且都比算术运算符的优先级高。

求值顺序

优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值。在大多数情况下,不会明确指定求值的顺序。对于如下的表达式:

int i = f1() * f2();

我们知道f1和f2一定会在执行乘法之前被调用,因为毕竟相乘的是这两个函数的返回值。但是我们无法知道到底f1在f2之前调用还是f2在f1之前调用

对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。举个简单的例子,<<运算符没有明确规定何时以及如何对运算对象求值,因此下面的输出表达式是未定义的:

int i = 0;
cout << i << " " << ++i << endl; // undefined

因为程序是未定义的,所以我们无法推断它的行为。

  • 编译器可能先求++i的值再求i的值,此时输出结果是1 1;

  • 也可能先求i的值再求++i的值,输出结果是0 1;

甚至编译器还可能做完全不同的操作。因为此表达式的行为不可预知,因此不论编译器生成什么样的代码程序都是错误的。

有4种运算符明确规定了运算对象的求值顺序

  1. 逻辑与(&&)运算符,它规定先求左侧运算对象的值,只有当左侧运算对象的值为真时才继续求右侧运算对象的值。
  2. 逻辑或(|| )运算符、
  3. 条件(? :)运算符
  4. 逗号(,)运算符。

求值顺序、优先级、结合律

运算对象的求值顺序与优先级和结合律无关,在一条形如f( )+g( ) *h()+j()的表达式中:

  • 优先级规定,g()的返回值和h ()的返回值相乘。
  • 结合律规定,f()的返回值先与g ()和 h ()的乘积相加,所得结果再与j()的返回值相加。
  • 对于这些函数的调用顺序没有明确规定。

如果f、g、h和j是无关函数independent functions ,它们既不会改变同一对象的状态也不执行IO任务,那么函数的调用顺序不受限制。反之,如果其中某几个函数影响同一对象,则它是一条错误的表达式,将产生未定义的行为。

建议:处理复合表达式

以下两条经验准则对书写复合表达式有益:

  1. 拿不准的时候最好用括号来强制让表达式的组合关系符合程序逻辑的要求。
  2. 如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。

第2条规则有一个重要例外,当改变运算对象的子表达式本身就是另外一个子表达式的运算对象时该规则无效。

例如,在表达式*++iter中,递增运算符改变 iter的值,iter(已经改变)的值又是解引用运算符的运算对象。此时(或类似的情况下),求值的顺序不会成为问题,因为递增运算(即改变运算对象的子表达式)必须先求值,然后才轮到解引用运算。显然,这是一种很常见的用法,不会造成什么问题。

算术运算符

左结合律

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

上表(以及后面章节的运算符表)按照运算符的优先级将其分组。

一元运算符的优先级最高,接下来是乘法和除法,优先级最低的是加法和减法。优先级高的运算符比优先级低的运算符组合得更紧密。上面的所有运算符都满足左结合律,意味着当优先级相同时按照从左向右的顺序进行组合。

除非另做特殊说明,算术运算符都能作用于任意算术类型以及任意能转换为算术类型的类型。

算术运算符的运算对象和求值结果都是右值。(当一个对象被用作右值的时候,用的是对象的值(内容);)

在表达式求值之前,小整数类型的运算对象被提升成较大的整数类型,所有运算对象最终会转换成同一类型。

一元正负号

一元正号运算符、加法运算符和减法运算符都能作用于指针。上一章节已经介绍过二元加法和减法运算符作用于指针的情况。当一元正号运算符作用于一个指针或者算术值时,返回运算对象值的一个(提升后的)副本。

(TODO:一元正号运算符作用于一个指针?查查有哪些用途。)

一元负号运算符对运算对象值取负后,返回其(提升后的)副本:

int i = 1024;
int k = -i; // i is -1024
bool b = true;
bool b2 = -b; // b2 is true!

我们指出布尔值不应该参与运算,-b就是一个很好的例子。

对大多数运算符来说,布尔类型的运算对象将被提升为 int 类型。如上所示,布尔变量b的值为真,参与运算时将被提升成整数值1,对它求负后的结果是-1。将-1再转换回布尔值并将其作为b2的初始值,显然这个初始值不等于0转换成布尔值后应该为1。所以,b2的值是真!(似非而是的结果)

加、减、乘、除、求余

当作用于算术类型的对象时,算术运算符+、-、*、/的含义分别是加法、减法、乘法和除法。整数相除结果还是整数,也就是说,如果商含有小数部分,直接弃除:

int ival1 = 21/6; // ival1 is 3; result is truncated; remainder is discarded
int ival2 = 21/7; // ival2 is 3; no remainder; result is an integral value

运算符%俗称“取余”或“取模”运算符,负责计算两个整数相除所得的余数,参与取余运算的运算对象必须是整数类:

int ival = 42;
double dval = 3.14;
ival % 12; // ok: result is 6
ival % dval; // error: floating-point operand

在除法运算中,如果两个运算对象的符号相同则商为正(如果不为0的话),否则商为负。C++语言的早期版本允许结果为负值的商向上或向下取整,C++11新标准则规定商一律向0取整(即直接切除小数部分)。

根据取余运算的定义,如果m和 n是整数且n非0,则表达式(m/n)*n+m%n的求值结果与m相等。隐含的意思是,如果m%n不等于0,则它的符号和m相同。C++语言的早期版本允许 m%n 的符号匹配n的符号,而且商向负无穷一侧取整,这一方式在新标准中已经被禁止使用了。除了-m 导致溢出的特殊情况,其他时候(-m)/n和 m/(-n)都等于- (m/ n),m% (-n)等于m%n,(-m)%n等于-(m%n)。具体示例如下:

21 % 6; /* result is 3 */
21 / 6; /* result is 3 */21 % 7; /* result is 0 */
21 / 7; /* result is 3 */-21 % -8; /* result is -5 */
-21 / -8; /* result is 2 */21 % -5; /* result is 1 */
c21 / -5; /* result is -4 */

提示:溢出和其他算术运算异常

算术表达式有可能产生未定义的结果。

  • 一部分原因是数学性质本身例如除数是0的情况;
  • 另外一部分则源于计算机的特点:例如溢出,当计算的结果超出该类型所能表示的范围时就会产生溢出。

假设某个机器的short类型占16位,则最大的short数值是32767。在这样一台机器上,下面的复合赋值语句将产生溢出:

short short_value = 32767; // max value if shorts are 16 bits
short_value += 1; // this calculation overflows
cout << "short_value: " << short_value << endl;

给short_value赋值的语句是未定义的,这是因为表示一个带符号数32768需要17位,但是short类型只有16位。很多系统在编译和运行时都不报溢出错误,像其他未定义的行为一样,溢出的结果是不可预知的。在我们的系统中,程序的输出结果是:

short_value: -32768

该值发生了“环绕(wrapped around)”,符号位本来是0,由于溢出被改成了1,于是结果变成一个负值。在别的系统中也许会有其他结果,程序的行为可能不同甚至直接崩溃。

逻辑和关系运算符

关系运算符作用于算术类型或指针类型,逻辑运算符作用于任意能转换成布尔值的类型。

逻辑运算符和关系运算符的返回值都是布尔类型。

值为0的运算对象(算术类型或指针类型)表示假,否则表示真。

对于这两类运算符来说,运算对象和求值结果都是右值。

结合律 运算符 功能 用法
! 逻辑非 !expr



<
<=
>
>=
小于
小于等于
大于
大于等于
expr < expr
expr <= expr
expr > expr
expr >= expr

==
!=
相等
不相等
expr == expr
expr != expr
&& 逻辑与 expr &&expr
|| 逻辑或 expr||expr

逻辑与和逻辑或运算符

对于逻辑与运算符(&&)来说,当且仅当两个运算对象都为真时结果为真。对于逻辑或运算符(||)来说,只要两个运算对象中的一个为真结果就为真。

逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略称为短路求值(short-circuit evaluation)。

  • 对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值。

  • 对于逻辑或运算符来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。

上一章的几个程序用到了逻辑与运算符,它们的左侧运算对象是为了确保右侧运算对象求值过程的正确性和安全性。如:

index != s.size() && !isspace(s[index])

首先检查index是否到达string对象的末尾,以此确保只有当index在合理范围之内时才会计算右侧运算对象的值。

举一个使用逻辑或运算符的例子,假定有一个存储着若干string对象的vector对象,要求输出string对象的内容并且在遇到空字符串或者以句号结束的字符串时进行换行。使用基于范围的for循环处理string对象中的每个元素:

// note s as a reference to const; the elements aren't copied and can't be changed
for (const auto &s : text) { // for each element in textcout << s; // print the current element// blank lines and those that end with a period get a newlineif (s.empty() || s[s.size() - 1] == '.')cout << endl;elsecout << " "; // otherwise just separate with a space
}

输出当前元素后检查是否需要换行。if语句的条件部分首先检查s 是否是一个空string,如果是,则不论右侧运算对象的值如何都应该换行(短路求值体现)。只有当string对象非空时才需要求第二个运算对象的值,也就是检查string对象是否是以句号结束的。在这条表达式中,利用逻辑或运算符的短路求值策略确保只有当s非空时才会用下标运算符去访问它。

值得注意的是,s被声明成了对常量的引用。因为 text的元素是string对象,可能非常大,所以将s声明成引用类型可以避免对元素的拷贝。又因为不需要对string对象做写操作,所以s被声明成对常量的引用

逻辑非运算符

逻辑非运算符(!)将运算对象的值取反后返回。下面再举一个例子,假设vec是一个整数类型的vector对象,可以使用逻辑非运算符将empty函数的返回值取反从而检查vec是否含有元素:

// print the first element in vec if there is one
if (!vec.empty())cout << vec[0];

子表达式

!vec.empty()//vec.empty()==false的简写

当empty函数返回假时结果为真。

关系运算符

顾名思义,关系运算符比较运算对象的大小关系并返回布尔值。关系运算符都满足左结合律。

因为关系运算符的求值结果是布尔值,所以将几个关系运算符连写在一起会产生意想不到的结果:

// oops! this condition compares k to the bool result of i < j
if (i < j < k) // true if k is greater than 1! error

if语句的条件部分首先把i、j和第一个<运算符组合在一起,其返回的布尔值再作为第二个<运算符的左侧运算对象。也就是说,k 比较的对象是第一次比较得到的那个或真或假的结果!要想实现我们的目的,其实应该使用下面的表达式:

// ok: condition is true if i is smaller than j and j is smaller than k
if (i < j && j < k) { /* ... */ }

相等性测试与布尔字面值

如果想测试一个算术对象或指针对象的真值,最直接的方法就是将其作为if语句的条件:

if (val) { /* ... */ } // true if val is any nonzero value
if (!val) { /* ... */ } // true if val is zero

在上面的两个条件中,编译器都将val转换成布尔值。如果val非0则第一个条件为真,如果val的值为О则第二个条件为真。

有时会试图将上面的真值测试写成如下形式:

if (val == true) { /* ... */ } // true only if val is equal to 1!

但是这种写法存在两个问题:首先,与之前的代码相比,上面这种写法较长而且不太直接(尽管大家都认为缩写的形式对初学者来说有点难理解);更重要的一点是,如果val不是布尔值,这样的比较就失去了原来的意义。

如果val不是布尔值,那么进行比较之前会首先把 true 转换成val的类型。也就是说,如果val不是布尔值,则代码可以改写成如下形式:

if (val == 1) { /* ... */ }

正如我们已经非常熟悉的那样,当布尔值转换成其他算术类型时,false转换成0而true转换成1。如果真想知道val的值是否是1,应该直接写出1这个数值来,而不要与true 比较。

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

赋值运算符

赋值运算符的左侧运算对象必须是一个可修改的左值。如果给定

int i = 0, j = 0, k = 0; // initializations, not assignment
const int ci = i; // initialization, not assignment

则下面的赋值语句都是非法的:

1024 = k; // error: literals are rvalues
i + j = k; // error: arithmetic expressions are rvalues
ci = k; // error: ci is a const (nonmodifiable) lvalue

赋值运算的结果是它的左侧运算对象,并且是一个左值。相应的,结果的类型就是左侧运算对象的类型。如果赋值运算符的左右两个运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型:

k = 0; // result: type int, value 0
k = 3.14159; // result: type int, value 3

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

k = {3.14}; // error: narrowing conversion
vector<int> vi; // initially empty
vi = {0,1,2,3,4,5,6,7,8,9}; // vi now has ten elements, values 0 through 9

如果左侧运算对象是内置类型,那么初始值列表最多只能包含一个值,而且该值即使转换的话其所占空间也不应该大于目标类型的空间。

对于类类型来说,赋值运算的细节由类本身决定。对于vector来说,vector模板重载了赋值运算符并且可以接收初始值列表,当赋值发生时用右侧运算对象的元素替换左侧运算对象的元素。

无论左侧运算对象的类型是什么,初始值列表都可以为空。此时,编译器创建一个值初始化的临时量并将其赋给左侧运算对象。

赋值运算满足右结合律

赋值运算符满足右结合律,这一点与其他二元运算符不太一样:

int ival, jval;
ival = jval = 0; // ok: each assigned 0

因为赋值运算符满足右结合律,所以靠右的赋值运算jval=0 作为靠左的赋值运算符的右侧运算对象。又因为赋值运算返回的是其左侧运算对象,所以靠右的赋值运算的结果(即jval)被赋给了ival。

对于多重赋值语句中的每一个对象,它的类型或者与右边对象的类型相同、或者可由右边对象的类型转换得到:

int ival, *pval; // ival is an int; pval is a pointer to int
ival = pval = 0; // error: cannot assign the value of a pointer to an int
string s1, s2;
s1 = s2 = "OK"; // string literal "OK" converted to string

因为ival和pval的类型不同,而且 pval 的类型(int*)无法转换成ival 的类型(int),所以尽管0这个值能赋给任何对象,但是第一条赋值语句仍然是非法的。

与之相反,第二条赋值语句是合法的。这是因为字符串字面值可以转换成string对象并赋给s2,而s2和s1的类型相同,所以s2的值可以继续赋给s1。

赋值运算优先级较低

赋值语句经常会出现在条件当中。因为赋值运算的优先级相对较低,所以通常需要给赋值部分加上括号使其符合我们的原意。下面这个循环说明了把赋值语句放在条件当中有什么用处,它的目的是反复调用一个函数直到返回期望的值(比如42)为止:

// a verbose and therefore more error-prone way to write this loop
int i = get_value(); // get the first value
while (i != 42) {// do something ...i = get_value(); // get remaining values
}

在这段代码中,首先调用get_value函数得到一个值,然后循环部分使用该值作为条件。在循环体内部,最后一条语句会再次调用get_value函数并不断重复循环。可以将上述代码以更简单直接的形式表达出来:

int i;
// a better way to write our loop---what the condition does is now clearer
while ((i = get_value()) != 42) {// do something ...
}

这个版本的while条件更容易表达我们的真实意图:不断循环读取数据直至遇到42为止。其处理过程是首先将get_value函数的返回值赋给i,然后比较i和42是否相等。

如果不加括号的话含义会有很大变化,比较运算符!=的运算对象将是get_value函数的返回值及42,比较的结果不论真假将以布尔值的形式赋值给i,这显然不是我们期望的结果。

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

切勿混淆相等运算符和赋值运算符

C++语言允许用赋值运算作为条件,但是这一特性可能带来意想不到的后果:

if (i = j)

此时,if语句的条件部分把j的值赋给i,然后检查赋值的结果是否为真。如果j不为0,条件将为真。然而程序员的初衷很可能是想判断i和j是否相等:

if (i == j)

程序的这种缺陷显然很难被发现,好在一部分编译器会对类似的代码给出警告信息。

复合赋值运算符

我们经常需要对对象施以某种运算,然后把计算的结果再赋给该对象。举个例子,考虑1.4.2节(第11页)的求和程序:

int sum = 0;
// sum values from 1 through 10 inclusive
for (int val = 1; val <= 10; ++val)sum += val; // equivalent to sum = sum + val

这种复合操作不仅对加法来说很常见,而且也常常应用于其他算术运算符或者将要介绍的位运算符。每种运算符都有相应的复合赋值形式:

+= -= *= /= %= // arithmetic operators
<<=  >>=  &= ^= |= // bitwise operators

任意一种复合运算符都完全等价于

a = a op b;

唯一的区别是左侧运算对象的求值次数:使用复合运算符只求值一次,使用普通的运算符则求值两次。这两次包括:一次是作为右边子表达式的一部分求值,另一次是作为赋值运算的左侧运算对象求值。其实在很多地方,这种区别除了对程序性能有些许影响外几乎可以忽略不计。

递增和递减运算符

递增运算符(++)和递减运算符(–)为对象的加1和减1操作提供了一种简洁的书写形式。这两个运算符还可应用于迭代器,因为很多迭代器本身不支持算术运算,所以此时递增和递减运算符除了书写简洁外还是必须的。

递增和递减运算符有两种形式:前置版本和后置版本。

  • 到目前为止,之前的程序使用的都是前置版本,这种形式的运算符首先将运算对象加1(或减1),然后将改变后的对象作为求值结果。
  • 后置版本也会将运算对象加1(或减1),但是求值结果是运算对象改变之前那个值的副本:
int i = 0, j;
j = ++i; // j = 1, i = 1: prefix yields the incremented value
j = i++; // j = 1, i = 2: postfix yields the unincremented value

这两种运算符必须作用于左值运算对象。

  • 前置版本将对象本身作为左值返回,
  • 后置版本则将对象原始值的副本作为右值返回。

建议:除非必须,否则不用递增递减运算符的后置版本

有C语言背景的读者可能对优先使用前置版本递增运算符有所疑问,其实原因非常简单:前置版本的递增运算符避免了不必要的工作,它把值加1后直接返回改变了的运算对象。与之相比,后置版本需要将原始值存储下来以便于返回这个未修改的内容。如果我们不需要修改前的值,那么后置版本的操作就是一种浪费。

对于整数和指针类型来说,编译器可能对这种额外的工作进行一定的优化,但是对于相对复杂的迭代器类型,这种额外的工作就消耗巨大了。建议养成使用前置版本的习惯,这样不仅不需要担心性能的问题,而且更重要的是写出的代码会更符合编程的初衷。

在一条语句中混用解引用和递增运算符

如果我们想在一条复合表达式中既将变量加1或减1又能使用它原来的值,这时就可以使用递增和递减运算符的后置版本

举个例子,可以使用后置的递增运算符来控制循环输出一个vector对象内容直至遇到(但不包括)第一个负值为止:

auto pbeg = v.begin();
// print elements up to the first negative value
while (pbeg != v.end() && *beg >= 0)cout << *pbeg++ << endl; // print the current value and advance pbeg

对于刚接触C++和C的程序员来说,*pbeg++不太容易理解。其实这种写法非常普遍,所以程序员一定要理解其含义

后置递增运算符的优先级高于解引用运算符,因此*pbeg++等价于*(pbeg++)。pbeg++把pbeg 的值加1,然后返回pbeg的初始值的副本作为其求值结果,此时解引用运算符的运算对象是pbeg未增加之前的值。最终,这条语句输出pbeg开始时指向的那个元素,并将指针向前移动一个位置。

这种用法完全是基于一个事实,即后置递增运算符返回初始的未加1的值。如果返回的是加1之后的值,解引用该值将产生错误的结果。不但无法输出第一个元素,而且更糟糕的是如果序列中没有负值,程序将可能试图解引用一个根本不存在的元素。

建议:简洁可以成为一种美德

形如*pbeg++的表达式一开始可能不太容易理解,但其实这是一种被广泛使用的、有效的写法。当对这种形式熟悉之后,书写

cout << *iter++ << endl;

要比书写下面的等价语句更简洁、也更少出错

cout << *iter << endl;
++iter;

不断研究这样的例子直到对它们的含义一目了然。大多数C++程序追求简洁、摒弃冗长,因此C++程序员应该习惯于这种写法。而且,一旦熟练掌握了这种写法后,程序出错的可能性也会降低。

运算对象可按任意顺序求值

大多数运算符都没有规定运算对象的求值顺序,这在一般情况下不会有什么影响。

然而,如果一条子表达式改变了某个运算对象的值,另一条子表达式又要使用该值的话,运算对象的求值顺序就很关键了。因为递增运算符和递减运算符会改变运算对象的值,所以要提防在复合表达式中错用这两个运算符。

为了说明这一问题,使用for循环将输入的第一个单词改成大写形式:

for (auto it = s.begin(); it != s.end() && !isspace(*it);
++it)*it = toupper(*it); // capitalize the current character

在上述程序中,我们把解引用it 和递增it 两项任务分开来完成。如果用一个看似等价的while循环进行代替

// the behavior of the following loop is undefined!
while (beg != s.end() && !isspace(*beg))*beg = toupper(*beg++); // error: this assignment is undefined

将产生未定义的行为。问题在于:赋值运算符左右两端的运算对象都用到了beg,并且右侧的运算对象还改变了beg 的值,所以该赋值语句是未定义的。

编译器可能按照下面的任意一种思路处理该表达式

*beg = toupper(*beg); // execution if left-hand side is evaluated first
*(beg + 1) = toupper(*beg); // execution if right-hand side is evaluated first

也可能采取别的什么方式处理它。

成员访问运算符

点运算符和箭头运算符都可用于访问成员,其中,点运算符获取类对象的一个成员,箭头运算符与点运算符有关,表达式ptr->mem等价于(*ptr).mem:

string s1 = "a string", *p = &s1;
auto n = s1.size(); // run the size member of the string s1
n = (*p).size(); // run size on the object to which p points
n = p->size(); // equivalent to (*p).size()

因为解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号。如果没加括号,代码的含义就大不相同了:

// run the size member of p, then dereference the result!
*p.size(); // error: p is a pointer and has no member named size

这条表达式试图访问对象p的size成员,但是p本身是一个指针且不包含任何成员,所以上述语句无法通过编译。

箭头运算符作用于一个指针类型的运算对象,结果是一个左值。

点运算符分成两种情况:

  1. 如果成员所属的对象是左值,那么结果是左值;
  2. 反之,如果成员所属的对象是右值,那么结果是右值。

条件运算符

条件运算符(?:)允许我们把简单的if-else逻辑嵌入到单个表达式当中,条件运算符按照如下形式使用:

cond ? expr1 : expr2;

其中 cond是判断条件的表达式,而expr1和expr2是两个类型相同或可能转换为某个公共类型的表达式。条件运算符的执行过程是:首先求cond 的值,如果条件为真对expr1求值并返回该值,否则对expr2求值并返回该值。举个例子,我们可以使用条件运算符判断成绩是否合格:

string finalgrade = (grade < 60) ? "fail" : "pass";

条件部分判断成绩是否小于60。如果小于,表达式的结果是"fail",否则结果是"pass"。有点类似于逻辑与运算符和逻辑或运算符( &&和||),条件运算符只对exprl和expr2中的一个求值。

当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值:否则运算的结果是右值。

嵌套条件运算符

允许在条件运算符的内部嵌套另外一个条件运算符。也就是说,条件表达式可以作为另外一个条件运算符的cond或expr。举个例子,使用一对嵌套的条件运算符可以将成绩分成三档:优秀(high pass)、合格( pass)和不合格( fail ):

finalgrade = (grade > 90) ? "high pass": (grade < 60) ? "fail" : "pass";

第一个条件检查成绩是否在90分以上,如果是,执行符号?后面的表达式,得到"highpass";如果否,执行符号:后面的分支。这个分支本身又是一个条件表达式,它检查成绩是否在60分以下,如果是,得到"fail";否则得到"pass"。

条件运算符满足右结合律,意味着运算对象(一般)按照从右向左的顺序组合。因此在上面的代码中,靠右边的条件运算(比较成绩是否小于60)构成了靠左边的条件运算的:分支。

随着条件运算嵌套层数的增加,代码的可读性急剧下降。因此,条件运算的嵌套最好别超过两到三层。

在输出表达式中使用条件运算符

条件运算符的优先级非常低,因此当一条长表达式中嵌套了条件运算子表达式时,通常需要在它两端加上括号。例如,有时需要根据条件值输出两个对象中的一个,如果写这条语句时没把括号写全就有可能产生意想不到的结果:

cout << ((grade < 60) ? "fail" : "pass"); // prints pass or fail
cout << (grade < 60) ? "fail" : "pass"; // prints 1 or 0!
cout << grade < 60 ? "fail" : "pass"; // error: compares cout to 60

在第二条表达式中, grade和60的比较结果是<<运算符的运算对象,因此如果grade<60为真输出1,否则输出0。<<运算符的返回值是cout,接下来cout作为条件运算符的条件。也就是说,第二条表达式等价于

cout << (grade < 60); // prints 1 or 0
cout ? "fail" : "pass"; // test cout and then yield one of the two literals// depending on whether cout is true or false

因为第三条表达式等价于下面的语句,所以它是错误的:

cout << grade; // less-than has lower precedence than shift, so print grade first
cout < 60 ? "fail" : "pass"; // then compare cout to 60!

位运算符

位运算符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合。位运算符提供检查和设置二进制位的功能,将会要介绍的,一种名为bitset的标准库类型也可以表示任意大小的二进制位集合,所以位运算符同样能用于bitset类型。

左结合律

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

一般来说,如果运算对象是“小整型”,则它的值会被自动提升成较大的整数类型。运算对象可以是带符号的,也可以是无符号的。如果运算对象是带符号的且它的值为负,那么位运算符如何处理运算对象的“符号位”依赖于机器。而且,此时的左移操作可能会改变符号位的值,因此是一种未定义的行为。(不像Java那样,用>>>和>>来处理负号位)

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

移位运算符

之前在处理输入和输出操作时,我们已经使用过标准IO库定义的<<运算符和>>运算符的重载版本。

这两种运算符的内置含义是对其运算对象执行基于二进制位的移动操作,

  1. 首先令左侧运算对象的内容按照右侧运算对象的要求移动指定位数,
  2. 然后将经过移动的(可能还进行了提升)左侧运算对象的拷贝作为求值结果。

其中,右侧的运算对象一定不能为负,而且值必须严格小于结果的位数,否则就会产生未定义的行为。二进制位或者向左移(<<)或者向右移(>>),移出边界之外的位就被舍弃掉了:

在下面的例子中右侧为最低位并且假定char占8位、int占32位。

unsigned char bits = 0233;//0233是八进制的字面值
1 0 0 1 1 0 1 1

bits << 8 //bits提升成int类型,然后向左移动8位
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 0 1 1 0 0 0 0 0 0 0 0

bits << 31 //向左移动31位,左边超出边界的位丢弃掉了
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

bits >> 3 //向右移动3位,最右边的3位丢弃掉了
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1

左移运算符(<<)在右侧插入值为0的二进制位。右移运算符(>>)的行为则依赖于其左侧运算对象的类型:

  • 如果该运算对象是无符号类型,在左侧插入值为0的二进制位;
  • 如果该运算对象是带符号类型,在左侧插入符号位的副本或值为0的二进制位,如何选择要视具体环境而定。

位求反运算符

位求反运算符(~)将运算对象逐位求反后生成一个新值,将1置为0、将0置为1:

unsigned char bits = 0233;//0233是八进制的字面值
1 0 0 1 0 1 1 1

~bits
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 0 1 0 0 0

char类型的运算对象首先提升成int类型,提升时运算对象原来的位保持不变,往高位(high order position)添加0即可。因此在本例中,首先将bits提升成int类型,增加24个高位0,随后将提升后的值逐位求反。

位与、位或、位异或运算符

与(&)、或(|)、异或(^)运算符在两个运算对象上逐位执行相应的逻辑操作:

代码 高二十四位 低八位
unsigned char b1 = 0145; - 0 1 1 0 0 1 0 1
unsigned char b2 = 0257; - 1 0 1 0 1 1 1 1
b1 & b2 24个高阶位都是0 0 0 1 0 0 1 0 1
b1 | b2 24个高阶位都是0 1 1 1 0 1 1 1 1
b1 ^ b2 24个高阶位都是0 1 1 0 0 1 0 1 0
  • 对于位与运算符(&)来说,如果两个运算对象的对应位置都是1则运算结果中该位为1,否则为0。

  • 对于位或运算符(|)来说,如果两个运算对象的对应位置至少有一个为1则运算结果中该位为1,否则为0。

  • 对于位异或运算符(^)来说,如果两个运算对象的对应位置有且只有一个为1则运算结果中该位为1,否则为0。

一种常见的错误是把位运算符和逻辑运算符搞混了,比如位与(&)和逻辑与(&&)、位或(I)和逻辑或(1l)、位求反(~)和逻辑非(!)

使用位运算符

(位图法)

我们举一个使用位运算符的例子:假设班级中有30个学生,老师每周都会对学生进行一次小测验,测验的结果只有通过和不通过两种。为了更好地追踪测验的结果,我们用一个二进制位代表某个学生在一次测验中是否通过,显然全班的测验结果可以用一个无符号整数来表示:

unsigned long quiz1 = 0; //我们把这个值当成是位的集合来使用

定义quiz1的类型是unsigned long,这样,quiz1在任何机器上都将至少拥有32位;给quiz1赋一个明确的初始值,使得它的每一位在开始时都有统一且固定的值。

教师必须有权设置并检查每一个二进制位。例如,我们需要对序号为27的学生对应的位进行设置,以表示他通过了测验。为了达到这一目的,首先创建一个值,该值只有第27位是1其他位都是0,然后将这个值与quiz1进行位或运算,这样就能强行将quiz1的第27位设置为1,其他位都保持不变。

为了实现本例的目的,我们将quiz1的低阶位赋值为0、下一位赋值为1,以此类推,最后统计quiz1各个位的情况。

使用左移运算符和一个unsigned long类型的整数字面值1就能得到一个表示学生27通过了测验的数值:

1UL << 27 //生成一个值,该值只有第27位为1

指定数位置1

1U 的低阶位上有一个1,除此之外(至少)还有31个值为0的位。之所以使用unsignedlong类型,是因为int类型只能确保占用16位,而我们至少需要27位。上面这条表达式通过在值为1的那个二进制位后面添加0,使得它向左移动了27位。

接下来将所得的值与quiz1进行位或运算。为了同时更新quiz1的值,使用一条复合赋值语句:

quiz1 |= 1UL<< 27; //表示学生27通过了测验

|-运算符的工作原理和+=非常相似,它等价于

quiz1 = quiz1 | 1UL << 27; //等价于quiz1 l= 1UL << 27;

指定数位置0

假定教师在重新核对测验结果时发现学生27实际上并没有通过测验,他必须要把第27位的值置为0。此时我们需要使用一个特殊的整数,它的第27位是0、其他所有位都是1。将这个值与quiz1进行位与运算就能实现目的了:

quiz1 &= ~(1UL << 27); //学生27没有通过测验

通过将之前的值按位求反得到一个新值,除了第27位外都是1,只有第27位的值是0。随后将该值与quiz1进行位与运算,所得结果除了第27位外都保持不变。

查指定数位

最后,我们试图检查学生27测验的情况到底怎么样:

bool status = quiz1 & (1UL << 27);// 学生27是否通过了测验?

我们将quiz1和一个只有第27位是1的值按位求与,如果quiz1的第27位是1,计算的结果就是非0(真);否则结果是0。

小结

  1. 指定数位置1:用 |
  2. 指定数位置0:用 ~ &
  3. 查指定数位 :用 &

移位运算符(又叫IO运算符)满足左结合律

尽管很多程序员从未直接用过位运算符,但是几乎所有人都用过它们的重载版本来进行IO操作。重载运算符的优先级和结合律都与它的内置版本一样,因此即使程序员用不到移位运算符的内置含义,也仍然有必要理解其优先级和结合律。

因为移位运算符满足左结合律,所以表达式

cout<< "hi" << " there" << endl;

的执行过程实际上等同于

((cout <<"hi") << " there" ) << endl;

在这条语句中,运算对象"hi"和第一个<<组合在一起,它的结果和第二个<<组合在一起,接下来的结果再和第三个<<组合在一起。

移位运算符的优先级不高不低,介于中间:比算术运算符的优先级低,但比关系运算符、赋值运算符和条件运算符的优先级高。因此在一次使用多个运算符时,有必要在适当的地方加上括号使其满足我们的要求。

cout << 42 + 10; //正确:+的优先级更高,因此输出求和结果
cout << (10 < 42); //正确:括号使运算对象按照我们的期望组合在一起,输出1
cout << 10 < 42; //错误:试图比较cout和42!

最后一个cout的含义其实是

(cout << 10) < 42;

也就是“把数字10写到cout,然后将结果(即 cout)与42进行比较”。

sizeof运算符

sizeof运算符返回一条表达式或一个类型名字所占的字节数。sizeof运算符满足右结合律,其所得的值是一个size_t类型的常量表达式。运算符的运算对象有两种形式:

sizeof (type)
sizeof expr

在第二种形式中,sizeof返回的是表达式结果类型的大小。与众不同的一点是,sizeof并不实际计算其运算对象的值:

Sales_data data, *p;
sizeof(Sales_data); // size required to hold an object of type Sales_data
sizeof data; // size of data's type, i.e., sizeof(Sales_data)
sizeof p; // size of a pointer
sizeof *p; // size of the type to which p points, i.e., sizeof(Sales_data)
sizeof data.revenue; // size of the type of Sales_data's revenue member
sizeof Sales_data::revenue; // alternative way to get the size of revenue

这些例子中最有趣的一个是sizeof *p。

  • 首先,因为sizeof满足右结合律并且与*运算符的优先级一样,所以表达式按照从右向左的顺序组合。也就是说,它等价于sizeof(*p)。

  • 其次,因为sizeof不会实际求运算对象的值,所以即使p是一个无效(即未初始化)的指针也不会有什么影响。在sizeof的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用。sizeof不需要真的解引用指针也能知道它所指对象的类型。

C++ 11新标准允许我们使用作用域运算符来获取类成员的大小。通常情况下只有通过类的对象才能访问到类的成员,但是sizeof运算符无须我们提供一个具体的对象,因为要想知道类成员的大小无须真的获取该成员。

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

  • 对char或者类型为char的表达式执行sizeof运算,结果得1。

  • 引用类型执行sizeof运算得到被引用对象所占空间的大小。

  • 指针执行sizeof运算得到指针本身所占空间的大小。

  • 解引用指针执行sizeof运算得到指针指向的对象所占空间的大小,指针不需有效。

  • 数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次sizeof运算并将所得结果求和。注意,sizeof运算不会把数组转换成指针来处理。

  • 对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。(这个要注意)

因为执行sizeof运算能得到整个数组的大小,所以可以用数组的大小除以单个元素的大小得到数组中元素的个数:

// sizeof(ia)/sizeof(*ia) returns the number of elements in ia
constexpr size_t sz = sizeof(ia) / sizeof(*ia);
int arr2[sz]; // ok sizeof returns a constant expression

因为sizeof的返回值是一个常量表达式,所以我们可以用sizeof的结果声明数组的维度。

逗号运算符

逗号运算符(comma operator)含有两个运算对象,按照从左向右的顺序依次求值。和逻辑与、逻辑或以及条件运算符一样,逗号运算符也规定了运算对象求值的顺序。

对于逗号运算符来说,首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符真正的结果是右侧表达式的值。如果右侧运算对象是左值,那么最终的求值结果也是左值。

逗号运算符经常被用在for循环当中:

vector<int>::size_type cnt = ivec.size();
// assign values from size... 1 to the elements in ivec
for(vector<int>::size_type ix = 0; ix != ivec.size(); ++ix, --cnt)ivec[ix] = cnt;

这个循环在for语句的表达式中递增ix、递减cnt,每次循环迭代ix和cnt相应改变。只要ix满足条件,我们就把当前元素设成cnt的当前值。

类型转换

在C++语言中,某些类型之间有关联。如果两种类型有关联,那么当程序需要其中一种类型的运算对象时,可以用另一种关联类型的对象或值来替代。换句话说,如果两种类型可以相互转换(conversion),那么它们就是关联的。

举个例子,考虑下面这条表达式,它的目的是将ival初始化为6:

int ival = 3.541 + 3;//编译器可能会警告该运算损失了精度

加法的两个运算对象类型不同:3.541的类型是double,3的类型是int。C++语言不会直接将两个不同类型的值相加,而是先根据类型转换规则设法将运算对象的类型统一后再求值。上述的类型转换是自动执行的,无须程序员的介入,有时甚至不需要程序员了解。因此,它们被称作隐式转换(implicit conversion)。

算术类型之间的隐式转换被设计得尽可能避免损失精度。很多时候,如果表达式中既有整数类型的运算对象也有浮点数类型的运算对象,整型会转换成浮点型。在上面的例子中,3转换成double类型,然后执行浮点数加法,所得结果的类型是double。

接下来就要完成初始化的任务了。在初始化过程中,因为被初始化的对象的类型无法改变,所以初始值被转换成该对象的类型。仍以这个例子说明,加法运算得到的double类型的结果转换成int类型的值,这个值被用来初始化ival。由 double向 int 转换时忽略掉了小数部分,上面的表达式中,数值6被赋给了ival。

何时发生隐式类型转换

在下面这些情况下,编译器会自动地转换运算对象的类型:

  • 在大多数表达式中,比int类型小的整型值首先提升为较大的整数类型。
  • 在条件中,非布尔值转换成布尔类型。
  • 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
  • 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
  • 函数调用时也会发生类型转换。(未来会介绍)

算术转换

算术转换(arithmetic conversion)的含义是把一种算术类型转换成另外一种算术类型,前些章节有介绍。算术转换的规则定义了一套类型转换的层次,其中运算符的运算对象将转换成最宽的类型

例如,

  • 如果一个运算对象的类型是 long double,那么不论另外一个运算对象的类型是什么都会转换成long double。
  • 还有一种更普遍的情况,当表达式中既有浮点类型也有整数类型时,整数值将转换成相应的浮点类型。

整型提升

整型提升(integral promotion)负责把小整数类型转换成较大的整数类型。

对于bool、char、signed char、 unsigned char、short和 unsigned short等类型来说,只要它们所有可能的值都能存在 int 里,它们就会提升成int 类型;否则,提升成unsigned int类型。就如我们所熟知的,布尔值false提升成0、true提升成1。

较大的char类型(wchar_t、char16_t、char32_t)提升成int、unsigned int、long、unsigned long、long long和 unsigned long long中最小的一种类型,前提是转换后的类型要能容纳原类型所有可能的值。

无符号类型的运算对象

如果某个运算符的运算对象类型不一致,这些运算对象将转换成同一种类型。但是如果某个运算对象的类型是无符号类型,那么转换的结果就要依赖于机器中各个整数类型的相对大小了。

像往常一样,首先执行整型提升。如果结果的类型匹配,无须进行进一步的转换。如果两个(提升后的)运算对象的类型要么都是带符号的、要么都是无符号的,则小类型的运算对象转换成较大的类型。

如果一个运算对象是无符号类型、另外一个运算对象是带符号类型:

  • 其中的无符号类型不小于(>=)带符号类型,那么带符号的运算对象转换成无符号的。

    • 例如,假设两个类型分别是unsigned int和 int,则 int类型的运算对象转换成unsigned int类型。
    • 需要注意的是,如果int型的值恰好为负值,其结果感人(具体转换方法回看第二章笔记),并带来该节描述的所有副作用。
  • 剩下的一种情况是带符号类型大于(>)无符号类型,此时转换的结果依赖于机器。

    • 如果无符号类型的所有值都能存在该带符号类型中,则无符号类型的运算对象转换成带符号类型。
    • 如果不能,那么带符号类型的运算对象转换成无符号类型。
    • 例如,
      • 如果两个运算对象的类型分别是long和 unsigned int,并且int和 long的大小相同,则long类型的运算对象转换成unsigned int 类型;(带符号的运算对象转换成无符号的)
      • 如果 long类型占用的空间比 int更多,则unsigned int类型的运算对象转换成long类型。

(Note:好麻烦)

理解算术转换

要想理解算术转换,办法之一就是研究大量的例子:

bool flag; char cval;
short sval; unsigned short usval;
int ival; unsigned int uival;
long lval; unsigned long ulval;
float fval; double dval;
3.14159L + 'a'; //1. 'a' promoted to int, then that int converted to long double
dval + ival; // ival converted to double
dval + fval; // fval converted to double
ival = dval; // dval converted (by truncation) to int
flag = dval; // if dval is 0, then flag is false, otherwise true
cval + fval; // cval promoted to int, then that int converted to float
sval + cval; // sval and cval promoted to int
cval + lval; // cval converted to long
ival + ulval; // ival converted to unsigned long
usval + ival; //2. promotion depends on the size of unsigned short and int
uival + lval; // conversion depends on the size of unsigned int and long
  1. 在第一个加法运算中,小写字母’a’是char型的字符常量,它其实能表示一个数字值。到底这个数字值是多少完全依赖于机器上的字符集,在我们的环境中,'a’对应的数字值是97。当把’a’和一个 long double类型的数相加时,char类型的值首先提升成int类型,然后int类型的值再转换成long double类型。最终我们把这个转换后的值与那个字面值相加。

  2. 最后的两个含有无符号类型值的表达式也比较有趣,它们的结果依赖于机器。(Note:有趣?Excuse me.)

其他隐式显示转换

除了算术转换之外还有几种隐式类型转换,包括如下几种。

数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针:

int ia[10]; // array of ten ints
int* ip = ia; // convert ia to a pointer to the first element

当数组被用作decltype关键字的参数,或者作为取地址符(&)、sizeof及typeid等运算符的运算对象时,上述转换不会发生。同样的,如果用一个引用来初始化数组,上述转换也不会发生。在将来会看到,当在表达式中使用函数类型时会发生类似的指针转换。

指针的转换:C++还规定了几种其他的指针转换方式,包括

  • 常量整数值0或者字面值nullptr能转换成任意指针类型;
  • 指向任意非常量的指针能转换成void*;
  • 指向任意对象的指针能转换成const void*。
  • 未来将要介绍,在有继承关系的类型间还有另外一种指针转换的方式。

转换成布尔类型:存在一种从算术类型或指针类型向布尔类型自动转换的机制。如果指针或算术类型的值为0,转换结果是false;否则转换结果是true:

char *cp = get_string();
if (cp) /* ... */ // true if the pointer cp is not zero
while (*cp) /* ... */ // true if *cp is not the null character

转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。也就是说,如果T是一种类型,我们就能将指向T的指针或引用分别转换成指向const T的指针或引用:

int i;
const int &j = i; // convert a nonconst to a reference to const int
const int *p = &i; // convert address of a nonconst to the address of a const
int &r = j, *q = p; // error: conversion from const to nonconst not allowed

相反的转换并不存在,因为它试图删除掉底层const。

类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。在第7章,我们将看到一个例子,如果同时提出多个转换请求,这些请求将被拒绝。

我们之前的程序已经使用过类类型转换:一处是在需要标准库string类型的地方使用C风格字符串;另一处是在条件部分读入istream:

string s, t = "a value"; // character string literal converted to type string
while (cin >> s) // while condition converts cin to bool

条件 (cin>>s) 读入cin的内容并将cin作为其求值结果。条件部分本来需要一个布尔类型的值,但是这里实际检查的是istream类型的值。

幸好,IO库定义了从istream向布尔值转换的规则,根据这一规则,cin自动地转换成布尔值。所得的布尔值到底是什么由输入流的状态决定,如果最后一次读入成功,转换得到的布尔值是 true;相反,如果最后一次读入不成功,转换得到的布尔值是false。

显示转换

有时我们希望显式地将对象强制转换成另外一种类型。例如,如果想在下面的代码中执行浮点数除法:

int i,j;
double slope = i/j;

就要使用某种方法将i和/或j显式地转换成double,这种方法称作强制类型转换(cast)。

WARNING:虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的。

命名的强制类型转换

一个命名的强制类型转换具有如下形式:

cast-name<type>(expression);

其中,type是转换的目标类型而expression是要转换的值。如果type是引用类型,则结果是左值。cast-name是 static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种。cast-name指定了执行的是哪种转换。

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*
//正确:将void*转换回初始的指针类型
double *dp = static_cast<double*>(p) ;

当我们把指针存放在void*中,并且使用static_cast将其强制转换回原来的类型时,应该确保指针的值保持不变。也就是说,强制转换的结果将与原始的地址值相等,因此我们必须确保转换后所得的类型就是指针所指的类型。

类型一旦不符,将产生未定义的后果。

(Note:Java的类转换错误会抛出ClassCastException)

const_cast

const_cast只能改变运算对象的底层const(第二章内容):

const char *pc;
char *p = const_cast<char*>(pc);// 正确:但是通过p 写值是未定义的行为

对于将常量对象转换成非常量对象的行为,我们一般称其为“去掉const性质(cast away the const)”。一旦我们去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为。

然而如果对象是一个常量,再使用const_cast执行写操作就会产生未定义的后果。

只有const_cast能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误。同样的,也不能用const_cast改变表达式的类型:

const char *cp;
//错误: static_cast不能转换掉const性质
char *q = static_cast<char*> (cp) ;static_cast<string>(cp);//正确:字符串字面值转换成string类型const_cast<string>(cp);//错误:const_cast只改变常量属性,下行才对
const_cast<char *>(cp);

const_cast常常用于有函数重载的上下文中,关于函数重载将在第六章进行详细介绍。

reinterpret_cast

reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。举个例
子,假设有如下的转换

int *ip;
char *pc = reinterpret_cast<char*>(ip);

我们必须牢记pc所指的真实对象是一个int而非字符,如果把pc当成普通的字符指针使用就可能在运行时发生错误。例如:

string str(pc);

可能导致异常的运行时行为。

使用reinterpret_cast是非常危险的,用pc初始化str的例子很好地证明了这一点。其中的关键问题是类型改变了,但编译器没有给出任何警告或者错误的提示信息。当我们用一个int的地址初始化pc时,由于显式地声称这种转换合法,所以编译器不会发出任何警告或错误信息。接下来再使用pc时就会认定它的值是char*类型,编译器没法知道它实际存放的是指向int的指针

最终的结果就是,在上面的例子中虽然用pc初始化 str没什么实际意义,甚至还可能引发更糟糕的后果,但仅从语法上而言这种操作无可指摘。查找这类问题的原因非常困难,如果将ip强制转换成pc的语句和用pc初始化string对象的语句分属不同文件就更是如此。Thus, the initialization of str with pc is absolutely correct—albeit尽管 in this case meaningless or worse! Tracking down the cause of this sort of problem can prove extremely difficult, especially if the cast of ip to pc occurs in a file separate from the one in which pc is used to initialize a string.

无可指摘

指没有什么可以指责的。表示做得妥当。

link

WARNING:reinterpret_cast本质上依赖于机器,要想安全地使用reinterpret_cast必须对涉及的类型和编译器实现转换的过程都非常了解。

(Note:没看懂reinterpret_cast作用,个人理解为"强人所难的强制类型转换",TODO:查查它的作用)

建议:避免强制类型转换

强制类型转换干扰了正常的类型检查,因此我们强烈建议程序员避免使用强制类型转换。

这个建议对于reinterpret_cast尤其适用,因为此类类型转换总是充满了风险。

在有重载函数的上下文中使用const_cast无可厚非,关于这一点将在第六章中详细介绍;但是在其他情况下使用const_cast也就意味着程序存在某种设计缺陷。其他强制类型转换,比如 static_cast 和dynamic_cast,都不应该频繁使用。

每次书写了一条强制类型转换语句,都应该反复斟酌能否以其他方式实现相同的目标。就算实在无法避免,也应该尽量限制类型转换值的作用域,并且记录对相关类型的所有假定,这样可以减少错误发生的机会。

旧式的强制类型转换

在早期版本的C++语言中,显式地进行强制类型转换包含两种形式:

type (expr); // function-style cast notation
(type) expr; // C-language-style cast notation

根据所涉及的类型不同,旧式的强制类型转换分别具有与const_cast 、static_cast或reinterpret_cast相似的行为。当我们在某处执行旧式的强制类型转换时,如果换成const_cast和 static_cast也合法,则其行为与对应的命名转换一致。

如果替换后不合法,则旧式强制类型转换执行与reinterpret_cast类似的功能:

int *ip;
char *pc = (char*) ip; // ip is a pointer to int

的效果与使用reinterpret_cast一样。

WARNING:与命名的强制类型转换相比,旧式的强制类型转换从表现形式上来说不那么清晰明了,容易被看漏,所以一旦转换过程出现问题,追踪起来也更加困难。

(Note:有新的就用新的,旧的搁置吧)

运算符优先级表

结合律 运算符 功能 用法


::
::
::
全局作用域
类作用域
命名空间作用域
::name
class::name
namespace::name




.
->
[]
()
()
成员选择
成员选择
下标
函数调用
类型构造
object.member
pointer->member
expr[expr]
name(expr_list)
type(expr_list)




+_

typeid
typeid
explicit cast
后置递增运算
后置递减运算
类型ID
运行时类型ID
类型转换
lvalue++
lvalue–
typeid(type)
typeid(expr)
cast_name<type>(expr)
















++

~
!
-
+
*
&
()
sizeof
sizeof
sizeof…
new
new[]
delete
delete[]
noexcept
前置递增运算
前置递减运算
位求反
逻辑非
一元负号
一元正号
解引用
取地址
类型转换
对象的大小
类型的大小
参数包的大小
创建对象
创建数组
释放对象
释放数组
能否抛出异常
++lvalue
–lvalue
~expr
!expr
-expr
+expr
*expr
&lvalue
(type)expr
sizeof expr
sizeof(type)
sizeof–(name)
new type
new type[size]
delete expr
delete[] expr
noexcept(expr)

->*
.*
指向成员选择的指针
指向成员选择的指针
ptr->*ptr_to_member
obj.*ptr_to_member


*
/
%
乘法
除法
取模(取余)
expr * expr
expr / expr
expr % expr

+
-
加法
减法
expr + expr
expr - expr

<<
>>
向左移位
向右移位
expr << expr
expr >> expr



<
<=
>
>=
小于
小于等于
大于
大于等于
expr < expr
expr <= expr
expr > expr
expr >= expr

==
!=
相等
不想等
expr == expr
expr != expr
& 位与 expr & expr
^ 位异或 expr ^ expr
| 位或 expr | expr
&& 逻辑与 expr && expr
|| 逻辑或 expr || expr
? : 条件 expr ? expr : expr
= 赋值 lvalue = expr



*=,/=,%=
+=,-=
<<=,>>=
&=,|=,^=
复合赋值 lvalue += expr等
throw 抛出异常 throw expr
逗号 expr, expr

说明:

  1. 结合律中,“左”表示从左到右,“右”表示从右到左。

《C++ Primer 5th》笔记(4 / 19):表达式相关推荐

  1. C++ Primer 5th笔记(chap 19 特殊工具与技术)定位 new 表达式

    1. 定位 new (placement new) 1.1 起因 内存分配和初始化分离开 Placement new allows you to construct an object in memo ...

  2. C++ Primer 5th笔记(chap 17 标准库特殊设施)子表达式subexpression

    1. 正则表达式的模式通常包含一个或多个子表达式 通常用括号表示子表达式 eg. r 有两个子表达式: 第一个是点之前表示文件名的部分, 第二个表示文件扩展名 regex r ("([[:a ...

  3. C++ Primer 5th笔记(chap 19 特殊工具与技术)typeid运算符

    1. typeid运算符(typeid operator) 它允许程序向表达式提问:你的对象是什么类型? typeid表达式的形式是typeid(e),其中e可以是任意表达式或类型的名字,它操作的结果 ...

  4. C++ Primer 5th笔记(chap 19 特殊工具与技术)运行时类型识別RTTI

    1. 运行时类型识别(run-time type identification ) 当我们将这两个运算符用于某种类型的指针或引用, 并且该类型含有虚函数时, 运算符将 使用指针或引用所绑定对象的动态类 ...

  5. C++ Primer 5th笔记(chap 19 特殊工具与技术)typeid

    1. 通常情况下使用 typeid 比较两条表达式的类型是否相同或者比较一条表达式的类型是否与指定的类型相同 Derived *dp = new Derived; Base *bp = dp; // ...

  6. C++ Primer 5th笔记(chap 19 特殊工具与技术)控制内存分配

    1. new void operator new (size_t, void ) ;//不允许重新定义这个版本 string *sp = new string("a value") ...

  7. C++ Primer 5th笔记(chap 19 特殊工具与技术)枚举类型

    1. 枚举类型(enumeration) 将一组整型常量组织在一起. 1.1 限定作用域 的 枚 举 类 型( scoped enumeration) 关 键 字 enum class ( enum ...

  8. C++ Primer 5th笔记(4)chapter4 表达式

    4.1 重载运算符overloaded operator 定义:运算符作用于内置类型和复合类型的运算对象时所执行的操作. 4.2 左值和右值 当一个对象被用作右值的时候,用的是对象的值(内容): 当对 ...

  9. C++ Primer 5th笔记(chap 19 特殊工具与技术)异常类层次

    1. 类 exception . bad_cast 和 bad_alloc 定 义 了 默 认 构 造 函 数 runtime_error 和 logic_error没有默认构造函数, 但是有一个可以 ...

  10. C++ Primer 5th笔记(chap 19 特殊工具与技术)type_info 类

    1. type_info 的操作 操作 描述 t1 == t2 如果type_info对象t1和t2表示同一种类型,则返回true t1 != t2 如果type_info对象t1和t2表示不同的类型 ...

最新文章

  1. php实现无限分类超牛逼,PHP超牛逼无限极分类生成树方法_php技巧
  2. 因为那里面有我,也有你
  3. 31.openssl编程——SSL实现
  4. list python 访问 键值对_基础|Python常用知识点汇总(中)
  5. 配对交易方法_COVID下的自适应配对交易,一种强化学习方法
  6. 查看linux是否为虚拟机,以及其它信息,cpu,主机型号,主板型号等
  7. 持续集成之 Jenkins+Gitlab 打包发布程序到 Tomcat(二)
  8. Linux常用的基本命令08
  9. ICC_lab总结——ICC_lab3:布局
  10. flash, sparkle, glow, gleam, twinkle, glitter
  11. 中国互联网有哪些黑色产业链?
  12. java stringbuilder_Java stringBuilder的使用方法及实例解析
  13. 基于struts2 拦截器ResultType为chain的Action之间数据传递 ——表单页面打开优化
  14. win10删除设备和驱动器中多余的软件图标
  15. 使用adb从手机拉取apk包
  16. 可塑造攻击_指导如何帮助塑造我的职业
  17. mac下使用PyCharm遇到的坑坑洼洼
  18. 怎么申请专利?专利多久下证?专利申请费用多少?
  19. chrome://tracing 性能分析神器
  20. [转]AppCompat 22.1,Goole暴走,MD全面兼容低版本

热门文章

  1. mysql 表上限_mysql 数据库表的上限
  2. 【转】Ubuntu16.04安装 Matlab2018a详细教程
  3. 【转】Vue.js入门教程(一)从静态页面到前后端分离开发
  4. 用于科研的移动机器人平台推荐
  5. Qt:QSound无法播放.wav声音的解决办法
  6. 【Python CheckiO 题解】Three Words
  7. MySQL什么是关系_MySQL教程-关系模型
  8. springboot 做表白墙_华广表白墙 第六期|hsl每天都想和你嘻嘻哈哈
  9. 【ZOJ - 2968 】Difference Game (贪心,思维模拟)
  10. 【HDU - 5475】An easy problem(线段树,思维)