本文讲解C语言中的运算符、表达式、语句及其相关的知识。
什么是运算符?其实我们并不陌生,从小到大已经接触得非常多了,对其已经非常熟悉了,所以不要害怕。就举一个最简单的例子,加法就是一个数学运算符,当然,在C语言中加法也是一种运算符,除此之外,还存在许多其他的运算符,接下来我们都要一 一学习。
什么是表达式?表达式包括运算符和操作数。
什么是语句?语句是编程语言中特有的,最简单的理解,语句就是表达式后面加上一个英文分号。
赋值运算符就是最常见、最简单的运算符,为一个等号 =
:
运算符 | 名称 | 说明 | 例子 |
---|---|---|---|
= | 赋值运算符 | 将一个值赋给一个变量 变量在运算符左边 |
int a = 1; |
算术运算符与数学中的四则运算、求余数运算相同,主要有以下运算符:
运算符 | 名称 | 说明 | 例子 括号内为结果 |
---|---|---|---|
+ | 加法运算符 | 双目运算符 将两个操作数相加 |
1 + 2(3) |
− - − | 减法运算符 | 双目运算符 左边的操作数减去右边的操作数 |
1 - 2(-1) |
* | 乘法运算符 | 双目操作符 将两个操作数相乘 |
2 * 3(6) |
/ | 除法运算符 | 双目运算符 左边的操作数除以右边的操作数 |
2 / 3(0) |
% | 求余运算符 | 双目操作符 左边的操作数除以右边的操作数后的余数,读作模 |
2 % 3(2) |
关于上述的算术运算符,有几个需要我们注意的点:
对于操作数,可以为字面量、变量或者表达式,或者函数;
除法运算符:括号内的结果是0,这与我们的认知不同;
求余运算符:C语言是如何求余数的,如果涉及到负数,余数该如何求呢?
除法运算符
在C语言中,如果除法的两个操作数是整数,那么结果也为整数,并且结果是直接省略掉小数部分。
# include
int 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 = %d\n",a);
printf("b = %d\n",b);
printf("c = %d\n",c);
printf("d = %d\n",d);
printf("e = %d\n",e);
printf("f = %d\n",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中至少存在一个负数,那么结果的符号依赖于实现。
# include
int main()
{
int a = 10 % 3;
int b = -10 % 3;
int c = 10 % -3;
int d = -10 % -3;
printf("a = %d\n",a);
printf("b = %d\n",b);
printf("c = %d\n",c);
printf("d = %d\n",d);
return 0;
}
结果:
a = 1
b = -1
c = 1
d = -1
除了上述的算术运算符外,还有以下两个运算符需要注意:
运算符 | 名称 | 说明 | 例子 |
---|---|---|---|
++ | 自增运算符 | 单目运算符 将变量的值+1 |
i++ ++i |
-- | 自减运算符 | 单目运算符 将变量的值-1 |
i-- --i |
注意:操作数必须为变量。
关于自增(自减)运算符,如果运算符和操作数的位置不同,则结果不同:
我们先看以下两段程序,来明确前置和后置的不同:
// 第一段程序
# include
int main()
{
int i = 1;
int j = ++i;
printf("i = %d\nj = %d",i,j);
return 0;
}
结果:i = 2 , j = 2
# include
int main()
{
int i = 1;
int j = i++;
printf("i = %d\nj = %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 0x402b08
0x0000000000401565 <+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 0x402b18
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
是什么值了。
要注意短路原则在代码中的影响:
# include int main()
{
int a = 1;
bool res = false && (a = 3) > 1;
printf("a = %d",a); return 0;
}
结果:
a = 1
由于短路原则,赋值语句a = 3
并未执行。
**位运算符是将操作数的对应比特位的值(也就是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
程序验证如下:
#include
int 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 = %u\n",a,b,c);
printf("%u | %u = %u\n",a,b,d);
printf("~%u = %u\n",a,e);
printf("%u & %u = %u\n",a,b,f);
return 0;
}
结果:
10 & 2 = 2
10 | 2 = 10
~10 = 245
10 & 2 = 8
移位运算符分为左移(<<
)和右移(>>
),两者都需要两个整数作为操作数。作用是将左操作数的二进制位,向左或向右移动右操作数个位置。如果右操作数为负数或大于等于左操作数比特位数,则结果是未定义的。
左移运算符<<
语法格式为: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("%x\n",a); // a的十六进制表示:0x80000000
printf("%x\n",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的十六进制表示:%x\n",a);
printf("b的十六进制表示:%x\nb的值为:%d\n",b,b);
return 0;
}
结果:
a的十六进制表示:80000000
b的十六进制表示:f0000000 // 与补码相同
b的值为:-268435456
可以看到高位补了符号位。
算术移位与逻辑移位
算术移位与逻辑移位都是针对有符号数而言的,
算术移位是指空出来的位用符号位填充;逻辑移位是指空出来的位用0填充。
什么是表达式,就是有运算符或/和操作数的式子,例如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行不是语句。
什么是运算符的优先级,指的是在一个表达式中,如果有多个运算符,应该先应用那个运算符的问题。就如 $ 1 - 2 * 3$,我们是先算乘法,再算减法,所以乘法的优先级比减法高。
完整的运算符优先级如下请参照资料[2]。
除了优先级,还有一个结合性的概念需要说明,比如,现有表达式 2 ∗ 2 % 3 2 * 2 \% 3 2∗2%3,由于乘法和模(求余数)运算的优先级相同,那么我们应该是先算乘法还是先算模运算呢?如果先算乘法,那么该表达式的结果为1,如果先算模运算,那么结果为0。所以此时需要结合性和、来帮助我们确定运算顺序,结合性有左结合性和右结合性,左结合性是指从左到右进行计算,右结合性则相反。**注意:**无论是左结合性,还是右结合性,是针对两个相邻的优先级相同的运算符而言。
具体的结合性,在运算符优先级表中也有体现。
什么是类型转换呢?例如,现在有一个表达式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字节。提升不会带来精度的损失,但是提升带来了一个问题,即如何填充多余的比特位呢?有两种方法:零填充与符号填充。
例如,现有一个char类型值为-1,其二进制为1111 1111
,将其提升为int类型,则零填充和符号填充分别如下:
零填充 : 00000000 00000000 00000000 11111111 值为:256
符号填充: 11111111 11111111 11111111 11111111 值为:-1
下面是C语言程序例子:
# include
int main()
{
char a = -1;
int b = a;
printf("b的十六进制:%x\nb的值:%d",b,b);
return 0;
}
结果:
b的十六进制:ffffffff
b的值:-1
可以看出采用的是符号填充方式。为什么采用符号填充方式呢,大家可以观察补码形式的101
和1111101
值相同,可以证明一下,将一个负数的补码前面补n个1,结果不变。这就是说符号填充后的结果是不变的,所以采用符号填充。
再说说截取,截取与提升相反,是指数据长度大的值,转换为数据长度小的值,例如,int类型转换为char类型,字节范围从4字节截取到了1字节。截取规则是将高位的比特位去除。
# include
int main()
{
int c = -129;
char d = (char)c;
printf("c的十六进制:%x\nc的值:%d\n",c,c);
printf("d的十六进制:%x\nd的值:%d\n",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 = %d\nd = %d",c,d);
return 0;
}
结果:
c = -1
d = 255
浮点数转换为整数
浮点数转换为整数,是会损失精度的,默认是将小数部分省略,只保留整数部分。
# include
int main()
{
int a = 1.1;
printf("%d",a);
return 0;
}
结果:
1
一般情况下,请使用强制类型转换语法。
一些特殊的情况
由于类型转换的存在,所以存在一些特殊的情况:
# include
int main()
{
printf("-1 < 0u的结果为:%d\n", -1 < 0u);
printf("2147483647U > -2147483647-1的结果为:%d\n", (2147483647U > -2147483647-1)) ;
printf("2147483647 > (int) 2147483648U的结果为:%d\n", (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