4 优先级和关系运算符
目录
1. 基础
表达式由一个或多个 运算对象 组成,对表达式求值将得到一个结果。字面值和变量是最简单的表达式,其结果就是字面值和变量的值。把一个 运算符 和一个或多个运算对象组合起来可以生成较复杂的表达式。
1.1 基础概念
-
C++语言定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。当运算符作用于类类型的运算对象时,用户可以自行定义其含义——这个过程实际上是为已存在的运算符赋予了针对该类对象的另外一层含义,称之为运算符重载。重载时,只能自行定义运算对象的类型和返回值的类型,但是运算对象的个数、运算符的优先级和结合律都是无法改变的。"+"号永远都是有加的意思。
- 运算符可以直接作用于内置类型和复合类型,但是不可以直接作用于你自己定义的类类型,这就要你自己去定义运算符在该类中的含义。
- 重载运算符的优先级和结合律都和它的内置版本一致
-
C++的表达式分为 右值 和 左值 :
- 当一个对象被用作右值的时候,用的是 对象的值(内容);
- 当一个对象被用作左值的时候,用的是 对象的身份(在内存中的位置)
- 帮助记忆理解:左值可以位于赋值语句的左侧,右值则不能
-
重要原则:需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。当一个左值被当做右值使用时,实际使用的是它的内容(值)
- 赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也仍然是一个左值;
- 取地址符&作用于一个左值运算对象,返回指向该运算对象的指针,该指针是一个右值(一个地址值);
- 内置解引用运算符*、下标运算符、迭代器解引用运算符、string 和 vector 的下标运算符都返回左值;
- 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本返回左值,后置版本返回右值。
-
如果 decltype 作用于一个求值结果是左值的表达式(非单个变量),会得到一个引用类型。
- 假定p的类型是int*,因为解引用运算符返回左值,所以decltype(*p)的结果是int&;
//decltype的表达式如果是加上括号的变量,结果将是引用
int i = 42;
decltype((i)) d; //错误:d是int&,必须初始化
decltype(i) e; //正确:e是一个(未初始化的)int
- 如果 decltype 作用于一个求值结果是右值的表达式(非单个变量),会得到一个与表达式相同的类型
- 假定p的类型是int*,因为取地址运算符生成右值,所以decltype(&p)的结果是int**,即结果是一个指向整型指针的指针。
1.2 优先级与结合律
- 复合表达式 指含有两个或多个运算符的表达式。运算符和运算对象合理地组合在一起,优先级与结合律决定了运算对象的组合方式,高优先级运算符先运行(乘法和除法,然后加法和减法),如果优先级相同,则其组合规则由结合律确定,从左向右顺序运行。
- 括号无视优先级与结合律,表达式中括号括起来的部分被当成一个单元来求值,然后再与其他部分一起按照优先级组合。
1.3 求值顺序
- 大多数运算符只是对外规定了运算符间的优先级,对内都没有规定其运算对象的求值顺序,所以运算对象可按任意顺序求值
int i= f1() * f2(); //无法知道f1和f2哪个先调用
- 对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为
int i = 0;
cout << i << " " << ++i << endl; // 未定义的
- 有 4 种运算符明确规定了运算对象的求值顺序:
- 逻辑与 ( && ) 运算符
- 逻辑或 ( || ) 运算符
- 条件 ( ? : ) 运算符
- 逗号 ( , ) 运算符
- 处理复合表达式时建议遵循以下两点是有益的:
- 不确定求值顺序时最好使用括号来强制让表达式的组合关系符合程序逻辑的要求;
- 如果表达式改变了某个运算对象的值,则在表达式的其他位置不要再使用这个运算对象。
- 不过,第2条规则有一个重要例外,当改变运算对象的子表达式本身就是另一个子表达式的运算对象时,规则无效。例如表达式 *++iter,递增运算符改变了 iter 的值,而改变后的 iter 的值又是解引用运算符的运算对象。这种或者类似情况下,求值的顺序不会成为问题,因为递增运算(即改变运算对象的子表达式)必须先求值,然后才轮到解引用运算
2. 算术运算符
- 算术运算符(按照运算符的优先级排序):
- 上面的所有运算符都满足左结合律, 意味着当优先级相同时按照从左向右的顺序进行组合
- 取余运算符%:计算整数相除的余数,运算对象必须是整数类型
int ival = 42;
double dval = 3.14;
ival % 12; // 正确:结果是6
ival % dval; // 错误:运算对象是浮点型
- 除法运算:整数相除( / )结果还是整数,即直接弃除商的小数部分;
- 在除法运算中,C++语言的早期版本允许结果为负数的商向上或向下取整,C++11新标准则规定商一律向0取整(即直接去除小数部分)。
- 总结:
- 商无论是正值还是负值,一律向0方向取整:(-m)/n = m/(-n)=-(m/n)
- 对于取余操作,m % (-n) = m % n, (-m) % n = -(m % n)
21 % 6; /* 结果是3 */ 21 / 6; /* 结果是3 */
21 % 7; /* 结果是0 */ 21 / 7; /* 给果是3 */
-21 % -8; /* 结果是-5 */ -21 / -8; /* 结果是2 */
21 % -5; /* 结果是1 */ 21 / -5; /* 结果是-4 */
3. 逻辑和关系运算符
- 关系运算符作用于算术类型和指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。
- 逻辑与运算符 && 和 逻辑或运算符 || 都是先计算左侧运算对象的值再计算右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会去计算右侧运算对象的值,这种策略称为 短路求值。
- 对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值。
- 对于逻辑或运算符来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。
// s 是对常量的引用;元素既没有被拷贝也不会被改变
for (const auto &s : text){ // 对于text 的每个元素
cout << s; // 输出当前元素
// 遇到空字符串或者以句号结束的字符串进行换行
if (s.empty() || s[s.size() - 1] == '.')
cout << endl;
else
cout << " "; // 否则用空格隔开
}
/*
if 语句的条件部分首先检查 s 是否是一个空 string,如果是,则不论右侧如何都换行;只有当 string 对象非空时才求第二个运算对象的值,即是否是以句号结束。
*/
- 逻辑非运算符 ! 将运算对象的值取反后返回
// 输出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 (val)
{
/* ... */
} // 如果val是任意的非0值,条件为真
if (!val)
{
/* ... */
} // 如果val是0,条件为真
if (val == true)
{
/* ... */
} // 只有当val等于true时条件才为真!
if (val == 1)
{
/* ... */
} // 只有当val等于1时条件才为真!
- 进行比较运算时,除非比较的对象是布尔类型,否则不要使用布尔字面值 true 和 false 作为运算对象。
4. 赋值运算符
- 赋值运算符 = 的左侧运算对象必须是一个 可修改的左值。
int i = 0, j = 0, k = 0; // 初始化而非赋值
const int ci = i; // 初始化而非赋值
1024 = k ; // 错误:字面值是右值
i + j = k ; // 错误:算术表达式是右值
ci = k; // 错误:ci是常量(不可修改的)左值
- 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)
{
// 其他处理......
}
- 注意:不要混淆相等运算符 == 和赋值运算符 =
if (i = j)
if (i == j)
- 复合赋值运算符包括 +=、-=、*=、/=、%=(算术运算符);<<=、>>=、&=、^= 和 |=(位运算符)。任意一种复合运算都完全等价于 a = a op b。
5. 递增和递减运算符
- 递增(++)和递减(--)运算符是为对象加1或减1的简洁书写形式。这两个运算符还可应用于迭代器,因为很多迭代器本身不支持算术运算。
- 递增和递减运算符分为前置版本和后置版本:
- 前置版本:首先将运算对象加1(或减1),然后将改变后的对象作为求值结果。
- 后置版本:也会将运算对象加1(或减1),但求值结果是运算对象改变前的值的副本。
int i = 0, j;
j = ++i; // j = 1, i = 1: 前置版本得到递增之后的值
j = i++; // j = 1, i = 2: 后置版本得到递增之前的值
- 优先使用前置版本
- 除非必须,否则不应该使用递增或递减运算符的后置版本。因为后置版本需要将原始值存储下来以便于返回修改前的内容,如果我们不需要这个值,那么后置版本的操作就是一种浪费。
- 对于整数和指针类型来说,编译器可能对这种额外的工作进行一定的优化;但是对于相对复杂的迭代器类型,这种额外的工作就消耗巨大了。建议养成使用前置版本的习惯,这样不仅不需要担心性能的问题, 而且更重要的是写出的代码会更符合编程的初衷。
- 解引用和后置版本的配合使用
- 一般情况下我们都是用前置版本。但如果我们想在一条复合表达式中既将变量加1或减1的同时又能使用它原来的值,这时就可以用递增递减运算符的后置版本——当我们要用到原始值时
auto pbeg = v.begin();
// 输出元素直至遇到第一个负值为止
while (pbeg != v.end() && *beg >= 0)
// 输出当前值并将pbeg向前移动一个元素
cout << *pbeg++ << endl;
/*
后置递增运算符的优先级高于解引用运算符,因此 *pbeg++ 等价于 *(pbeg++)。
pbeg++ 把 pbeg 的值加1, 然后返回 pbeg 的初始值的副本作为其求值结果,此时解引用运算符的运算对象是 pbeg 未增加之前的值。
最终,这条语句输出 pbeg 开始时指向的那个元素,并将指针向前移动一个位置。
如果返回的是加1之后的值,解引用该值将产生错误的结果。
不但无法输出第一个元素,而且更糟糕的是如果序列中没有负值,程序将可能试图解引用一个根本不存在的元素。
*/
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的成员
7. 条件运算符
- 条件运算符(? :)的使用形式为:cond ? expr1 : expr2;
- 其中 cond 是判断条件的表达式,而 expr1 和 expr2 是两个类型相同或可能转换为某个公共类型的表达式。先求 cond 的值,如果 cond 为真则对 expr1 求值并返回该值,否则对 expr2 求值并返回该值。
string finalgrade = (grade < 60) ? "fail" : "pass";
- 只有当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果才是左值,否则运算的结果就是右值。
- 嵌套条件运算符
- 条件运算符可以嵌套,但是考虑到代码的可读性,运算的嵌套层数最好不要超过两到三层。
- 条件运算符满足右结合律,意味着运算对象(一般)按照从右到左的顺序组合(但运算顺序是从左到右)
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
8. 位运算符
- 位运算符(左结合律):
- 一般来说,如果运算对象是“小整型”,则它的值会被自动提升成较大的整数类型。运算对象可以是带符号的,也可以是无符号的。
- 在位运算中符号位如何处理并没有明确的规定,所以强烈建议仅将位运算符用于无符号类型的处理。
- 移位运算符:<<和>>
- 左移运算符 << 在运算对象右侧插入值为0的二进制位,右移运算符 >> 的行为依赖于其左侧运算对象的类型:如果该运算对象是无符号类型,在其左侧插入值为0的二进制位;如果是带符号类型,在其左侧插入符号位的副本或者值为0的二进制位,如何选择视具体环境而定。
- 之前在处理输入和输出操作时,使用的<<和>>运算符实际上是标准库定义的重载版本。
- 移位运算符满足左结合律,优先级不高不低,介于中间:比算术运算符的优先级低,但比关系运算符、赋值运算符和条件运算符的优先级高。
- 左移运算符 << 在运算对象右侧插入值为0的二进制位,右移运算符 >> 的行为依赖于其左侧运算对象的类型:如果该运算对象是无符号类型,在其左侧插入值为0的二进制位;如果是带符号类型,在其左侧插入符号位的副本或者值为0的二进制位,如何选择视具体环境而定。
cout << 42 + 10; // 正确:+的优先级更高,因此输出求和结果
cout << (10 < 42); // 正确:括号使运算对象按照我们的期望组合在一起,输出1
cout << 10 < 42; // 错误:试图比较cout和42!
// 等价于<=>
// (cout << 10) < 42;
- 位求反运算符:~
- 将运算对象逐位求反而生成一个新值,将1置为0、将0置为1。
- char类型的运算对象首先提升成int类型,提升时运算对象原来的位保持不变,往高位添加0即可,之后将提升后的值逐位取反。
- 将运算对象逐位求反而生成一个新值,将1置为0、将0置为1。
- 位与&、位或|、位异或^运算符:在两个运算对象上逐位执行相应的逻辑操作。
9. sizeof运算符
- sizeof 运算符返回一个表达式或一个类型名字所占的字节数,满足右结合律,返回值是 size_t 类型的常量表达式。该运算符的运算对象有两种形式:
sizeof (type)
sizeof expr
- 在第二种形式中,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 的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用
- sizeof 运算符的结果部分依赖于其作用的类型:
- 对 char 或者类型为 char 的表达式执行 sizeof 运算,返回值为1。
- 对引用类型执行 sizeof 运算得到被引用对象所占空间的大小。
- 对指针执行 sizeof 运算得到指针本身所占空间的大小。
- 对解引用指针执行 sizeof 运算得到指针指向的对象所占空间的大小,指针不需要有效。
- 对数组执行 sizeof 运算得到整个数组所占空间的大小。
- 对 string 或 vector 对象执行 sizeof 运算只返回该类型固定部分的大小,不会计算对象中元素所占空间的大小。
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;
11. 类型转换
- 在C++语言中,某些类型之间有关联。如果两种类型有关联, 那么当程序需要其中一种类型的运算对象时,可以用另一种关联类型的对象或值来替代。换句话说,如果两种类型可以相互转换,那么它们就是关联的。
- 无须程序员介入,会自动执行的类型转换叫做 隐式转换,可以尽可能地避免损失精度。
- 何时发生隐式类型转换。在下面这些情况下,编译器会自动地转换运算对象的类型:
- 在大多数表达式中,比 int 类型小的整型值首先提升为较大的整数类型。
- 在条件中,非布尔值转换成布尔类型。
- 在初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
- 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。
- 函数调用时也会发生类型转换。
11.1 算术转换
- 把一种算术类型转换成另一种算术类型叫做 算术转换,其中运算符的运算对象将被转换成最宽的类型。
- 整型提升 负责把小整数类型转换成较大的整数类型。
- 如果某个运算符的运算对象类型不一致, 这些运算对象将转换成同一种类型。但是如果某个运算对象的类型是无符号类型,那么转换的结果就要依赖于机器中各个整数类型的相对大小了。
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所占空间的大小进行转换
11.2 其他隐式类型转换
- 数组转换成指针:在大多数表达式中,数组名字自动转换成指向数组首元素的指针。
int ia[10]; // 含有10个整数的数组
int* ip = ia; // ia转换成指向放组首元素的指针
- 指针的转换:
- 常量整数值0或字面值 nullptr 能转换成任意指针类型;
- 指向任意非常量的指针能转换成 void*;
- 指向任意对象的指针能转换成 const void*。
- 转换成布尔类型:
- 任意一种算术类型或指针类型都能转换成布尔类型。
- 如果指针或算术类型的值为0,转换结果是 false,否则是 true。
char *cp = get_string();
if (cp) /* ... */ // 如果指针cp不是0,条件为真
while (*cp) /* ... */ // 如果*cp不是空字符,条件为真
- 转换成常量:
- 允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。
int i;
const int &j = i; // 非常量转换成const int的引用
const int *p = &i; // 非常量的地址转换成const的地址
int &r = j, *q = p; // 错误:不允许const转换成非常量
- 类类型定义的转换:
- 类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换
string s, t = "a value"; // 字符串字面值转换成string类型
while (cin >> s) // while的条件部分把cin转换成布尔值
11.3 显式类型转换
- 显式类型转换 也叫做 强制类型转换(cast)。
int i, j;
double slope = i/j;
- 虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的。建议尽量避免强制类型转换。
- 命名的强制类型转换 形式如下:
cast-name<type>(expression);
- 其中 type 是转换的目标类型,expression 是要转换的值。如果 type 是引用类型,则转换结果是左值。cast-name 是 static_cast、dynamic_cast、const_cast 和 reinterpret_cast 中的一种,用来指定转换的方式。
static_cast
-(1)任何具有明确定义的类型转换,只有不包含底层const,都可以使用static_cast
// 进行强制类型转换以便执行浮点数除法
double slope = static_cast<double>(j) / i;
- (2)当需要把一个较大的算术类型赋值给较小的类型时(如果没有显式转换,编译器会给出警告信息),static_cast非常有用。它会让编译器知道我们不在乎潜在的精度损失,而关闭警告信息。
- (3)static_cast对于编译器无法自动执行的类型转换也非常有用。
/*
我们可以使用static_cast找回存在于void*指针中的值
*/
void *p = &d; // 正确:任何非常量对象的地址都能存入void*
// 正确:将void*转换为初始的指针类型
double *dp=static_cast<double*>(p);
/*
当我们把指针存放在void*中,并且使用static_cast将其强制转换回原来的类型时,
应该确保转换后所得的类型就是原来指针所指的类型。
类型一旦不符,将产生未定义的后果
*/
const_cast
- (1)const_cast只能改变运算对象的底层const。
const char* pc;
char *p=const_cast<char*>(pc); //正确:但是通过p写值是未定义的行为
- 将常量对象转换成非常量对象的行为,被称为去掉const性质。
- (2)只有const_cast能改变表达式的常量属性**,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误。同样的,也不能用const_cast 改变表达式的类型
const char* cp;
char *q=static_cast<char*>(cp); // 错误:static_cast不能转换掉const性质
staic_cast<string>(cp); // 正确:字符串字面值转换成string类型
const_cast<string>(cp); // 错误:const_cast只改变常量属性
- const_cast 常常用于有函数重载的上下文中
reinterpret_cast
- 通常为运算对象的位模式提供较低层次的重新解释。尽量不使用。主要是指针之间的转换
int *ip;
char *pc = reinterpret_cast<char*>(ip);
// pc所指的真实对象是一个int而非字符。
// 如果把pc当做普通的字符指针使用就可能在运行时发生错误。
// 例如:string str(pc); 可能导致异常的运行时行为
- reinterpret_cast 本质上依赖于机器。要想安全地使用reinterpret_cast 必须对涉及的类型和编译器实现转换的过程都非常了解。
- 使用reinterpret_cast时非常危险
总结
- dynamic_cast 支持运行时类型识别。
- static_cast:任何具有明确定义的类型转换,只要不包含底层 const,都能使用 static_cast。当需要把一个较大的算术类型赋值给较小的类型时,static cast 非常有用。static cast 对于编译器无法自动执行的类型转换也非常有用。
- const_cast 只能改变运算对象的底层 const,同时也只有 const_cast 能改变表达式的常量属性。const_cast 常常用于函数重载。
- reinterpret_cast 通常为运算对象的位模式提供底层上的重新解释。
旧式的强制类型转换
- 早期版本的C++语言中,显式类型转换包含两种形式:
type (expression); // 函数形式的强制类型转换
(type) expression; // C语言风格的强制类型转换
- 与命名的强制类型转换相比,旧式的强制类型转换从表现形式上来说不那么清晰明了了,容易被看漏,所以一旦转换过程出现问题,追踪起来也更加困难。
12. 运算符优先级表