接下来我们将继续探讨C语言的基础知识。上一章讲解了数据类型的概念,本章将重点介绍运算符的相关内容。让我们直接进入主题,开启C语言运算符的学习之旅。
- 各类数值型数据间的混合运算
- 隐式转换
- 显示转换
- C运算符和C表达式
- 算术运算符
- 赋值运算符
- 关系运算符
- 逻辑运算符
- 逗号运算符
- 位运算
各类数值型数据间的混合运算
首先大家先可以知道整型、浮点型、字符型数据可以进行混合运算,如:
10-‘a'*1.5=10-97*1.5//保证参与运算的都是数字=10.0-97.0*1.5//临时申请一块空间,保证运算数据类型一致、运算完毕,临时空间回收(隐式转换)解释:整型、浮点型、字符型能进行混合运算,核心原因是它们都属于数值型数据——字符本质是特殊的数值型(参与数值计算时使用ASCll码,取值范围是0~127)
运算规则:若参与运算的两个数类型不同,会先将类型转换为一致后再运算。转换规则分为隐式转换和显示转换。
隐式转换
隐式转换又被称为自动类型转换,由编译系统自动控制规则是将低等级提示为高等级类型。
转换关系(由低到高)如图所示:
注:
①int → unsigned int → long → double(int与unsigned的转换顺序可能因平台(如操作系统位数)和编译器不同存在差异,需结合具体环境判断。)
②char /short →int(必然转换)
③float → double (必然转换,C语言默认以双精度浮点型(double)进行浮点运算,因此float类型参与运算时会自动隐式转换为double)
关键注意事项
混合运算中的类型转换仅在运算过程中临时生效,不会改变原变量的类型和内存的存储形式(所谓类型提升,其实就是系统运算时,将原本的数据产生一份副本,由副本数据参与计算,计算完毕,副本销毁,原数据不受影响)
举例:
inta=10;//a的类型始终是intdoublec=a+22.5;//运算时a临时转换为double,等价于10.0 + 22.5,其实就是a的double类型的副本参与运算a=21;//a仍然是int,变量一旦申请其内存大小不会发生改变显示转换
显示转换又被称作强制类型转换,由程序员手动指定转换类型。
语法说明:
(double)a://将变量 a 的值强制转换为double类型。(int)(x+y)://将表达式 x+y 的结果转换为int类型。(括号比较包含整个表达式,否则转换x)(int)x+y://仅将变量x转换为int类型,在于y进行计算(括号仅作用于x)举例:
doublea=2,b=3;doublec=(int)a+b;//第1步:显示将a转换为int(2),第2步:混合运算int转化为double,最终等价于2.0 + 3.0doublenum1=12.55;intnum2=(int)num1;//显示将num1转换为int类型,浮点型转整型,舍弃小数部分,不是四舍五入)printf("%d\n",num2);num1=15;关键注意事项
1、强制类型转换仅在运算过程中临时改变值的类型,不会修改原变量的类型和内存存储形式;
2、浮点型转整型时直接舍弃小数部分,不进行四舍五入;
3、转换后的类型需与接收变量的类型匹配,避免数据溢出或精度丢失。
C运算符和C表达式
C运算符
C表达式
由运算数(常量、变量、表达式)和运算符按照C语法规则连接而成的式子(表达式 = 运算数 +运算符)
| 名称 | 举例 | 结果说明与说明 |
|---|---|---|
| 算术表达式 | 2 + 6.7 *3.5 | 数值类型(int或double等取决于运算数类型) |
| 关系表达式 | x > 0 、y < z+6 | 布尔类型(C语言用0表示假,非0表示真) |
| 逻辑表达式 | x > 0 && y > 0 | 布尔类型 |
| 赋值表达式 | a = 5.6、sum += i、a = b = c = 5; | 左侧变量的类型,运算顺序自右向左 |
| 逗号表达式 | x = 3, y += 4 , z -= 8 | 最后一个表达式的类型,结果为最后一个表达式的值 |
注:注意逗号表达式和逗号分隔符。
逗号运算符:在表达式中,用来连接多个表达式,有 “顺序执行 + 返回最后一个值” 的作用(比如 (a=1, b=2))。
逗号分隔符:用来分隔函数参数、变量声明等,没有 “逗号表达式” 的特性(比如 printf(“%d”, a) 里的逗号,或者 int a, b, c; 里的逗号)
逗号表达式: x = 3, y += 4 , z -= 8 最后一个表达式的类型,结果为最后一个表达式的值
C语言运算符优先级与结合性
C语言通过优先级和结合性规定表达式的求值顺序:优先级高的运算符先执行;优先级相同时,按结合性顺序执行。
简化优先级表(从高到低):
| 优先级 | 运算符类别 | 运算符示例 | 结合性 |
|---|---|---|---|
| 1 | 括号、下标、成员访问 | ()、[]、.、-> | 从左向右 |
| 2 | 后缀自增 / 自减 | a++、a-- | 从左向右 |
| 3 | 前缀自增 / 自减、逻辑非等 | ++a、--a、!、sizeof、&、-(负号) | 从右向左 |
| 4 | 强制类型转换 | (type) | 从右向左 |
| 5 | 算术乘除、取余 | *、/、% | 从左向右 |
| 6 | 算术加减 | +、- | 从左向右 |
| 7 | 位左移 / 右移 | <<、>> | 从左向右 |
| 8 | 关系运算符(比较大小) | >、<、>=、<= | 从左向右 |
| 9 | 关系运算符(相等判断) | ==、!= | 从左向右 |
| 10 | 位与 | & | 从左向右 |
| 11 | 位异或 | ^ | 从左向右 |
| 12 | 位或 | | | 从左向右 |
| 13 | 逻辑与 | && | 从左向右 |
| 14 | 逻辑或 | || | 从左向右 |
| 15 | 条件运算符 | ?: | 从右向左 |
| 16 | 赋值运算符(含复合赋值) | =、+=、-=、*=、/=等 | 从右向左 |
| 17 | 逗号运算符 | , | 从左向右 |
算术运算符
| 运算符 | 功能 | 类型 | 注意事项 |
|---|---|---|---|
| + | 加法/正值 | 双目/单目 | 单目时表示正值,如+5 |
| - | 减法/负值 | 双目/单目 | 单目时表示负值,如-5 |
| * | 乘法 | 双目 | 无 |
| / | 除法 | 双目 | 除数不能为0,整型相除结果为整型,如1/2=0 |
| % | 取余 | 双目 | 仅适用于整型;结果符合与被除数一致; |
| ++ | 自增 | 单目 | 是变量值增1 |
| – | 自减 | 单目 | 使变量值减1 |
面试题:
1/2+1/2的结果是多少?答案是0(两个整数相除结果为0,0+0=0)1.0/2+1.0/2.0的结果是多少?答案是1.0(两个浮点型运算,0.5+0.5=1.0)自增、自减运算符(++/–)
作用:使变量值增1或者减1,适用于算术类型(整型、字符型、浮点型等)的可修改左值,实践中常用整型和字符型变量。
核心区别:运算符位置绝对变量值的更新时机。
++i/–i(前缀自增/自减)
运算规则:先更新变量值,后使用变量(先计算,后使用,自己计算,别人使用)
步骤一:变量值自增1(i = i+1)或自减1(i = i-1)
步骤二:使用更新后的变量值参与计算、赋值、比较等操作。
i++/i–(后缀自增/自减)
运算规则:先使用变量,后更新变量值(先使用,后计算,)
步骤一:使用变量当前值参与赋值、计算、比较等操作。
步骤二:变量值自增1(i = i+1)或自减1(i = i-1)
赋值运算符
基本赋值运算符
作用:将右侧运算数(常量、变量、表达式)的值存入左侧变量的内存单元。
核心规则:
1、运算顺序:自右向左(先计算右侧表达式,再赋值给左侧变量)
2、左侧必须是可修改的变量(左值),不能是常量或表达式,如5 = a、a + b = 3均为非法
3、赋值表达式的值等于左侧变量的最终值,如 b = a = 5 中,a = 5 的值为5,再赋值给b,最终b = 5.
类型转换规则(赋值时)
若赋值运算符 = 两侧类型不一致,会自动进行类型转换,转换规则如下:
| 源类型 | 目标类型 | 转换规则 | 举例 |
|---|---|---|---|
| 浮点型{double、float} | 整型(short、int、long) | 舍弃小数部分、仅保留整数部分 | int a = 5.9 → a = 5 |
| 整型(short、int、long) | 浮点型(double、float) | 数值不变,以目标浮点型格式存储 | double b = 5 → b = 5.000000 |
| 字符型(char) | 整型(short、int、long) | 字符的ASCll码存入整型低8位,高位补0 | int c = ‘A’ → c = 65 |
| 长整型(long long) | 短整型 | 截取低字节数据,可能导致数据溢出 | short d = 32768(int)→溢出 |
复合赋值运算符
复合赋值运算符是基本赋值运算符与算术运算符/位运算的结合,简化代码书写
常见复合赋值运算符:
| 运算符 | 等价形式 | 举例 | 结果 |
|---|---|---|---|
+= | a = a + b | int a=1; a+=3; | a=4(1+3) |
-= | a = a - b | int a=5; a-=2; | a=3(5-2) |
*= | a = a * b | int a=3; a*=4; | a=12(3*4) |
/= | a = a / b | int a=8; a/=2; | a=4(8/2) |
%= | a = a % b | int a=7; a%=3; | a=1(7%3) |
&= | a = a & b | int a=5; a&=6; | a=4(5&6) |
| |= | a = a| b | int a=5;a|=6; | a=7(5|6) |
^= | a = a ^ b | int a=5; a^=6; | a=3(5^6) |
<<= | a = a << b | int a=2; a<<=1; | a=4(2<<1) |
>>= | a = a >> b | int a=4; a>>=1; | a=2(4>>1) |
关系运算符
作用:比较两个运算数的大小或相等关系,结果为布尔类型(C语言用0表示假,非0表示真)
常用关系运算符(均为双目运算符):
| 运算符 | 功能 | 举例 | 结果(假设a=5,b=4) |
|---|---|---|---|
| > | 大于 | a >b | 1(true-真) |
| < | 小于 | a <b | 0(false-假) |
| >= | 大于等于 | a>=5 | 1(true-真) |
| <= | 小于等于 | b<=3 | 0(false-假)) |
| == | 等于 | a == 5 | 1(true-真) |
| != | 不等于 | a !=b | 1(true-真) |
关键注意事项:
1、避免链式比较:C语言不推荐 0 <= score <= 100(逻辑比较)这类链式写法,逻辑上会报错,编译不报错。
错误原因:编译器按左结合性依次计算,如 0 <= score <=100,等价于(0 <=score) <=
100,结果永远为真,无论score是否在0~100范围内。
正确写法:使用逻辑与运算符 score >= 0 && score <=100
2、浮点型相等比较需要用差值法介绍:由于浮点型存储精度限制(如1.1无法精确存储),直接用 == 判断相等会导致逻辑错误。
错误示例:
floata=1.1f+1.2f;// 2.300000floatb=2.3f;printf("a=%.20f,b=%.20f,%d\n",a,b,a==b);//a=2.30000019073486328125,b=2.29999995231628417969,0正确写法:
判断两数差值的绝对值是否小于极小值(如1e-6,即0.000001),需引入math.h头文件使用fabs函数(取绝对值)
#include<math.h>#defineEPSle-6//定义误差允许范围if(fabs(a-b)<EPS){//并不是真实的相等,只是判断两个数差值的绝对值是否在可接受的范围内}浮点型比较总结:
逻辑运算符
作用:布尔类型的运算数进行逻辑运算,结果仍然为布尔类型(0表示假,非0表示真)
| 运算符 | 名称 | 类型 | 运算规则 | 短路效果 |
|---|---|---|---|---|
| ! | 逻辑非 | 单目 | 真→假,假→真 | 无(仅一个运算数) |
| && | 逻辑与 | 双目 | 全1则1,有0则0 | 左侧为假时,右侧不执行 |
| || | 逻辑或 | 双目 | 全0为0,有1唯1 | 左侧为真时,右侧不执行 |
逻辑非 !
仅作用于右侧运算数,优先级较高
仅同一运算数取非奇数次,结果与原值相反;取非偶数次,结果与原值相同。
举例:
inta=5,b=0;printf("%d\n",!a);// 0(a非0为真,!真=假)printf("%d\n",!b);// 1(b=0为假,!假=真)printf("%d\n",!(a%2!=0));// 0(a%2!=0为真,!真=假)printf("%d\n",!!a);// 1(!!真=真,自右向左)逻辑与&&
运算顺序:自左向右
短路效果:若左侧运算数(表达式)为假,右侧运算数不再执行(因最终结果必为假,无需计算右侧)。
举例:
intscore=90;printf("%d\n",score>=0&&socre<=100);intx=3,y=5;if(x>y&&y++)//此时触发短路{//不执行}printf("y=%d\n",y);//输出 y = 5逻辑或||
运算顺序:自左向右
短路效果:若左侧运算数(表达式)为真,右侧运算数不再执行(因最终结果必为真,无需计算右侧)。
举例:
intyear=2025;//闰年判断:能被4整除且不能被100整除或者能被400整除printf("%d\n",(year%4==0&&year%100!=100)||year%400==0)逗号运算符
作用:将多个表达式串联成一个表达式(又称作顺序求值运算符),按从左到右的顺序执行每一个表达式。
核心规则:
1、求值顺序:从左往右依次计算每个表达式的值
2、最终结果:整个逗号表达式的值等于最后一个表达式的值。
3、优先级:逗号运算符的优先级是所有运算符中,优先级最低的。需要注意括号的使用。
举例:
#include<stdio.h>intmain(){//变量,函数中的逗号,叫做逗号分隔符inta=0,b=0;//整体是赋值表达式,(a = 3,b = 5, a + b)内是逗号表达式,结果是最后一个表达式的结果,8intresult=(a=3,b=5,a+b);//根据条件判断的逗号表达式intx=10,y=20;intmax=(x++,y++,(x>y)?x:y);//这个整体是赋值表达式,只有后面(x++,y++,(x>y) ? x : y)才是逗号表达式}建议:如果无法区分逗号表达式和赋值表达式,就看谁最后执行,谁最后执行,就是谁!
位运算
(记住在计算机中参与运算的都是补码,正数原码=反码=补码)
作用:直接对数据的二进制位(bit)进行运算,常用于嵌入式开发、底层编程、数据加密等场景。
前提:参与位运算的操作数需要先转换为二进制(signed类型按补码存储、unsigned类型按原码存储)。
按位取反(~)
类型:单目运算符(只有一个运算数)
规则:对数据的每一个二进制位取反(0→1,1→0)
注意:整数在内存中以补码形式存储,取反需要结合补码规则转换为十进制(如:~5 = -6)
演示:以8位二进制为例
| 十进制 | 二进制原码 | 按位取反 | 十进制结果 |
|---|---|---|---|
| 5 | 0000 0101 | 1111 1010 | -6 |
举例:
printf("%d\n",~5);//输出:-6(32位系统中补码运算结果)按位与(&)
类型:双目运算符
规则:对应二进制位均为1时,结果为1;否则为0(有0则0)
演示案例:以8位二进制为例
| 十进制 | 二进制补码 | 运算 | 十进制 | 二进制补码 | 结果(二进制) | 结果(十进制) |
|---|---|---|---|---|---|---|
| 5 | 0000 0101 | & | 6 | 0000 0110 | 0000 0100 | 4 |
举例:
printf("%d\n",5&6);常用场景:
1、按位清零:将特定位设为0(如num & 0xFFFFFFF将num的最低设置为0,后续展开讲解)
2、提取特定位:获取数据的某几段(如num & 0x0F提取num的低4位)寄存器、flash(相对大一点的数据)、sdcard(持久化的文件)
按位或(|)
类型:双目运算符
规则:对应二进制位有一个为1时,结果为1;否则为0(有1则1)
演示案例:以8位二进制为例
| 十进制 | 二进制补码 | 运算 | 十进制 | 二进制补码 | 结果(二进制) | 结果(十进制) |
|---|---|---|---|---|---|---|
| 5 | 0000 0101 | & | 6 | 0000 0110 | 0000 0111 | 7 |
举例:
printf("%d\n",5|6);//7按位异或(^)
类型:双目运算符
规则:对应二进制位相同为0,不同为1(相同为0,不同为1)
演示案例:以8位二进制为例
| 十进制 | 二进制补码 | 运算 | 十进制 | 二进制补码 | 结果(二进制) | 结果(十进制) |
|---|---|---|---|---|---|---|
| 5 | 0000 0101 | ^ | 6 | 0000 0110 | 0000 0011 | 3 |
常用场景:
1、数据交换:无需临时变量交换整数(a = a ^ b; b = a ^ b;a = a ^ b)
2、按位翻转:将特定为翻转(0→1,1→0)
3、加密解密:与同一个秘钥异或两次可以还原数据。
面试题:不使用临时变量,如何实现两个变量的交换?
提示:交换变量有3种方法:①创建临时变量②使用函数传址方式③使用位运算中的按位异或
这里介绍第三种:
答:无需临时变量交换整数(a = a ^ b; b = a ^ b;a = a ^ b)
易错点:在进行一正一负时,首先搞清楚正数:原码=反码=补码,如果算出来补码首位是0,那就是正数,直补码=反码
如下图所示:
按位左移(<<)
说明:将原操作数的所有二进制位整体 向左移动指定的位数。移位规则为“高舍低补”(高位溢出的二进制位直接丢弃,低位补0)。该规则与存储模式(大/小端)无关,仅取决于移位操作本身。
无符号左移
核心公式:无溢出时,结果等价于操作数∗2移动位数操作数 * 2^{移动位数}操作数∗2移动位数(溢出后公式失效)
举例:
unsignedchara=3<<3;//结果为24(3*2^3=24)unsignedintb=5<<4;//结果为80(5*2^4=80)推导:
有符号左移
(注意是通过补码的左移,低位补0,最后在转换为原码)
语法:同上
核心公式:同上
举例:
chara=-3<<3;//结果为-24(-3 * 2 ^ 3 = -24)intb=240<<2;//结果为960(240 * 2 ^2=960)推导:
8位有符号char:-3<<3
| 步骤 | 二进制(8 位) | 说明 |
|---|---|---|
| 原数 -3 | 原码:10000011 | 符号位 1(负),数值位0000011 |
反码:11111100 | 原码符号位不变,数值位取反 | |
补码:11111101 | 反码 + 1(运算基于补码) | |
| 左移 3 位 | 补码:11101000 | 高位溢出丢弃,低位补 0 |
| 移位后反码 | 11100111 | 补码 - 1(还原反码) |
| 移位后原码 | 10011000 | 反码符号位不变,数值位取反 |
| 结果 | -24 | 原码10011000对应十进制-24 |
注意:
1、有符号数的移位基于补码进行(运算过程:原码→反码→补码→移位→反码→原码→结果);
2、若移位后符号位被覆盖(如正数左移后溢出变为负数),公式不在适用,结果由补码规则决定。
反例:
如:char127;左移1位,通过公式计算=127*2^1=254,实际通过补码规则计算结果为-2按位右移(>>)
说明:将原操作数的所有二进制位整体向右移动指定的位数。移位规则为“高补低舍”(低位溢出的二进制位直接丢失,高位由操作数类型决定补值)。
| 操作数 | 高位补值规则 | 移位类型 |
|---|---|---|
| 无符号数 | 补0 | 逻辑右移 |
| 有符号数 | 补符号位(1补1,0补0) | 算术右移 |
注意:大部分编译器(如GCC,Clang,MSVC)对有符号数均采用“算术右移”,极少数特殊平台例外。
核心公式:结果等价于操作数/2移动位数操作数/2^{移动位数}操作数/2移动位数(向下取整)(注意:向下、向上、四舍五入)
无符号右移
举例:
unsignedchara=3>>3;//结果为0(3/2^3=0.375,向下取整为0)推导:
有符号右移
举例:
chara=-3>>3;//结果为-1(-3/2^3=-0.375,向下取整为-1)推导:
8位有符号char演示:-3 >> 3:
移位运算完整流程:
原数据→二进制原码→二进制反码→二进制补码→进行移位操作→二进制反码→二进制原码→目标进制结果
C语言运算符的内容到这里就结束了。如果文章里有什么讲得不够清楚或者有错误的地方,还请大家多多担待。欢迎在评论区留言交流,咱们下一章「分支与循环」再见!