第4章:表达式

  • #1.基础
    • 1.1 基本概念
    • 1.2 优先级和结合律
    • 1.3 求值顺序
  • #2.算术运算符
  • #3.逻辑和关系运算符
  • #4.赋值运算符
  • #5.递增和递减运算符
  • #6.成员访问运算符
  • #7.条件运算符
  • #8.位运算符
  • #9.sizeof运算符
  • #10.逗号运算符
  • #11.类型转换
    • 11.1 算术转换
    • 11.2 其他隐式类型转换
    • 11.3 显示转换

表达式由一个或多个运算对象组成,对表达式求值将得到一个结果。字面值和变量是最简单的表达式,其结果就是字面值和变量的值。

#1. 基础

1.1 基本概念

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

组合运算符和运算对象

对于含有多个运算符的复杂表达式来说,要理解它的含义首先要理解运算符的优先级、结合律以及运算对象的求值顺序。

运算对象转换

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

重载运算符

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

左值和右值

C++的表达式要不然是右值,要不然就是左值。当一个对象被用作右值的时候,用的是对象的值;当对象被用作左值的时候,用的是对象的身份(对象在内存中的位置)。一个重要的原则是在需要右值的地方可以用左值来代替,但是不能把右值当作左值使用。

1.2 优先级和结合律

复合表达式是指含有两个或多个运算符的表达式。高优先级运算符的运算对象比低优先级运算符的运算对象更为紧密的结合在一起。如果优先级相同,则其组合规则由结合律确定。

括号无视优先级和结合律

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

//不同括号的组合导致不同的组合结果
cout << (6+3) * (4/2+2) << endl; //36
cout << ((6 + 3) * 4) / 2 + 2 << endl; //20

1.3 求值顺序

优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序进行求值。对于那些没有指定执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为:

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

处理复合表达式经验准则:

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

#2. 算术运算符

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

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


#3. 逻辑和关系运算符

关系运算符作用于算术类型或指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。值为0的运算对象表示假,否则为真。

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

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

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

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

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

关系运算符

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


#4. 赋值运算符

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

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

赋值运算的结果是它的左侧运算对象,并且是一个左值。

赋值运算满足右结合律
int ival,jval;
ival = jval = 1; //正确:都被赋值为1

因为赋值运算符满足右结合律,所以靠右的赋值运算jval=1作为靠左的赋值运算符的右侧运算对象。

赋值运算优先级较低

赋值语句经常会出现在条件当中。因为赋值运算的优先级相对较低,所以通常需要给赋值部分加上括号使其符合我们的原意。

int i;
while((i = get_vaule()) != 42) {
    ...
}

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

复合赋值运算符
int sum = 0;
for(int val = 1;val <= 10;++val) {
    sum += val; //等价于 sum = sum + val;
}

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

#5. 递增和递减运算符

递增运算符(++)和递减运算符(--)为对象加1和减1操作提供了一种简洁的书写形式。

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

  • 前置版本,这种形式的运算符首先将运算对象加1(或减1),然后将改变后的对象作为求值的结果。
  • 后置版本也会将运算对象加1(或减1),但是求值结果是运算对象改变以前那个值的副本。
int i = 0,j;
j = ++i; //j = 1,i = 1:前置版本得到递增之后的值
j = i++; //j = 1,i = 2:后置版本得到递增之前的值
在一条语句中混用解引用和递增运算符
auto pbeg = v.begin();
while(pbeg != v.end() && *beg >= 0) {
    cout << *pbeg++ << endl; //输出当前值并将pbeg向前移动一个元素
}

后置递增运算符的优先级高于解引用运算符,因此*pbeg++等价于*(pbeg++)。pbeg++把pbeg的值加1,然后返回pbeg的初始值的副本作为求值结果,此时解引用运算符对象是pbeg未增加之前的值。

运算对象可按任意顺序求值
for(auto it = s.begin();it != s.end() && !isspace(*it);++it) {
    *it = toupper(*it); //将当前字符改成大写形式
}

while(beg != s.end() && !isspace(*beg)) {
    *beg = toupper(*beg++); //错误:该赋值语句未定义
    //编译器可能按照下面的任意一种思路处理该表达式:
    //*beg = toupper(*beg); //如果先求左侧的值
    //*(beg + 1) = toupper(*beg); //如果先求右侧的值
}

#6. 成员访问运算符

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

string s1 = "hello world",*p = &s1;
auto n = s1.size();
n = (*p).size(); //运行p所指对象的size成员
n = p->size(); //等价于(*p).size

因为解引用运算符的优先级低于点运算符,所以执行解引用运算符的子表达式两端必须加上括号。


#7. 条件运算符

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

cond? expr1 : expr2

