【C++】C++ primer 第四章学习笔记

第四章 表达式

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

4.1 基础

4.1.1 基本概念

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

  • 作用于一个运算对象的运算符是二元运算符,如取 地址符( & )解引用符( * )
  • 作用于两个运算对象的运算符是二元运算符,如 相等运算符( == )乘法运算符( * )
  • 除此之外,还有一个作用于三个运算对象的 三元运算符(Ternary Operator)
  • 函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。

一些符号如* 既能作为一元运算符也能作为二元运算符。对于这类符号,他的两种用法互不相干,完全可以当作两个不同的符号。

组合运算符和运算对象

  • 对于含有多个运算符的复杂表达式来说,要想理解它的含义首先要理解 运算符的优先级(precedence)、结合律(associativity)、以及运算对象的 求值顺序(order of evaluation)

运算对象转换

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

类型转换的规则有点复杂,如:整数能转换成浮点数,浮点数也能转换成整数,但是指针不能转换成浮点数。而小整数类型(如 boolcharshort 等)通常会被 提升(promoted) 为较大的整数类型,主要是 int

重载运算符

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

  • 当运算符作用于 类类型 的运算对象时,用户可以自定义其含义,被称作 运算符重载(overloaded operator)。如:IO 库的 >><< 运算符以及 string 对象、vector 对象和迭代器使用的运算符等。

左值和右值

C++的表达式分为 右值(rvalue)左值(lvalue)

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

一个重要原则(13章介绍一种例外情况):需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。

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

注意:如果 关键字decltype 作用于一个求值结果是左值的表达式(不是变量),会得到一个引用类型。(详见P121)

4.1.2 优先级与结合律

复合表达式(compound expression) 指含有两个或多个运算符的表达式。

求复合表达式的值需先将运算符和运算对象合理地组合在一起,优先级与结合律决定了运算对象的组合方式,高优先级运算符先运行(先乘除,后加减),如果优先级相同,则其组合规则由结合律确定(算术运算符从左往右结合)。

括号无视优先级与结合律

括号无视普通的组合规则,表达式中括号括起来的部分被当成一个单元来求值,然后再与其他部分一起按照优先级组合。

4.1.3 求值顺序

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

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

int i = 0;
cout << i << " " << ++i << endl;    // 未定义的

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

  • 逻辑与 ( && ) 运算符
  • 逻辑或 ( || ) 运算符
  • 条件 ( ? : ) 运算符
  • 逗号 ( , ) 运算符

建议:处理复合表达式时遵循以下两条经验准则是有益的

  • 不确定求值顺序时最好使用括号来强制让表达式的组合关系符合程序逻辑的要求;
  • 如果表达式改变了某个运算对象的值,则在表达式的其他位置不要再使用这个运算对象。

注意,第2条规则有一个重要例外,当改变运算对象的子表达式本身就是另一个子表达式的运算对象时,规则无效。例如,在表达式 *++iter中,递增运算符改变了 iter 的值,而改变后的 iter 的值又是解引用运算符的运算对象。此时(或类似情况下),求值的顺序不会成为问题,因为递增运算(即改变运算对象的子表达式)必须先求值,然后才轮到解引用运算。

4.2 算数运算符

算术运算符(左结合律)(按照运算符的优先级分组):

算术运算符的运算对象和求值结果都是右值

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

    int ival = 42;
    double dval = 3.14;
    ival % 12;			// 正确:结果是6
    ival % dval;		// 错误:运算对象是浮点类型
    

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

示例:

21 % 6; 	/* 结果是3  */ 	21 / 6; 	/* 结果是3  */
21 % 7; 	/* 结果是0  */ 	21 / 7; 	/* 给果是3  */
-21 % -8; 	/* 结果是-5 */ 	-21 / -8; 	/* 结果是2  */
21 % -5; 	/* 结果是1  */ 	21 / -5; 	/* 结果是-4 */

4.3 逻辑和关系运算符

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

逻辑和关系运算符的运算对象和求值结果都是右值。

逻辑与和逻辑或运算符

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

  • 对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值。
  • 对于逻辑或运算符来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。
// s 是对常量的引用;元素既没有被拷贝也不会被改变
for (const auto &s : text){	// 对于text 的每个元素
	cout << s; 				// 输出当前元素
	// 遇到空字符亭或者以句号结束的字符串进行换行
	if (s.empty() || s[s.size() - 1] == '.')
		cout << endl;
	else
		cout << " "; 		// 否则用空格隔开
}

