本文讲解C语言中的运算符、表达式、语句及其相关的知识。
文章目录- 1. 概述
- 2. 运算符
- 2.1 赋值运算符
- 2.2 算术运算符
- 2.3 比较运算符
- 2.4 逻辑运算符
- 2.5 位运算符
- 2.6 移位运算符
- 3. 表达式与语句
- 4. 运算符的优先级
- 5. 类型转换
- 参考资料
什么是运算符?其实我们并不陌生,从小到大已经接触得非常多了,对其已经非常熟悉了,所以不要害怕。就举一个最简单的例子,加法就是一个数学运算符,当然,在C语言中加法也是一种运算符,除此之外,还存在许多其他的运算符,接下来我们都要一 一学习。
什么是表达式?表达式包括运算符和操作数。
什么是语句?语句是编程语言中特有的,最简单的理解,语句就是表达式后面加上一个英文分号。
2. 运算符 2.1 赋值运算符赋值运算符就是最常见、最简单的运算符,为一个等号 = :
| 运算符 | 名称 | 说明 | 例子 |
|---|---|---|---|
| = | 赋值运算符 | 将一个值赋给一个变量 变量在运算符左边 | int a = 1; |
算术运算符与数学中的四则运算、求余数运算相同,主要有以下运算符:
| 运算符 | 名称 | 说明 | 例子 括号内为结果 |
|---|---|---|---|
| + | 加法运算符 | 双目运算符 将两个操作数相加 | 1 + 2(3) |
| − - − | 减法运算符 | 双目运算符 左边的操作数减去右边的操作数 | 1 - 2(-1) |
| * | 乘法运算符 | 双目操作符 将两个操作数相乘 | 2 * 3(6) |
| / | 除法运算符 | 双目运算符 左边的操作数除以右边的操作数 | 2 / 3(0) |
| % | 求余运算符 | 双目操作符 左边的操作数除以右边的操作数后的余数,读作模 | 2 % 3(2) |
关于上述的算术运算符,有几个需要我们注意的点:
-
对于操作数,可以为字面量、变量或者表达式,或者函数;
-
除法运算符:括号内的结果是0,这与我们的认知不同;
-
求余运算符:C语言是如何求余数的,如果涉及到负数,余数该如何求呢?
除法运算符
在C语言中,如果除法的两个操作数是整数,那么结果也为整数,并且结果是直接省略掉小数部分。
# includeint main() { int a = 10 / 3; // 3.3333... int b = 3 / 2; // 1.5 int c = 2 / 3; // 0.6666... int d = -10 / 3; // -3.333... int e = -3 / 2; // -1.5 int f = -2 / 3; // -0.6666... printf("a = %dn",a); printf("b = %dn",b); printf("c = %dn",c); printf("d = %dn",d); printf("e = %dn",e); printf("f = %dn",f); return 0; }
结果:
a = 3 b = 1 c = 0 d = -3 e = -1 f = 0
求余运算符
在C语言中,我们要明确这么一个事实:
(
a
/
b
)
∗
b
+
a
%
b
=
a
(a/b)*b + a%b = a
(a/b)∗b+a%b=a
所以,a%b的值等于 a - (a/b)*b。
如果a%b中,a和b都是非负数,那么结果为非负数。如果a和b中至少存在一个负数,那么结果的符号依赖于实现。
# includeint main() { int a = 10 % 3; int b = -10 % 3; int c = 10 % -3; int d = -10 % -3; printf("a = %dn",a); printf("b = %dn",b); printf("c = %dn",c); printf("d = %dn",d); return 0; }
结果:
a = 1 b = -1 c = 1 d = -1
除了上述的算术运算符外,还有以下两个运算符需要注意:
| 运算符 | 名称 | 说明 | 例子 |
|---|---|---|---|
| ++ | 自增运算符 | 单目运算符 将变量的值+1 | i++ ++i |
| -- | 自减运算符 | 单目运算符 将变量的值-1 | i-- --i |
注意:操作数必须为变量。
关于自增(自减)运算符,如果运算符和操作数的位置不同,则结果不同:
- i++:后置自增运算符
- ++i:前置自增运算符
我们先看以下两段程序,来明确前置和后置的不同:
// 第一段程序 # includeint main() { int i = 1; int j = ++i; printf("i = %dnj = %d",i,j); return 0; }
结果:i = 2 , j = 2
# includeint main() { int i = 1; int j = i++; printf("i = %dnj = %d",i,j); return 0; }
结果:i = 2 , j = 1
先说前置和后置自增运算符的执行顺序:
对于前置运算符:
- 首先将i+1,则i等于2;
- 然后将i赋值给j,则j=2;
对于后置运算符:
- 首先将i赋值给j,则j=1;
- 然后将i+1,则i等于2;
关于执行顺序,我们可以通过查看Dev-cpp编译器中调试模式的查看CPU窗口,获取汇编代码(对于入门者,以下内容可略过,我也不太懂汇编代码wwwwwww,所以此处不讲解,大家可以自行查找资料进行了解,或者参考java语言版的博文,网上讲解得比较多):
MORE:关于Dev-cpp如何开启调试模式,请参考资料[1]。
对于第一段程序,汇编语言如下:
0x0000000000401530 <+0>: push %rbp 0x0000000000401531 <+1>: mov %rsp,%rbp 0x0000000000401534 <+4>: sub $0x30,%rsp 0x0000000000401538 <+8>: callq 0x4020f0 <__main> 0x000000000040153d <+13>: movl $0x1,-0x4(%rbp) 0x0000000000401544 <+20>: addl $0x1,-0x4(%rbp) 0x0000000000401548 <+24>: mov -0x4(%rbp),%eax 0x000000000040154b <+27>: mov %eax,-0x8(%rbp) => 0x000000000040154e <+30>: mov -0x8(%rbp),%edx 0x0000000000401551 <+33>: mov -0x4(%rbp),%eax 0x0000000000401554 <+36>: mov %edx,%r8d 0x0000000000401557 <+39>: mov %eax,%edx 0x0000000000401559 <+41>: lea 0x2aa0(%rip),%rcx # 0x404000 0x0000000000401560 <+48>: callq 0x402b080x0000000000401565 <+53>: mov $0x0,%eax 0x000000000040156a <+58>: add $0x30,%rsp 0x000000000040156e <+62>: pop %rbp 0x000000000040156f <+63>: retq
对于第二段程序,汇编语言如下:
0x0000000000401530 <+0>: push %rbp 0x0000000000401531 <+1>: mov %rsp,%rbp 0x0000000000401534 <+4>: sub $0x30,%rsp 0x0000000000401538 <+8>: callq 0x402100 <__main> 0x000000000040153d <+13>: movl $0x1,-0x4(%rbp) 0x0000000000401544 <+20>: mov -0x4(%rbp),%eax 0x0000000000401547 <+23>: lea 0x1(%rax),%edx 0x000000000040154a <+26>: mov %edx,-0x4(%rbp) 0x000000000040154d <+29>: mov %eax,-0x8(%rbp) => 0x0000000000401550 <+32>: mov -0x8(%rbp),%edx 0x0000000000401553 <+35>: mov -0x4(%rbp),%eax 0x0000000000401556 <+38>: mov %edx,%r8d 0x0000000000401559 <+41>: mov %eax,%edx 0x000000000040155b <+43>: lea 0x2a9e(%rip),%rcx # 0x404000 0x0000000000401562 <+50>: callq 0x402b182.3 比较运算符0x0000000000401567 <+55>: mov $0x0,%eax 0x000000000040156c <+60>: add $0x30,%rsp 0x0000000000401570 <+64>: pop %rbp 0x000000000040 1571 <+65>: retq
比较运算符用于确定两个值是否相等或大小关系,结果为一个bool值:
| 运算符 | 名称 | 说明 | 例子(括号内为结果) |
|---|---|---|---|
| == | 等于运算符 | 双目运算符 判断两个操作数是否相等,相等返回true | 1 == 2(false) |
| != | 不等于运算符 | 双目运算符 判断两个操作数是否不相等,不相等返回true | 1 != 2(true) |
| > | 大于运算符 | 双目运算符 判断左边的操作数是否大于右边的操作数,是则返回true | 1 > 1(false) |
| >= | 大于等于运算符 | 双目运算符 判断左边的操作数是否大于等于右边的操作数,是则返回true | 1 >= 1(true) |
| < | 小于运算符 | 双目运算符 判断左边的操作数是否小于右边的操作数,是则返回true | 1 < 1(false) |
| <= | 小于等于运算符 | 双目运算符 判断左边的操作数是否小于等于右边的操作数,是则返回true | 1 <= 1(true) |
逻辑运算符有与(&&)、或(||)和非(!)。
逻辑与运算符是双目运算符,需要两个布尔操作数,其返回值是一个布尔值,情形列表如下:
| 情形 | 结果 |
|---|---|
| true && true | true |
| true && false | false |
| false && true | false |
| false && false | false |
逻辑或运算符也是双目运算符,需要两个布尔操作数,其返回值是一个布尔值,情形列表如下:
| 情形 | 结果 |
|---|---|
| true || true | true |
| true || false | true |
| false || true | true |
| false || false | false |
逻辑非操作符是单目运算符,需要一个布尔操作数,其返回值是一个布尔值,情形列表如下:
| 情形 | 结果 |
|---|---|
| ! true | false |
| ! false | true |
总结:
-
逻辑运算符的操作数是布尔值,可以为布尔字面量,布尔变量,或者返回布尔结果的表达式或函数;在C语言中,特殊的是将0作为false,将非0作为true,所以也可以用数字字面量、数字变量、表达式或函数来做操作数;
-
对于逻辑与操作符,只有两个操作数为true,结果才为true;
-
对于逻辑或操作符,只有两个操作数为false,结果才为false;
短路原则
所谓短路原则,就是对于逻辑与和逻辑或操作符,只要第一个操作数就能确定最终结果了,那么就无需判断第二个操作数了。
例如,当情形 false && X,那么无论X为true或false,结果都为false,此时就不用再看X是什么值了。
要注意短路原则在代码中的影响:
# includeint main() { int a = 1; bool res = false && (a = 3) > 1; printf("a = %d",a); return 0; }
结果:
a = 1
由于短路原则,赋值语句a = 3并未执行。
2.5 位运算符**位运算符是将操作数的对应比特位的值(也就是0或1),按照一定规则进行计算,得到结果对应比特位的值。**位运算符与逻辑运算符有很多相似的地方。位运算符包括以下四种:与&、或|、取反~、异或^。
位运算符与&结果对应表:
| 情形 | 结果 |
|---|---|
| 1 & 1 | 1 |
| 1 & 0 | 0 |
| 0 & 1 | 0 |
| 0 & 0 | 0 |
位运算符或|结果对应表:
| 情形 | 结果 |
|---|---|
| 1 | 1 | 1 |
| 1 | 0 | 1 |
| 0 | 1 | 1 |
| 0 | 0 | - |
位运算符取反~结果对应表:
| 情形 | 结果 |
|---|---|
| ~0 | 1 |
| ~1 | 0 |
位运算符异或^结果对应表:
| 情形 | 结果 |
|---|---|
| 1 ^ 1 | 0 |
| 1 ^ 0 | 1 |
| 0 ^ 1 | 1 |
| 0 ^ 0 | 0 |
例如,计算10 & 2,10 | 2,~10和10 ^ 2的值。我们以八个比特位来存储数据,以无符号整数来解释二进制。
0000 1010 0000 0010 & 0000 0010 = 2 | 0000 1010 = 10 ~ 1111 0101 = 245 ^ 0000 1000 = 8
程序验证如下:
#includeint main() { unsigned char a = 10; unsigned char b = 2; unsigned char c = a & b; unsigned char d = a | b; unsigned char e = ~a; unsigned char f = a ^ b; printf("%u & %u = %un",a,b,c); printf("%u | %u = %un",a,b,d); printf("~%u = %un",a,e); printf("%u & %u = %un",a,b,f); return 0; }
结果:
10 & 2 = 2 10 | 2 = 10 ~10 = 245 10 & 2 = 82.6 移位运算符
移位运算符分为左移(<<)和右移(>>),两者都需要两个整数作为操作数。作用是将左操作数的二进制位,向左或向右移动右操作数个位置。如果右操作数为负数或大于等于左操作数比特位数,则结果是未定义的。
左移运算符<<
语法格式为:x << n
表示x的二进制表示向左移动n位,其中高位的n位二进制位去除,低位新增n位二进制位,新增的二进制位用0补齐。结果类型与x的类型相同。如果x有k个二进制位,那么按照x的类型,结果分别如下:
对于无符号数,结果值等于 ( x ∗ 2 n ) % 2 k (x * 2^n) % 2^k (x∗2n)%2k;
对于有符号数,如果x的值为非负数,并且 x ∗ 2 n x * 2^n x∗2n在结果类型中是可以表示的,那么 x ∗ 2 n x*2^n x∗2n就是结果值;如果x的值为负数,那么结果值是根据C标准实现的,这就涉及符号位是否移动。
关于负数的移位操作,可以参照下述例子进行测试:
# include# include int main() { int a = INT_MIN; // a的二进制补码表示为 1000 0000 0000 0000 0000 0000... int b = a << 1; // b的二进制补码表示为 0000 0000 0000 0000 0000 0000... printf("%xn",a); // a的十六进制表示:0x80000000 printf("%xn",b); // b的十六进制表示:0x00000000 return 0; }
经测试,符号位向左移动一位被舍弃。
右移运算符>>
语法格式为:x >> n
表示x的二进制表示向右移动n位,其中低位的二进制位去除,高位按照情况补0或补1。
对于无符号数,结果值等于 x / 2 n x / 2^n x/2n的整数部分,高位补0。
对于有符号数,如果x的值为非负数,则结果值等于 x / 2 n x / 2^n x/2n的整数部分;如果x的值为负数,那么结果值是根据C标准实现的。在有符号数的移位中,高位补符号位。
# include# include int main() { int a = INT_MIN; // a的二进制表示为 1000 0000 0000 0000 0000 0000... int b = a >> 3; // b的二进制表示为 1111 0000 0000 0000 0000 0000... // b的原码:10010000 00000000 00000000 00000000 // b的反码:11101111 11111111 11111111 11111111 // b的补码:11110000 00000000 00000000 00000000 printf("a的十六进制表示:%xn",a); printf("b的十六进制表示:%xnb的值为:%dn",b,b); return 0; }
结果:
a的十六进制表示:80000000 b的十六进制表示:f0000000 // 与补码相同 b的值为:-268435456
可以看到高位补了符号位。
算术移位与逻辑移位
算术移位与逻辑移位都是针对有符号数而言的,
算术移位是指空出来的位用符号位填充;逻辑移位是指空出来的位用0填充。
3. 表达式与语句什么是表达式,就是有运算符或/和操作数的式子,例如1是一个表达式,1+1是一个表达式,1*2%3/4<=9 && 78-90>0也是一个表达式,所以表达式有简单的,也有复杂的。
什么是语句,最简单的理解是表达式后面加上一个分号就是语句。语句是组成C语言程序的基本结构,为了代码的清晰明了,一行一般只有一句语句。在C语言中,还保留了一些特殊的无用语句,例如:
int main()
{
1;
int a;
a;
;
1+1;
return 0;
}
上述代码并没有报错,但是上述代码什么事也没做,什么功能也没实现。
在有些语言中,类似的语句是不合法的,例如Java:
编译器提示第3、5、7行不是语句。
4. 运算符的优先级什么是运算符的优先级,指的是在一个表达式中,如果有多个运算符,应该先应用那个运算符的问题。就如 $ 1 - 2 * 3$,我们是先算乘法,再算减法,所以乘法的优先级比减法高。
完整的运算符优先级如下请参照资料[2]。
除了优先级,还有一个结合性的概念需要说明,比如,现有表达式 2 ∗ 2 % 3 2 * 2 % 3 2∗2%3,由于乘法和模(求余数)运算的优先级相同,那么我们应该是先算乘法还是先算模运算呢?如果先算乘法,那么该表达式的结果为1,如果先算模运算,那么结果为0。所以此时需要结合性和、来帮助我们确定运算顺序,结合性有左结合性和右结合性,左结合性是指从左到右进行计算,右结合性则相反。**注意:**无论是左结合性,还是右结合性,是针对两个相邻的优先级相同的运算符而言。
具体的结合性,在运算符优先级表中也有体现。
5. 类型转换什么是类型转换呢?例如,现在有一个表达式1 + 2.2,结果是多少呢?结果是什么类型呢?这就涉及到类型转换。下面我们就来详细讲解类型转换涉及的知识。
类型转换从不同的角度可以分为自动的或强制的类型转换,也可以分为提升或截取,按照转换的类型,可以分为无符号数之间的转换、有符号数之间的转换和无符号数与有符号数之间的转换、还涉及浮点数与整数之间的转换。这些概念都是相互交织的,或者说是不同层次的。
自动类型转换与强制类型转换
首先我们来说说自动类型转换,自动类型转换有如下规则:
1、若参与运算的操作数类型不同,则先转换成同一类型,然后进行运算。
2、转换按数据长度增加的方向进行,以保证精度不降低。如int型和long型运算时,先把int型转成long型后再进行运算。
a、若两种类型的字节数不同,转换成字节数高的类型;
b、若两种类型的字节数相同,且一种有符号,一种无符号,则转换成无符号类型;
3、所有的浮点运算都是以双精度进行的,即使仅含float单精度量运算的表达式,也要先转换成double型,再做运算。
4、char型和short型参与运算时,必须先转换成int型。
5、在赋值运算中,赋值号两边量的数据类型不同时,赋值号右边量的类型将转换为左边量的类型。如果右边量的数据类型长度比左边长时,将丢失一部分数据,这样会降低精度,丢失的部分按四舍五入向前舍入。
综上所述,自动类型转换的方向如下:
例如,以下的例子就包含了自动类型转换:
1 + 1.1 // 1和1.1都自动转换为double类型,所以结果为int型
char a = 1;
short b = 2;
a + b; // char和short必须先转换成int型,所以结果为int型
再说说强制类型转换,语法为:
(要转换成的类型)值
例如:
(int)1.1 // 将double型字面量1.1强制转换为int型 int a = 1; char b = (char)a; //将int型变量a强制转换为char型,并赋值给变量b
关于转换规则,请接着往下看。
提升与截取
什么是提升?是指数据长度小的值,转换为数据长度大的值,例如,char类型转换为int类型,字节范围从1字节提升到了4字节。提升不会带来精度的损失,但是提升带来了一个问题,即如何填充多余的比特位呢?有两种方法:零填充与符号填充。
- 零填充就是用0来填充多余的比特位
- 符号填充就是用符号位来填充多余的比特位
例如,现有一个char类型值为-1,其二进制为1111 1111,将其提升为int类型,则零填充和符号填充分别如下:
零填充 : 00000000 00000000 00000000 11111111 值为:256 符号填充: 11111111 11111111 11111111 11111111 值为:-1
下面是C语言程序例子:
# includeint main() { char a = -1; int b = a; printf("b的十六进制:%xnb的值:%d",b,b); return 0; }
结果:
b的十六进制:ffffffff b的值:-1
可以看出采用的是符号填充方式。为什么采用符号填充方式呢,大家可以观察补码形式的101和1111101值相同,可以证明一下,将一个负数的补码前面补n个1,结果不变。这就是说符号填充后的结果是不变的,所以采用符号填充。
再说说截取,截取与提升相反,是指数据长度大的值,转换为数据长度小的值,例如,int类型转换为char类型,字节范围从4字节截取到了1字节。截取规则是将高位的比特位去除。
# includeint main() { int c = -129; char d = (char)c; printf("c的十六进制:%xnc的值:%dn",c,c); printf("d的十六进制:%xnd的值:%dn",d,d); return 0; }
结果:
c的十六进制:ffffff7f c的值:-129 d的十六进制:7f d的值:127
分析:
c的补码形式为:11111111 11111111 11111111 01111111 int型转换为char,4字节截取为1字节,去除最高位的三字节,剩下最后1字节 d的补码形式为:01111111 将d转换为数值为127
有的数转换为char类型后,如果以十六进制输出,会出现ffffff,请参考资料[5]。
有符号数与无符号数之间的转换
有符号数与无符号数都是整数。
-
有符号数与有符号数之间的转换:根据两者的大小,按照提升或截取规则进行转换
-
无符号数与无符号数之间的转换:根据两者的大小,按照提升或截取规则进行转换
-
有符号数与无符号数之间的转换:如果两者的大小不同,按照提升或截取规则进行转换;如果两者的大小相同,那么在位级别上不做更改,只需要更改解释二进制的方式。例如,将char类型的数-1转换为unsigned char类型,那么规则如下:
char类型中,-1的二进制补码为:1111 1111 将其转换为unsigned char,二进制不变,仍为:1111 1111 但是这个二进制视为无符号数,则转换后的值为:255
# include
int main() { char c = -1; unsigned char d = c; printf("c = %dnd = %d",c,d); return 0; } 结果:
c = -1 d = 255
浮点数转换为整数
浮点数转换为整数,是会损失精度的,默认是将小数部分省略,只保留整数部分。
# includeint main() { int a = 1.1; printf("%d",a); return 0; }
结果:
1
一般情况下,请使用强制类型转换语法。
一些特殊的情况
由于类型转换的存在,所以存在一些特殊的情况:
# includeint main() { printf("-1 < 0u的结果为:%dn", -1 < 0u); printf("2147483647U > -2147483647-1的结果为:%dn", (2147483647U > -2147483647-1)) ; printf("2147483647 > (int) 2147483648U的结果为:%dn", (2147483647 > (int)2147483648U)); return 0; }
结果为:
-1 < 0u的结果为:0 2147483647U > -2147483647-1的结果为:0 2147483647 > (int) 2147483648U的结果为:1
0代表false,1代表true。
我们以-1 < 0u为例进行解释,第一眼看上去,结果为true,但是实际情况为false,这就类型转换带来的问题。
由于-1是有符号整型,0u是无符号整型,所以-1要转换为无符号整型,转换后表达式变为4294967295u < 0u,结果当然为false。
参考资料[1] Dev-cpp开启调试模式:https://www.jianshu.com/p/1602264dadf2
[2] 运算符优先级:https://baike.baidu.com/item/%E8%BF%90%E7%AE%97%E7%AC%A6%E4%BC%98%E5%85%88%E7%BA%A7/4752611
[3] Computer System: A Programmer’s Prospective. 3rd Edition.
[4] 自动类型转换:https://baike.baidu.com/item/%E8%87%AA%E5%8A%A8%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2/4400140
[5] C语言中以十六进制输出字符型变量会出现’ffffff"的问题 : https://www.cnblogs.com/shirishiqi/p/5392854.html



