目录
前言
C语言为我们提供了品种繁多的运算符(大约40个),这个特点使得它很难被精通,但是C的许多运算符具有其他语言的运算符无可抗衡的价值。本文是对一些常用的运算符进行总结,以及表达式和语句的知识点的归纳
一、各种运算符
Ⅰ、算术运算符
1.加法运算符:+
用于加法运算,相加的值可以是变量也可以是常量。如printf("%d",4+20); income = salary + bribes;均是正确表达。
2.减法运算符:-
用于减法运算,使其左侧的值减去右侧的值
3.乘法运算符:*
用于乘法运算,不同于数学上的'x'。
4.除法运算符:/
(1)用于除法运算,不同于数学上的‘÷’。/左侧是被除数,/右侧是除数。
(2)整数除法和浮点数除法不同。浮点数除法的结果是浮点数,整数除法的结果是整数。
#include <stdio.h>
int main()
{
printf("%d\n",5 / 4);
printf("%d\n",6 / 3);
printf("%.2f\n",7. / 4.);
printf("%.2f\n",7. / 4);
return 0;
}
编译并运行代码,输出如下:
1 //整型除法会截断计算结果的小数部分(丢弃整个小数部分),不会四舍五入
2
1.75
1.75 //整数和浮点数的计算结果是浮点数,实际上,计算机不会真正用浮点数除以整数,编译器会把两个运算对象转换成相同类型
(3)负数如何截断?如-3.8是变成-3还是-4,“趋零截断”这一概念告诉我们-3.8会被处理成-3
5.求模运算符:%
(1)用于整数运算,不能用于浮点数,求模运算符给出其左侧整数除以右侧整数的余数。13%5(读作:13求模5)。
(2)求模运算符常用于控制程序流
(3)负数求模:如果第一个运算对象为负数,那么求模结果为负数;如果第一个运算对象为正数,那么求模结果为正数
6.符号运算符:+和-
用于标明或改变一个值的代数符号。它们是一元运算符(单目运算符),即只需要一个运算对象。
7.递增运算符:++
(1)功能:将其运算对象递增1
(2)两种形式:①前缀模式:++出现在其作用的变量前面
②后缀模式:++出现在其作用的变量后面
(3)这种缩写形式的好处:让程序更加简洁、美观,可读性更高。
(4)前缀模式:先++,再使用
后缀模式:先使用,再++
#include <stdio.h>
int main()
{
int a = 1,b = 1;
int a_post,pre_b;
a_post = a++;
pre_b = ++b;
printf("a a_post b pre_b \n");
printf("%d %7d %2d %6d\n", a, a_post, b, pre_b);
return 0;
}
编译并运行该代码,输出如下:
a a_post b pre_b
2 1 2 2
可以看到后缀模式:使用a的值之后,递增a
前缀模式:使用b的值之前,递增b
8.递减运算符:--
与++同理。
Ⅱ、赋值运算符
1.赋值运算符:=
1°、一些术语:
(1)数据对象:用于存储值的数据存储区
(2)左值:用于标识特定数据对象的名称或表达式。
Tips:提到左值,这意味着它①指定一个对象,可以引用内存中的地址②可以用在赋值运算符的左侧。但是const创建的变量不可修改,不满足②,所以后来又提出了可修改的左值这一概念,用于标识可修改的对象
(3)右值:能赋值给可修改左值的量,且本身不为左值
2°、在C语言中,=不意味着“相等”,而是一个赋值运算符,num = 2021,读作“把值2021赋给变量num”,赋值行为从右往左进行
3°、2021 = num;?在C语言中类似这样的语句是没有意义的,因为此时,2021被称为右值,只能是字符常量,不能给常量赋值,常量本身就是它的值。因此,我们要记住赋值运算符左侧必须引用一个存储位置,最简单的方法就是使用变量名。C使用可修改的左值标记那些可赋值的实体。
4°、C语言中不回避三重赋值,赋值顺序从右向左,如a = b = c = 100;首先把100赋给从c,然后再赋给b,最后赋给a。
关于左值,右值,我们来看以下代码例子:
#include <stdio.h>
int main()
{
int ex, why, zee;//ex,why,zee都是可修改的左值
const int TWO = 2;//TWO是不可修改的左值,只能放在=的右侧,此处的=是初始化,不是赋值,因此并没有违反规则
why = 42;
zee = why;
ex = TWO * (why + zee);//(why + zee)是右值,该表达式不能表示特定内存位置,而且也不能给它赋值
return 0;
}
2.其他赋值运算符:+=、-=、*=、/=、%=
(1)以下每一行的左右两种表达等价
scores += 20 | score = score + 20 |
dimes -= 2 | dimes = dimes - 2 |
bunnies *= 2 | bunnies = bunnies * 2 |
time /= 2.73 | time = time /2.73 |
reduce %= 3 | reduce = reduce % 3 |
(2)优点:让代码更紧凑,与一般形式相比,组合形式的赋值运算符生成的机器代码更高效
3.以上两大类运算符优先级和求值顺序问题
(1)
运算符 | 结合律 |
() | 从左往右 |
+ - ++ --(单目) | 从右往左 |
* / | 从左往右 |
+ -(双目) | 从左往右 |
= += -= *= /= %= | 从右往左 |
Tip:如果两个运算符优先级相同,则根据它们在语句中出现的顺序来执行
(2)虽然运算符的优先级为表达式中的求值顺序提供了重要依据,但是并没有规定所有的顺序。如:用 y= 6 * 12 + 5 * 20;由(1)可知,先进行6 * 12和5 * 20,再进行加法运算,但是优先级并没有规定先进行哪个乘法。C语言把主动权交给语言的实现者,根据不同的硬件来决定先计算前者还是后者,但是无论采取何种方案,都不会影响最终结果。结合律适用于共享同一运算对象的运算符,如:12 / 3 * 2,/和*的优先级相同,共享运算符3,所以从左往右的结合律可以起作用。
(3)如果一个变量出现在一个函数的多个参数中,不要对该变量使用递增或递减运算符;如果一个变量多次出现在一个表达式中,不要对该变量使用递增或递减运算符。举个例子:ans = num/2 + 5*(1 + num++);我们可能会认为,先计算第一项(num/2),接着计算(5*(1 + num++));但是编译器可能先计算第二项,递增num,然后在num/2中使用num递增后的新值
Ⅲ、关系运算符
1.
< | 小于 |
> | 大于 |
<= | 小于等于 |
>= | 大于等于 |
== | 等于 |
!= | 不等于 |
结合律:从左向右
2.用关系运算符将两个表达式连接起来的式子,称为关系表达式。关系表达式的结果是一个逻辑量,取值“真”或“假”,即“1”或“0”。
3.优先级:赋值<关系<算术。所以x > y + 2和x > (y + 2)相同;x = y >2和x = (y >2)相同,若y>2,给x赋值为1,否则赋值为0
高优先级组 | < > <= >= |
低优先级组 | == != |
4.如果待比较的值是一个常量,可以把该常量放在左侧有助于编译器捕获错误,如:5 = canoes是一种语法错误,而5 == canoes可以检查 canoes的值是否为5。这是因为C语言不允许给常量赋值,编译器会把赋值运算符的这种用法作为语法错误标记出来。我们再构建比较是否相等的表达式时可以把常量放在左侧。
Ⅳ、逻辑运算符
1.与关系运算一样,用整数1代表“真”,用整数0代表“假”
2.
目数 | 单目 | 双目 | |
运算符 | ! | && | || |
名称 | 逻辑非 | 逻辑与 | 逻辑或 |
假设exp1和exp2是两个简单的关系表达式,那么:
(1)当且仅当exp1和exp2都为真时,exp1 && exp2才为真
(2)如果exp1或exp2为真,则exp1 || exp2为真
(3)如果exp1为真,则!exp1为假;如果exp1为假,则!exp1为真
3.&& 和 ||都是序列点,所以程序在从一个运算对象执行到下一个运算对象之前,所有的副作用都会生效。
4.&&可用于测试范围,如测试score是否在90~100的范围,可以这样写 if(score >= 90 && score <= 100)
printf ("Good!\n");
但是不可以写成if(90 <= score <= 100)
printf("Good!\n");
这是代码的语义错误,不是语法错误,因此编译器并不会捕捉这样的问题。<=的求值顺序为从左到右,子表达式90 <= score的值要么为1,要么为0,这两个值都小于100,所以不管score的值是多少,整个表达式都恒为真。
5.与其他表达式的运算过程不同,在求解&&和||连接的逻辑表达式时,按从左到右的顺序计算该运算符两侧的操作数,一旦能得到表达式的结果,就停止运算。(1)exp1 && exp2,先计算exp1,若其值为0,则exp1 && exp2的值一定为0(2)exp1 || exp2,先计算exp1,若其值为非0,则exp1 && exp2的值一定为1
如:
#include <stdio.h>
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
printf("a = %d,b = %d,c = %d,d = %d\n", a, b, c, d);
return 0;
}
编译并运行该代码,输出如下:
a = 1,b = 2,c = 3,d = 4
//后置++,先使用再++,使用时,a = 0,为假,后面的已经没有必要算了
//i = 0
4.优先级:
! |
&& || |
Ⅴ、条件运算符(?:)
1.作为if else语句的一种便携方式。是C语言中唯一的三目运算符(带三个运算对象)
2.通用形式:expression1?expression2:expression3 (结合律:从左向右)
如果expression1为真,那么整个条件表达式的值与expression2相同;如果expression1为假,那么整个条件表达式的值与expression3相同
举个例子:(5 > 3)? 1 : 2 值为1
Ⅵ、逗号运算符(,)
1.一些概念:
(1)副作用:对数据对象或文件的修改
(2)序列点:程序执行的点,在该点上,所有的副作用都在进入下一步之前发生。在C语言中,语句中的分号标记了一个序列点
2.在C语言中,逗号既可以作分隔符,又可以作运算符。逗号作为分隔符使用时,用于间断说明语句中的变量或函数中的参数;作为运算符使用时,将若干个独立的表达式连接在一起,组成逗号表达式。其一般形式为:表达式1,表达式2,...,表达式n。其运算过程为:先计算表达式1,然后计算表达式2,......,最后计算表达式n,并将表达式n的值作为逗号表达式的值。
3.一些情况:
(1)假设有该表达式:a++, b = a * 10,在该表达式中,先递增a,然后在第二个子表达式中使用a的新值。作为序列点的逗号保证了左侧表达式的副作用对右侧表达式求值之前发生
(2)如果有人在写数字时不小心输入了逗号,如:price = 12,30; 这不是语法错误,编译器会把它解释为一个逗号表达式
Ⅶ、位运算符
1.位运算符概览
& | 按位“与” |
| | 按位“或” |
^ | 按位“异或” |
~ | 取反 |
<< | 左移 |
>> | 右移 |
(1)位运算符中除^是单目运算符外,其它均为双目运算符
(2)位运算符所操作的操作数只能是整型或字符型的数据以及它们的变体
2.按位“与”:
(1)二元运算符&通过逐位比较两个运算对象,生成一个新值。对于每个位,只有两个运算对象中相应的位都为1时,结果才为1。如:
(10010011)& (00111101) 的结果为 00010001
(2)C语言中有一个按位和赋值结合的运算符:&=
3.按位“或”:
(1)二元运算符 | 通过逐位比较两个运算对象,生成一个新值。对于每个位,如果两个运算对象中相应的位为1,结果为1。如:
(10010011)| (00111101) 的结果为 10111111
(2)C语言中有一个按位和赋值结合的运算符:|=
4.按位“异或”:
(1)二元运算符^通过逐位比较两个运算对象,生成一个新值。对于每个位,如果两个运算对象中相应的位一个为1,结果为1。如:
(10010011)^ (00111101) 的结果为 10101110
(2)C语言中有一个按位和赋值结合的运算符:^=
(3)异或是支持交换律的:看一条题:在不创建临时变量(第三个变量),实现两个数的交换。我们用两种方法来实现这条题。
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
a = a + b;
b = a - b;
a = a - b;
printf("%d %d\n",a, b);
return 0;
}
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("%d %d\n",a, b);
return 0;
}
两种方法比较:第一种方法在数字太大时会溢出;第二种方法的可读性不好,只适用于整型,效率也没有第一种方法好。
5.取反:
一元运算符~把1变为0,把0变为1。如:~(10011010)的结果为01100101
6.左移:
(1)<<将其左侧运算对象每一位的值向左移动其右侧运算对象指定的位数。左侧运算对象移出左末端位的值丢失,用0填充空出的位置。如图:
(2)该操作产生一个新的位值,但是不改变运算对象 。如:假设a = 1,a<<2为4,但是a本身的值不被改变。可以使用<<=左移赋值运算符来更改变量的值,如:假设a = 1,a<<=2,此时a的值改为4。
7.右移:
(1)>>将其左侧运算对象每一位的值向右移动其右侧运算对象指定的位数。左侧运算对象移出右末端位的值丢失。对于无符号型,用0填充空出的位置;对于有符号类型,其结果取决于机器,空出的位置可以用0填充,也可以用符号位的副本填充
如:有符号的例子:(10001010)>> 2的结果可能是(00100010),也有可能是(11100010),看系统。
无符号的例子:(10001010)>> 2的结果均为(00100010)
(2)该操作产生一个新的位值,但是不改变运算对象 。可以使用<<=左移赋值运算符来更改变量的值
(3)移位运算符对2的幂提供了快速有效的乘法和除法:
number << n | number乘以2的n次幂 |
number >> n | 若number>0,则用number除以2的n次幂 |
注意:移位运算具体实现有3种方式
①循环移位:移入的位等于移出的位
②逻辑移位:移出的位丢失,移入的位取0
③算术移位(带符号):移出的位丢失,左移入的位取0,右移入的位取符号位,即最高位代表数据符号,保持不变
C语言中的移位运算与具体的C语言编译器有关。
8.一道例题:求一个整数存储在内存中的二进制中1的个数。(我们用3种方法实现它)
#include <stdio.h>
int main()
{
int num;
int count = 0;
scanf("%d", &num);
while(num)
{
if(num % 2 == 1)
{
count++;
}
num /= 2;
}
printf("%d\n",count);
return 0;
}
#include <stdio.h>
int main()
{
int num, i;
int count = 0;
scanf("%d", &num);
for(i = 0; i < 32; i++)
{
if(((num >> i) & 1) == 1)//挪动i位,使得每一位都和1按位“与”
{
count++;
}
}
printf("%d\n", count);
return 0;
}
#include <stdio.h>
int main()
{
int num;
int count = 0;
scanf("%d", &num);
while(num)
{
count++;
num = num & (num - 1);
}
printf("%d\n", count);
return 0;
}
以上3种方法,过程逐渐优化,需要我们好好体会。
Ⅷ、其他运算符
1.长度运算符sizeof():单目运算符,以字节为单位返回运算对象的大小。C语言规定,sizeof()返回size_t类型的值,这是一个无符号整数类型。
2.函数调用运算符()
3.下标引用运算符[]:用来表示数组元素
4.强制类型转换运算符(类型):强制类型转换
4.查找地址 &运算符:一元&运算符给出变量的存储地址
5.间接(解引用)运算符*:找出存储在某变量中的值
6.访问结构成员(.)(->)
二、优先级大总结
1.
运算符 | 结合方向 |
[] () . -> | 左到右 |
-(负号运算符) (类型)++ -- *(取值运算符) &(取地址运算符) ! ~ sizeof() | 右到左 |
/ * % | 左到右 |
+ - | 左到右 |
<< >> | 左到右 |
> < >= <= | 左到右 |
== != | 左到右 |
&(按位“与”) | 左到右 |
^ | 左到右 |
| | 左到右 |
&& | 左到右 |
|| | 左到右 |
?: | 左到右 |
= /= *= %= += -= <<= >>= &= ^= |= | 右到左 |
, | 左到右 |
注意:同一优先级的运算符,运算次序由结合方向决定
2.一些容易出错的优先级问题:
(1).的优先级高于*(->操作符用于消除这个问题):*p.f等价于*(p.f)
(2)[]高于*:int* ap[]等价于int* (ap[])
(3)函数()高于*:int* fp()等价于int* (pf())
(4)==和!=高于位操作:(val & mask != 0)等价于val & (mask != 0)
(5)==和!=高于赋值符:c = getchar() != EOF等价于c = (getchar() != EOF)
(6)算术运算符高于位移运算符:msb << 4 + lsb等价于msb << (4 + lsb)
(7)逗号运算符的优先级最低:i=1,2等价于(i = 1),2
三、类型转换
1.隐式类型转换
(1)C语言的整型算术运算符总是至少以缺省(默认值)整型类型的精度来进行的。为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升
(2)整型提升的意义:表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
如char a, b, c; c = a + b; a和b的值被提升为普通整型,然后再执行加法运算。加法运算完成之后,结果将被截断,然后再存储于c中。
(3)如何整型提升:整形提升是按照变量的数据类型的符号位来提升的
①有符号整型提升:高位补充符号位
负数:char c1 = -1;//11111111变量c1的二进制位(补码)中只有8bit;整型提升:高位补充符号位,即为1,所以结果为11111111111111111111111111111111
正数:char c2 = 1;//00000001变量c2的二进制位(补码)中只有8bit;整型提升:高位补充符号位,即为0,所以结果为 00000000000000000000000000000001
②无符号整型提升:高位补充0
(4)一条例题:
#include <stdio.h>
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
return 0;
}
编译运行代码,输出如下:
1
4
4
//c只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以 sizeof(+c) 是4个字节;同理,sizeof(-c) 也是4个字节
2.算术转换:
3.强制类型转换 :
即在某个量的前面用圆括号括起来类型名,该类型名即是希望转换成的目标类型。
对比下面两行代码:假设sum是int 型变量:
sum = 1.6 + 1.7;//结果为3。首先,1.6+1.7=3.3,为了匹配int型变量,3.3被类型转换截断成3(自动类型转换)
sum = (int)1.6 + (int)1.7;//结果为2。1.6和1.7在相加之前都被强制转换成1,然后把1+1的结果赋给sum。(强制类型转换)
四、表达式:
表达式是由运算符和运算对象组成的。最简单的表达式是一个单独的运算对象。C表达式的一个最重要特性就是,每个表达式都有一个值。
五、语句:
1.语句时C程序的基本构建块。一条语句相当于一条完整的计算机指令。在C中,大部分语句以分号结尾。最简单的语句是空语句,即只有一个分号。C把末尾上加上一个分号的表达式都看作是一条语句,所以8;4+3;类似这样写也是可以的。虽然一条有用的语句相当于一条完整的指令,但并不是所有的指令都是语句,如:x = 6 + (y = 5);此处的y = 5便是一条完整的指令,但是它只是语句的一部分。
2.复合语句:用花括号括起来的一条或多条语句,复合语句也称为块。
请看下面两个程序段:
while (index++ < 10)
{
sam = 10 * index + 2;
printf("%d\n",sam);
}
while (index++ < 10){
sam = 10 * index + 2;
printf("%d\n",sam);
}
两种风格的区别:第一种:强调语句形成一个块;第二种:突出块附属于while循环。但是对于编译器来说,这两种风格完全相同。