if 语句的条件部分首先检查 s 是否是一个空 string,如果是,则不论右侧如何都换行;只有当 string 对象非空时才求第二个运算对象的值,也就是检查string对象是否以句号结束。

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

逻辑非运算符

逻辑非 运算符 ! 将运算对象的值取反后返回。

// 输出vec的首元素(如果有的话)
if (!vec.empty())
	cout << vec[O];

关系运算符

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

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

// 哎哟!这个条件居然拿i<j的布尔值结果和k比较
if ( i < j < k) 	// 若k大于1则为真
    
// 正确:当i小于j并且j小于k时条件为真
if (i < j && j < k)
{
	/* ... */
}

相等性测试与布尔字面值

如果想测试真值,最直接的方法就是将其作为 if 语句的条件。

if (val) { /* ... */ } // 如果val是任意的非0值,条件为真
if (!val){ /* ... */ } // 如果val是0,条件为真

在上面两个条件中,编译器都将val转换为布尔值。

如果将上面的真值测试写成如下形式:

if (val == true){ /* ... */ } 	// 只有当val等于true时条件才为真!

这种写法存在两个问题:1.写法较长而且不太直接;2.如果val不是布尔值,这样的比较就失去了原来的意义。

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

4.4 赋值运算符

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

int i = 0, j = 0, k = 0;	// 初始化而非赋值
const int ci = i;			// 初始化而非赋值

1024 = k ;					// 错误:字面值是右值
i + j = k ;					// 错误:算术表达式是右值
ci = k;						// 错误:ci是常量(不可修改的)左值
  • 如果赋值运算符的左右两个运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型。

    k = 0;			// 结果:类型是int,值是0
    k = 3.14159		// 结果:类型是int,值是3
    
  • C++11标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象:

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

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

  • 对于类类型来说,赋值运算符的细节由类本身决定。

赋值运算满足右结合律

int ival, jval;
ival = jval = 0;    // 正确:都被赋值为0

int ival, *pval;	// ival的类型是int;pval是指向int的指针
ival = pval = 0;	// 错误: 不能把指针的值赋给int
string s1, s2;
s1 = s2 = "OK";		// 字符串字面值"OK"转换成string对象

赋值运算优先级较低

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

// 这是一种形式烦琐、容易出错的写法
int i = get_value(); // 得到第一个值
while (i != 42)
{
	// 其他处理......
	i = get_value(); // 得到剩下的值
}

// 更好的写法:条件部分表达得更加清晰
int i;
while ((i = get_value()) != 42)
{
	// 其他处理......
}

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

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

if (i = j)
if (i == j)
  • 千万注意不要混淆相等运算符 == 和赋值运算符 =
    程序的这种缺陷很难被发现,好在一部分编译器会对类似的代码给出警告信息。

复合赋值运算符

复合赋值运算符包括

+=    -=    *=    /=    %=     // 算术运算符
<<=   >>=   &=    ^=    |=     // 位运算符

任意一种复合运算都完全等价于 a = a op b

唯一的区别是左侧运算对象的求值次数:使用复合赋值运算符只求值一次,使用普通的运算符则求值两次。

4.5 递增和递减运算符

递增( ++ )和递减( -- )运算符为对象加1和减1的操作提供了简洁的书写形式。这两个运算符还可应用于迭代器,因为很多迭代器本身不支持算术运算。

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

  • 前置版本:首先将运算对象加1(或减1),然后将改变后的对象作为求值结果。
  • 后置版本:也会将运算对象加1(或减1),但求值结果是运算对象改变前的值的副本。
int i = 0, j;
j = ++i;    // j = 1, i = 1: 前置版本得到递增之后的值
j = i++;    // j = 1, i = 2: 后置版本得到递增之前的值

这两种运算符必须作用于左值运算对象。前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。

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

因为后置版本需要将原始值存储下来以便于返回修改前的内容,如果我们不需要这个值,那么后置版本的操作就是一种浪费。

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

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

示例,输出一个vector对象内容直至遇到(但不包括)第一个负值为止:

auto pbeg = v.begin();
// 输出元素直至遇到第一个负值为止
while (pbeg != v.end() && *beg >= 0)
	cout << *pbeg++ << endl;	// 输出当前值并将pbeg向前移动一个元素

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

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

  • 如果返回的是加1之后的值,解引用该值将产生错误的结果。不但无法输出第一个元素,而且更糟糕的是如果序列中没有负值,程序将可能试图解引用一个根本不存在的元素。

