C语言— —运算符与表达式

本文讲解C语言中的运算符、表达式、语句及其相关的知识。

文章目录

    • 1. 概述
    • 2. 运算符
      • 2.1 赋值运算符
      • 2.2 算术运算符
      • 2.3 比较运算符
      • 2.4 逻辑运算符
      • 2.5 位运算符
      • 2.6 移位运算符
    • 3. 表达式与语句
    • 4. 运算符的优先级
    • 5. 类型转换
    • 参考资料

1. 概述

什么是运算符?其实我们并不陌生,从小到大已经接触得非常多了,对其已经非常熟悉了,所以不要害怕。就举一个最简单的例子,加法就是一个数学运算符,当然,在C语言中加法也是一种运算符,除此之外,还存在许多其他的运算符,接下来我们都要一 一学习。

什么是表达式?表达式包括运算符和操作数。

什么是语句?语句是编程语言中特有的,最简单的理解,语句就是表达式后面加上一个英文分号。

2. 运算符

2.1 赋值运算符

赋值运算符就是最常见、最简单的运算符,为一个等号 =

运算符 名称 说明 例子
= 赋值运算符 将一个值赋给一个变量
变量在运算符左边
int a = 1;

2.2 算术运算符

算术运算符与数学中的四则运算、求余数运算相同,主要有以下运算符:

运算符 名称 说明 例子
括号内为结果
+ 加法运算符 双目运算符
将两个操作数相加
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

注意:操作数必须为变量。

关于自增(自减)运算符,如果运算符和操作数的位置不同,则结果不同:

  • 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]。

C语言— —运算符与表达式_第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  

2.3 比较运算符

比较运算符用于确定两个值是否相等或大小关系,结果为一个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)

2.4 逻辑运算符

逻辑运算符有与(&&)、或(||)和非(!)。

逻辑与运算符是双目运算符,需要两个布尔操作数,其返回值是一个布尔值,情形列表如下:

情形 结果
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并未执行。

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 & 210 | 2~1010 ^ 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

2.6 移位运算符

移位运算符分为左移(<<)和右移(>>),两者都需要两个整数作为操作数。作用是将左操作数的二进制位,向左或向右移动右操作数个位置。如果右操作数为负数或大于等于左操作数比特位数,则结果是未定义的。

左移运算符<<

语法格式为:x << n

表示x的二进制表示向左移动n位,其中高位的n位二进制位去除,低位新增n位二进制位,新增的二进制位用0补齐。结果类型与x的类型相同。如果x有k个二进制位,那么按照x的类型,结果分别如下:

对于无符号数,结果值等于 ( x ∗ 2 n ) % 2 k (x * 2^n) \% 2^k (x2n)%2k

对于有符号数,如果x的值为非负数,并且 x ∗ 2 n x * 2^n x2n在结果类型中是可以表示的,那么 x ∗ 2 n x*2^n x2n就是结果值;如果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填充。

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:

C语言— —运算符与表达式_第2张图片

编译器提示第3、5、7行不是语句。

4. 运算符的优先级

什么是运算符的优先级,指的是在一个表达式中,如果有多个运算符,应该先应用那个运算符的问题。就如 $ 1 - 2 * 3$,我们是先算乘法,再算减法,所以乘法的优先级比减法高。

完整的运算符优先级如下请参照资料[2]。

除了优先级,还有一个结合性的概念需要说明,比如,现有表达式 2 ∗ 2 % 3 2 * 2 \% 3 22%3,由于乘法和模(求余数)运算的优先级相同,那么我们应该是先算乘法还是先算模运算呢?如果先算乘法,那么该表达式的结果为1,如果先算模运算,那么结果为0。所以此时需要结合性和、来帮助我们确定运算顺序,结合性有左结合性和右结合性,左结合性是指从左到右进行计算,右结合性则相反。**注意:**无论是左结合性,还是右结合性,是针对两个相邻的优先级相同的运算符而言。

具体的结合性,在运算符优先级表中也有体现。

5. 类型转换

什么是类型转换呢?例如,现在有一个表达式1 + 2.2,结果是多少呢?结果是什么类型呢?这就涉及到类型转换。下面我们就来详细讲解类型转换涉及的知识。

类型转换从不同的角度可以分为自动的或强制的类型转换,也可以分为提升或截取,按照转换的类型,可以分为无符号数之间的转换、有符号数之间的转换和无符号数与有符号数之间的转换、还涉及浮点数与整数之间的转换。这些概念都是相互交织的,或者说是不同层次的。

自动类型转换与强制类型转换

首先我们来说说自动类型转换,自动类型转换有如下规则:

1、若参与运算的操作数类型不同,则先转换成同一类型,然后进行运算。

2、转换按数据长度增加的方向进行,以保证精度不降低。如int型和long型运算时,先把int型转成long型后再进行运算。

​ a、若两种类型的字节数不同,转换成字节数高的类型;

​ b、若两种类型的字节数相同,且一种有符号,一种无符号,则转换成无符号类型;

3、所有的浮点运算都是以双精度进行的,即使仅含float单精度量运算的表达式,也要先转换成double型,再做运算。

4、char型和short型参与运算时,必须先转换成int型。

5、在赋值运算中,赋值号两边量的数据类型不同时,赋值号右边量的类型将转换为左边量的类型。如果右边量的数据类型长度比左边长时,将丢失一部分数据,这样会降低精度,丢失的部分按四舍五入向前舍入。

综上所述,自动类型转换的方向如下:

C语言— —运算符与表达式_第3张图片

例如,以下的例子就包含了自动类型转换:

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语言程序例子:

# include 
int main()
{
	char a = -1; 
	int b = a;  
	printf("b的十六进制:%x\nb的值:%d",b,b);
	return 0;
}

结果:

b的十六进制:ffffffff
b的值:-1

可以看出采用的是符号填充方式。为什么采用符号填充方式呢,大家可以观察补码形式的1011111101值相同,可以证明一下,将一个负数的补码前面补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

你可能感兴趣的:(C,c语言,运算符与表达式)