#8. 位运算符

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

运算符 功能 用法
~ 位求反 ~expr
<< 左移 expr1 << expr2
>> 右移 expr1 >> expr2
& 位与 expr & expr
^ 位异或 expr ^ expr
位或 expr | expr
移位运算符
unsigned char bits = 0233; [10011011]
bits << 8 //bits提升成int类型,然后向左移动8位
[00000000 00000000 10011011 00000000]
bits << 31 //向左移动31位,左边超出边界的位丢弃掉了
[10000000 00000000 00000000 00000000]
bits >> 3 //向右移动3位,最右边的3位丢弃掉了
[00000000 00000000 00000000 00010011]

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

位求反运算符

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

unsigned char bits = 0227;[10010111]
~bits
[11111111 11111111 11111111 01101000]

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

位与、位或、位异或运算符
unsigned char b1 = 0145;[01100101]
unsigned char b2 = 0257;[10101111]
b1 & b2
[00000000 00000000 00000000 00100101]
b1 | b2
[00000000 00000000 00000000 11101111]
b1 ^ b2
[00000000 00000000 00000000 11001010]
  • 位与运算符(&),如果两个运算对象的对应位置都是1则运算结果中该位为1,否则为0。
  • 位或运算符(|),如果两个运算对象的对应位置至少有一个为1则运算结果中该位为1,否则为0。
  • 位异或运算符(^),如果两个运算对象对应位置有且只有一个为1则运算结果中该位为1,否则为0。[==不同为真, 相同为假==]

#9. sizeof运算符

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

sizeof (type)
sizeof expr

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

  • 对char或者类型为char的表达式执行sizeof运算,结果得1。
  • 对引用类型执行sizeof运算得到被引用对象所占空间的大小。
  • 对指针执行sizeof运算得到指针本身所占空间的大小。
  • 对解引用指针执行sizeof运算得到指针所指向对象所占空间的大小,指针不需有效。
  • 对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有元素各执行一次sizeof运算并将结果求和。注意,sizeof不会把数组转换成指针处理。
  • 对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。

#10. 逗号运算符

逗号运算符含有两个运算对象,按照从左向右的顺序依次求值。

vector::size_type cnt = ivec.size();
for(vector::size_type ix = 0;ix != ivec.size();++ix,--cnt) {
    ivec[ix] = cnt;
}

#11. 类型转换

在C++语言中,某些类型之间有关联。如果两种类型可以相互转换,那么它们就是关联的。

int ival = 3.14 + 3; //编译器可能会警告该运算损失了精度
何时发生隐式类型转换
  • 在大多数表达式中,比int类型小的整形值首先提升为较大的整数类型。
  • 在条件中,非布尔值转换成布尔类型。
  • 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
  • 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型。

11.1 算术转换

算术转换的含义是把一种算术类型转换为另外一种算术类型。

整型提升

整型提升负责把小整数类型转换为较大的整数类型。

无符号类型的运算对象

如果某个运算符的运算对象不一致,这些运算对象将转换成同一类型。如果一个运算对象是无符号类型、另一个运算对象是带符号类型,而且其中的无符号类型不小于带符号类型,那么带符号类型的运算对象转换成无符号的。例如,假设两个类型分别是unsigned int和int,则int类型的运算对象将转换为unsigned int。

11.2 其他隐式类型转换

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

int ia[10];
int *p = ia; //ia转换为数组首元素的指针 

指针的转换:C++规定了几种其他的指针转换方式,包括常量整数值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的地址

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

11.3 显示转换

命名的强制类型转换

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

cast-name(expression);

其中,type是转换的目标类型而exppression是要转换的值。如果type是引用类型,则结果是左值。cast-name是static-castdynamic-castconst-castreinterpret-cast中的一种。

static_cast

任何具有明确定义的类型转换,只要不包含底层的const,都可以使用static_const。

//进行强制类型转换以便执行浮点数除法
double slope2 = static_cast(j) / i;

static_cast对于编译器无法自动执行的类型转换也非常有用。

double d = 3.14;
void *p = &d; //正确:任何非常量的地址都能存入void*
double *dp = static_cast(p); //正确:将void*转换为初始指针类型
const_cast

const_cast只能改变运算对象的底层cast[==指针所指向的对象是常量==]。

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

对于将常量对象转换成非常量对象的行为,我们一般称其为“去掉const性质”。一旦我们去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。

const char *cp;
//错误:static_cast不能转换掉const性质
char *p = static_cast(cp);
static_cast(cp); //正确:字符串字面值转换成string类型
const_cast(cp); //错误:const_cast只能改变常量属性
reinterpret_cast

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

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

你可能感兴趣的:(第4章:表达式)