建议:在某些语句中混用解引用和递增运算符可以使程序更简洁!

cout << *iter++ << endl;
//上面的语句要比书写下面的等价语句更简洁、也更少出错。
cout << *iter << endl;
++iter;

大多数C++程序追求简洁、摒弃冗长,因此C++程序员应该习惯于这种写法。

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

4.6 成员访问运算符

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

string s1 = "a string", *p = &s1;
auto n = s1.size(); // 运行string对象s1的size成员
n = (*p).size();    // 运行p所指对象的size成员
n = p->size();      // 等价于(*p).size()
  • 因为解引用运算符的优先级低于点运算符(*优先级低于.),所以执行解引用运算的子表达式两端必须加上括号。如果没如括号,代码的含义就大不相同了:

    //运行p的size成员,然后解引用size的结果
    *p.size (); // 错误:p是一个指针,它没有名为size的成员
    

箭头运算符作用于一个指针类型的运算对象,结果是一个左值。点运算符分为两种情况:如果成员所属的对象是左值,那么结果是左值;反之,如果成员所属的对象是右值,那么结果是右值。

4.7 条件运算符

条件运算符(? :)按照如下形式使用:

cond ? expr1 : expr2;

其中 cond 是判断条件的表达式,而 expr1expr2 是两个类型相同或可能转换为某个公共类型的表达式。条件运算符的执行过程:先求 cond 的值,如果 cond 为真则对 expr1 求值并返回该值,否则对 expr2 求值并返回该值。

示例,判断成绩是否合格:

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

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

嵌套条件运算符

条件表达式可以作为另外一个条件运算符的 condexpr

示例:

finalgrade = (grade > 90) ? "high pass"
							: (grade < 60) ? "fail" : "pass";
  • 条件运算符满足右结合律,意味着运算对象(一般)按照从右向左的顺序组合。

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

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

  • 条件运算符的优先级非常低,因此当一个长表达式中嵌套了条件运算子表达式时,通常需要在它两端加上括号。
cout << ((grade < 60) ? "fail" : "pass"); 	// 输出pass或者fail

cout << (grade < 60) ? "fail" : "pass"; 	// 输出1或者0!
// 等价于<=>
cout << (grade < 60); 			// 输出1或者0
cout ? "fail" : "pass"; 		// 根据cout的值是true还是false产生对应的字面值

cout << grade < 60 ? "fail" : "pass"; 		// 错误:试图比较cout和60
等价于<=>
cout << grade; 					// 小于运算符的优先级低于移位运算符,所以先输出grade
cout < 60 ? "fail" : "pass"; 	 // 然后比较cout和60

4.8 位运算符

位运算符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合。

位运算符(左结合律):

一般来说,如果运算对象是 “小整型”,则它的值会被自动提升成较大的整数类型。

运算对象可以是带符号的,也可以是无符号的。如果运算对象是带符号的且它的值为负,那么位运算符如何处理运算对象的 “符号位” 依赖于机器。而且此时的左移操作可能会改变符号位的值,因此是一种未定义的行为。

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

移位运算符

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

示例:

位求反运算符

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

示例:

char 类型的运算对象首先提升成 int 类型,提升时运算对象原来的位保持不变, 往 高位(high order position) 添0即可。

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

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

  • 对于 位与或运算符( & 来说,如果两个运算对象的对应位置元素都是1,则运算结果中该位为1,否则为0。
  • 对于 位或运算符( | 来说,如果两个运算对象的对应位置至少有一个为1,则运算结果中该位为1,否则为0。
  • 对于 位异或运算符( ^ 来说,如果两个运算对象的对应位置元素不同则运算结果中该位为1,否则为0。

使用位运算符

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

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

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

4.9 sizeof运算符

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

运算符的运算对象有两种形式:

sizeof (type)
sizeof expr
  • 在第二种形式中,sizeof返回的是表达式结果类型的大小。
  • 注意:sizeof并不实际计算其运算对象的值。
Sales_data data, *p;
sizeof(Sales_data); 			// 存储Sales_data类型的对象所占的空间大小
sizeof data;					// data的类型的大小,即sizeof(Sales_data)
sizeof p;						// 指针所占的空间大小
sizeof *p;						// p所指类型的空间大小,即sizeof(Sales_data)
sizeof data.revenue; 			// Sales_data的revenue成员对应类型的大小
sizeof Sales_data::revenue; 	// 另一种获取revenue大小的方式
  • sizeof 的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用。

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

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

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

4.10 逗号运算符

逗号运算符 , 含有两个运算对象,按照从左向右的顺序依次求值:首先对左侧的表达式求值,然后将求值结果丢弃;真正的结果是右侧表达式的值。

如果右侧运算对象是左值,那么最终的求值结果也是左值。

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

vector<int>::size_type cnt = ivec.size();
// 将把从size到1的值赋给ivec的元素
for(vector<int>::size_type ix = 0; 
				ix != ivec.size(); ++ix, --cnt)
    ivec[ix] = cnt;

4.11 类型运算符

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

无须程序员介入,会自动执行的类型转换被称为 隐式转换(implicit conversions)

  • 算术类型之间的隐式转换被设计得尽可能避免损失精度。

何时发生隐式类型转换

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

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

4.11.1 算数转换

算术转换(arithmetic conversion)的含义是把一种算术类型转换成另一种算术类型,其中运算符的运算对象将被转换成最宽的类型。

整型提升

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

无符号类型的运算对象

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

(祥见P142)

理解算术转换

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

bool flag; 		char cval;
short sval; 	unsignedshort usval;
int ival; 		unsigned int uival;
long lval; 		unsigned long ulval;
float fval; 	double dval;

3.14159L + 'a';	// 'a'提升成int,然后该int值转换成long double
dval + ival; 	// ival转换成double
dval + fval;	// fval转换成double
ival = dval;	// dval转换成(切除小数部分后)int
flag = dval;	// 如果dval是0,则flag是false,否则flag是true
cval + fval;	// cval提升成int,然后该int值转换成float
sval + cval;	// sval和cval都提升成int
cval + lval;	// cval转换成long
ival + ulval;	// ival转换成unsigned long
usval + ival;	// 根据unsigned short和int所占空间的大小进行提升
uival + lval;	// 根据unsigned int和long所占空间的大小进行转换

4.11.2 其他隐式类型转换

  • 数组转换成指针:在大多数表达式中,数组名字自动转换成指向数组首元素的指针。

    int ia[10]; 	// 含有10个整数的数组
    int* ip = ia;	// ia转换成指向放组首元素的指针
    

    当数组被用作decltype关键字的参数,或者作为取地址符(&)、sizeoftypeid等运算符的运算对象时,再或者用一个引用来初始化数组,上述转换不会发生。

  • 指针的转换:常量整数值0或字面值 nullptr 能转换成任意指针类型;指向任意非常量的指针能转换成 void*;指向任意对象的指针能转换成 const void*

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

    char *cp = get_string();
    if (cp) /* ... */ 		// 如果指针cp不是0,条件为真
    while (*cp) /* ... */ 	// 如果*cp不是空字符,条件为真
    
  • 转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。也就是说,如果T是一种类型,我们能将指向T的指针或引用分别转换成指向const T的指针或引用。

    int i;
    const int &j = i;	// 非常量转换成const int的引用
    const int *p = &i;	// 非常量的地址转换成const的地址
    int &r = j, *q = p;	// 错误:不允许const转换成非常量
    

    相反的类型并不存在,因为它试图删掉底层const

  • 类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。

    string s, t = "a value";	// 字符串字而位转换成string类型
    while (cin >> s)			// while的条件部分把cin转换成布尔值
    

4.11.3 显式转换

有时希望显式地将对象强制转换成另外一种类型。这种方法称为 强制类型转换(cast)

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

命名的强制类型转换

命名的强制类型转换(named cast) 具有如下形式:

cast-name<type>(expression);

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

  • dynamic_cast 支持运行时类型识别。

  • 任何具有明确定义的类型转换,只要不包含底层 const,都能使用 static_cast
    当需要把一个较大的算术类型赋值给较小的类型时,static cast 非常有用。
    static cast 对于编译器无法自动执行的类型转换也非常有用。

  • const_cast 只能改变运算对象的底层 const

    如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为。然而如果对象是一个常量,再使用const_cast执行写操作就会产生未定义的后果。
    只有 const_cast 能改变表达式的常量属性。
    const_cast 常常用于函数重载。

  • reinterpret_cast 通常为运算对象的位模式提供底层上的重新解释。

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

建议:避免使用强制类型转换

旧式的强制类型转换

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

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

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

4.12 运算符优先级表

posted @ 2021-01-23 14:50  ClimberCoding  阅读(72)  评论(0编辑  收藏  举报