一.操作符的分类
在C语言中,有许多像数学一样的操作符,与加减乘除相同的是,他们都有独属于自己的功能,可以实现不同的效果,下面将展示部分常用的操作符:
算术操作符: + - * / %
移位操作符: >> <<
位操作符: & | ^
赋值操作符: = += -= *= /= %= >>= <<= &= |= ^=
单目操作符: ! ++ -- & * + - ~ sizeof (类型)
关系操作符: > >= < <= == !=
逻辑操作符: && ||
条件操作符: ? :
逗号表达式: ,
下标引用: [ ]
函数调用: ():
结构成员访问: . ->
上述就是C语言中大部分常用的操作符,下面进行一一详细的说明之前,先补充进制方面的知识,方便后续操作符的学习。
二.进制详情和进制转换
在计算机这门课程中,我们常常会听到二进制、八进制、十进制、十六进制。不同的进制区别是表述数字的方式不同,不同进制仅仅是数字形式不同而已,就相当于one和1都表示数字一是一个道理。
1.进制详情
(1)二进制
在生活中,二进制主要用于计算机的逻辑电路和数据存储,一般是计算机的底层数字表示形式(因为二进制的数字表示形式只有0和1,可以使计算机用于底层:是和非)。二进制的数字表示形式中数字一共有2个,分别为0和1.
(2)八进制
在当前社会中,八进制的存在感还是蛮低的,八进制主要是为了简化二进制产生的,可是因为有十进制这个更好的选项来简化,所以八进制现在较少使用。八进制的数字表示形式中一共有8个数字,范围是[0,7]。其主要的作用就是早期简化二进制。为了区分其它进制的数字,写二进制数字时往往以0b或者0B开头。为了区分其它进制数字,在使用八进制表示数字时应该在前面加上0。
(3)十进制
在生活中,用的最多的数字表示形式就是10进制了,它有着易于理解的特点。我们学习的数学、物理等理科均应用的是十进制来表示数字的。十进制数字的表示形式中数字一共有10个,范围是[0,9]。十进制的作用就是为了满足人们日常生活的计数需要。为了区分其它进制,使用十进制时,往往会在数字后面加上d,但是这并不常见,应该是十进制在生活中使用比较频繁,所以为了方便就不常用同后缀。
(4)十六进制
在计算机应用中,十六进制是除了二进制以外更广泛的另一种数字表示形式了。十六进制的数字表示形式中,数字一共有16个,其中包括(0 .1. 2 .3 .4 .5 .6 .7. 8 .9 .A .B .C .D .E .F)。十六进制的作用主要是编程和硬件设计(因为十六进制可以具有很强的数字简化能力,所以常常用来表示其他进制数字比较长,不易运用的情况)。为了区分其他进制不同的数字表示形式,十六进制在使用时,往往会在数字前加上0x或者0X。
2.进制转换
(1)其它进制转十进制
在进制不同的表示形式中,每一位上的数字都有自己的权重。
比如123d这个十进制数字,它的权重就可以表示为1*10^2+2*10^1+3*10^0。当左边的权重式子相加得到的答案就是123这个十进制数字,我们可以把这个理念进行推广,用于其它进制转换十进制。

如上图所示:在十进制中,每一位对应的权重是进制数乘上10进制的位。这样我们就可以根据这个原理将其他进制的数字转换位我们熟知的十进制数字了。 下面举一个例子进行进制转换计算的强化:

如上述图片那样,二进制的1101这个数字,经过进制转换就会变成十进制的13。
(2)十进制转其它进制
在进制转换中,如果是十进制转换为其它进制,就需要用到长除法,下面对此方法进行详细的介绍:
长除法只能在十进制转其它进制的时候使用,通常是把一个十进制数字一直除以需要转化的进制数,直到将此十进制数字除到只剩0,记录产生的余数,并以从后往前的顺序写出这些余数,得到的这个新的数字便是需要转换的进制数。下面用图片举一个例子:

(3)其它进制转二进制
十进制转二进制在上面已经说过了,现在介绍八进制和十六进制转二进制。
<1>八进制转二进制
八进制的每一位都对应二进制的三位数字,所以八进制转二进制的步骤如下:
1.将八进制数的每一位单独分开。
2.将每一位八进制转换为对应的三位二进制数。
3.将每一位转换的二进制数字拼接起来。
通过上述步骤得到的便是八进制转二进制的数字。
下面是转换的图表:
八进制 | 二进制 |
---|---|
0 | 000 |
1 | 001 |
2 | 010 |
3 | 011 |
4 | 100 |
5 | 101 |
6 | 110 |
7 | 111 |
[八进制与二进制对应表] |
<2>十六进制转二进制
十六进制数的每一位对应4位二进制数。所以十六进制转二进制的步骤如下:
1.将十六进制数的每一位字符单独分开。
2.将每个十六进制字符转换为对应的4位二进制数。
3.将所有转换后的二进制数按顺序拼接起来。
通过上述步骤得到的便是二进制数字
下面是转换图表:
十六进制 | 二进制 |
---|---|
0 | 0000 |
1 | 0001 |
2 | 0010 |
3 | 0011 |
4 | 0100 |
5 | 0101 |
6 | 0110 |
7 | 0111 |
8 | 1000 |
9 | 1001 |
A | 1010 |
B | 1011 |
C | 1100 |
D | 1101 |
E | 1110 |
F | 1111 |
[十六进制与二进制对应表] |
(4)二进制转其它进制
二进制转十进制上面讲过,下面只讲二进制转八进制和十六进制:
<1>二进制转八进制
8进制的数字每一位是0~7的数字,各自写成2进制,最多有3个2进制位就够了,如7的二进制是111,所以在2进制转8进制数的时候,从2进制序列中右边低位开始向左每3个2进制位根据上面的对应表格换算⼀ 个8进制位,剩余不够3个2进制位的直接换算。

<2>二进制转十六进制
16进制的数字每一位是0~9和A~F组成的,把16进制的每一位各自写成2进制,最多有4个2进制位就够了, 比如F的二进制是1111,所以在2进制转16进制数的时候,从2进制序列中右边低位开始向左每4个2进制位根据上面的对应表格换算一个16进制位,剩余不够4个二进制位的直接换算。

三.原码、反码、补码
1.原码、反码、补码介绍
二进制的数字表示形式方法一共有三种:反码、原码、补码。
有符号整数的三种表示方法均有符号位和数值位两部分,2进制序列中,最高位是被当做符号 位,剩余的都是数值位。 符号位都是用0表示"正",用1表示"负"。
2.相互转换公式
正整数的原、反、补码都相同。
负整数的三种表示方法各不相同。 原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。 反码:将原码的符号位不变,其他位依次按位取反(1变为0;0变为1)就可以得到反码。 补码:反码+1就得到补码。 补码也可以得到原码:取反,+1的操作。
对于整形来说:数据存放内存中存放的是补码。 为什么呢? 在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
signed int a=-10;
//int占用4个字节,32个bit位。对于有符号整型来说:第一个是二进制的符号位,其他的31位就是数值位。所以a的原码形式为:1000 0000 0000 0000 0000 0000 0000 1010。其中第一位上的1表示这个二进制的数字为负数。所以a的反码为1111 1111 1111 1111 1111 1111 1111 0101。a的补码为:1111 1111 1111 1111 1111 1111 1111 0110。
//对于整型来说:数据存放在内存中的形式为二进制的补码。
3.为什么数据存放用补码?
举个简单的例子1+(-1)=0,这是个简单的运算。但是我们知道在计算机中是以二进制的数字进行计算的。
假设计算机的存储是以二进制的原码形式的:
二进制原码形式-1+1计算 | 十进制-1+1计算 |
---|---|
0000 0000 0000 0000 0000 0000 0000 0001 | 1 |
1000 0000 0000 0000 0000 0000 0000 0001 | -1 |
1000 0000 0000 0000 0000 0000 0000 0010 | -1+1 |
根据上述的表格,左边是-1+1的二进制原码形式的计算过程,但是最后计算的结果竟然是-1+1=-2。这明显不对。下面来看二进制补码形式的计算过程:
二进制补码形式-1+1计算 | 十进制-1+1计算 |
---|---|
0000 0000 0000 0000 0000 0000 0000 0001 | 1 |
1111 1111 1111 1111 1111 1111 1111 1111 | -1 |
1000 0000 0000 0000 0000 0000 0000 0000 | -1+1=0 |
[ ] |
根据上述二进制的补码形式的计算可以发现:计算机用二进制的补码形式可以计算出正确的加减法的答案,这就是为什么计算机中数据存储用二进制的补码形式的原因。
四.移位操作符: << >>
首先需要注意的是:移位操作符的操作数只能是正整数,且参与运算的均为二进制的补码。
1.左移位操作符
移位规则:左边抛弃、右边补0。
cpp
#include <stdio.h>
int main()
{
int num = 10;
int n = num<<1;
printf("n= %d\n", n);
printf("num= %d\n", num);
return 0;
}

在左移操作时,应先将操作数转换为补码的形式,再按照移位操作的移位规则进行计算。根据上面的左移操作,我们不难看出:左移1有乘2的效果。
2.右移位操作符
右移操作符分为2种,不同种类的右移操作符有着不同的移位规则。移位种类主要取决于编译器。目前主要是算术右移操作符。
①逻辑右移操作符:左边用0填补,右边丢弃。
②算术右移操作符:左边用该值符号位填充,右边丢弃。(原来为正则补0;原来为负则补1)
cpp
#include <stdio.h>
int main()
{
int num = 10;
int n = num>>1;
printf("n= %d\n", n);
printf("num= %d\n", num);
return 0;
}
下面为逻辑右移操作的图像:

下面为算术右移操作的图像:

五.位操作符
1.位操作符介绍
位操作符需要注意的是:位操作数必须是整数。且运算时的操作数均为2进制的补码形式。
位操作符 | 注释 | 运算规则 |
---|---|---|
& | 按位与 | 有0则0,全1才1 |
| | 按位或 | 有1则1,全0才0 |
^ | 按位异或 | 相同为0,相异为1 |
~ | 按位取反 | 将每一位取反 |
[位操作符介绍] |
2.例子补充
cpp
#include <stdio.h>
int main()
{
int a=6;
int b=-7;
int c1=a&b;
int c2=a|b;
int c3=a^b;
int c4=~a;
return 0;
}
下来计算一下不同c的值吧!
(1)a&b
因为a为正数,所以a的原码、反码、补码相同均为:0000 0000 0000 0000 0000 0000 0000 0110
因为b是负数,所以补码需要计算:b的原码是:1000 0000 0000 0000 0000 0000 0000 0111
经过符号位不变,其余位按位取反可以得出b的反码是:1111 1111 1111 1111 1111 1111 1111 1000
因为补码等于反码+1,所以b的补码为:1111 1111 1111 1111 1111 1111 1111 1001
上面计算了a和b的补码,下面用补码对a和b进行按位与运算
|------|-----------------------------------------|
| a的补码 | 0000 0000 0000 0000 0000 0000 0000 0110 |
| b的补码 | 1111 1111 1111 1111 1111 1111 1111 1001 |
| c的补码 | 0000 0000 0000 0000 0000 0000 0000 0000 |
经过上述的按位与运算,我们求出了c的补码,最后经过公式换算将c的补码转换为c的原码为:0000 0000 0000 0000 0000 0000 0000 0000。所以6&-7=0
下来说明一下&操作符的特点:
1.交换律:a & b = b & a
2.结合律:( a & b ) & c = a & ( b & c )
3.幂等律:a & a = a
4.零律:a & 0 = 0
5.单位元:a & -1 = a //因为-1的补码全为1,所以a & -1计算时,只要a的该位为1,结果就为1;该位为0,结果就为0。
(2)a|b
|------|-----------------------------------------|
| a的补码 | 0000 0000 0000 0000 0000 0000 0000 0110 |
| b的补码 | 1111 1111 1111 1111 1111 1111 1111 1001 |
| c的补码 | 1111 1111 1111 1111 1111 1111 1111 1111 |
经过上述的按位或运算,我们得到了c的补码经过取反+1的运算规则,可以得到c的原码是:1000 0000 0000 0000 0000 0000 0000 0001。所以6|-7=-1
这里需要注意的是:& | 和&& ||操作符不能搞混了。两者有着不同的运算效果。前者是运算操作符,关注运算过程;后者关注逻辑关系,在乎操作数的真假。
(3)a^b
|------|-----------------------------------------|
| a的补码 | 0000 0000 0000 0000 0000 0000 0000 0110 |
| b的补码 | 1111 1111 1111 1111 1111 1111 1111 1001 |
| c的补码 | 1111 1111 1111 1111 1111 1111 1111 1111 |
| c的原码 | 1000 0000 0000 0000 0000 0000 0000 0001 |
经过上述的按位异或运算,我们得到了6^-7=-1
下面说明按位异或操作符的特点:
1.交换律:a ^ b = b ^ a
2.结合律:( a ^ b ) ^ c = a ^ ( b ^ c )
3.自反性:a ^ a = 0
4.零律:a ^ 0 = a
(4)~a
|------------|-----------------------------------------|
| a的原码、反码、补码 | 0000 0000 0000 0000 0000 0000 0000 0110 |
| c的补码 | 1111 1111 1111 1111 1111 1111 1111 1001 |
| c的原码 | 1000 0000 0000 0000 0000 0000 0000 0111 |
因为按位取反为单目操作符,所以只有一个操作数。经过上述的按位取反的运算,我们可以得到~6=-7
3.代码练习
(1)练习一
题目介绍:不能创建临时变量(第三个变量),实现两个整数的交换。
写法一:
cpp
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
a = a+b;
b = a-b;
a = a-b;
printf("a = %d b = %d\n", a, b);
return 0;
}
上述代码的写法是通过数学方法写出的,通过两变量之间的相互计算达到交换的目的。
写法二:
cpp
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
a = a^b;
b = a^b;
a = a^b;
printf("a = %d b = %d\n", a, b);
return 0;
}
上述是通过按位异或操作符的规则,写出了不用第三个变量进行两整数的交换。其中按位异或操作符满足交换律,上述代码可以变换成:b=a^b^b a=a^b^a。如左边所示b^b等于0,而a^0=a,所以就得到了题目要求。
(2)练习二
题目描述:编写代码实现:求一个整数存储在内存中的二进制中1的个数。
写法一:
cpp
#include <stdio.h>
int main()
{
int num = 0;
scanf("%d",&num);
int count= 0;//计数
while(num)
{
if(num%2 == 1)
count++;
num = num/2;
}
printf("⼆进制中1的个数 = %d\n", count);
return 0;
}
方法一:num=num/2相当于将num的二进制位向右移1位,然后根据num%2==1来判断二进制最后一位上的数字是否为1。理由是:在十进制的数字中,奇数的二进制最后一位一定是1;偶数的最后一位一定为0。根据这个性质我们判断一个二进制的最后一位,就右移1次,这样就可以统计二进制中1的个数了。(在编译器仅支持逻辑右移,或者该数为整数情况下)
写法二:
cpp
#include <stdio.h>
int main()
{
int num = 0;
scanf("%d",&num);
int i = 0;
int count = 0;//计数
for(i=0; i<32; i++)
{
if( num & (1 << i) )
count++;
}
printf("⼆进制中1的个数 = %d\n",count);
return 0;
}
方法二: num & (1 << i)的意思是将1先左移i位,就可以让1<<i这个二进制数的倒数第i位变成1,同时其余位为0。&的意思是按位与,当num和前者进行按位与操作,如果结果不是0,则说明num二进制数的倒数第i位不是0,是1。以此类推...就可以统计所有二进制位上1的个数了。
写法三:
cpp
#include <stdio.h>
int main()
{
int num = -1;
int i = 0;
int count = 0;//计数
while(num)
{
count++;
num = num&(num-1);
}
printf("⼆进制中1的个数 = %d\n",count);
return 0;
}
方法三:num = num&(num-1)的意思是将num和num最右端为0情况下的数字进行按位与操作。两者按位与最后赋值给num使得num最右端的1变为0,统计次数,得到的次数就是num二进制数1的个数。
(3)练习三
题目描述:编写代码将13二进制序列的第5位修改为1,然后再改回0。

代码展示:
cpp
#include <stdio.h>
int main()
{
int a = 13;
a = a | (1<<4);
printf("a = %d\n", a);
a = a & ~(1<<4);
printf("a = %d\n", a);
return 0;
}
上述代码通过让1的二进制数的最后一位的1挪到倒数第五位上,然后进行按位异或和按位取反操作。通过运算规则达到了题目的要求。
(4)练习四
题目描述:写一个代码,判断n是否为2的次方数
cpp
#include <stdio.h>
int main()
{
int n=0;
scanf("%d",&n);
if(n&(n-1)==0)
printf("该数是2的次方数");
return 0;
}
上述代码通过数学上的知识:当n是2的次方数,就能说明n的二进制数序列中只有1个1。从而达到了题目要求。
六.单目操作符
下面是单目操作符常用的几种:
!、++、--、&、*、+、-、~ 、sizeof、(类型)
操作符 | 注释 |
---|---|
! | 非 |
++ | 自增操作符 |
-- | 自减操作符 |
& | 取地址操作符 |
* | 解引用操作符 |
+ | 正号 |
- | 负号 |
~ | 按位取反操作符 |
sizeof | 求内存大小操作符 |
(类型) | 强制类型转换操作符 |
其中需要知道的是单目操作符只有一个操作数,并且上方的单目操作符中只有& *两者未介绍,等到指针章节尽情期待!
七.逗号操作符
1.逗号操作符介绍
逗号操作符就是用逗号隔开的多个表达式。 逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
2.练习题
下面做一些逗号操作符的练习题吧:
cpp
//代码1
int a = 1;
int b = 2;
int c = (a>b, a=b+10, a, b=a+1);//逗号表达式
c是多少?
//代码2
if (a =b + 1, c=a / 2, b > 0)
上述两个代码中,代码一的c等于13;代码二if语句内部的表达式结果为1。
八.下标访问操作符
下标引用操作符,顾名思义就是引用数组下标时使用的操作符,操作数:一个数组名+一个索引值(下标)
int arr[10];
//创建数组 arr[9] = 10;
//使用下标引用操作符时, [ ]的两个操作数是arr和9。
九.函数调用操作符
cpp
#include <stdio.h>
void test1()
{
printf("hehe\n");
}
void test2(const char *str)
{
printf("%s\n", str);
}
int main()
{
test1(); //这⾥的()就是作为函数调⽤操作符。
test2("hello bit.");//这⾥的()就是函数调⽤操作符。
return 0;
}
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。所以函数调用操作符的操作数至少有1个。
十.结构体成员访问操作符
1.结构体的介绍
C语言已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类型还是不够的,假设我想描述一位学生的信息,描述一本书,这时单一的内置类型是不行的。 描述一个学生需要名字、年龄、学号、身高、体重等; 描述一本书需要作者、出版社、定价等。C语言为了解决这个问题,增加了结构体这种自定义的数据类型,让程序员可以自己创造适合的类型。结构体可以类比做数组,只不过数组是相同元素的集合,而结构体则是不同类型元素的集合,这些不同的类型被称为结构体的成员变量。
结构体是C语言中一种用户自定义的复合数据类型,允许将不同类型的数据组合成一个整体。通过结构体,可以更方便地管理相关联的数据。
2.结构体的声明
struct tag
{
member-list;
} variable-list ;
//tag为定义该结构体的变量名
//member-list为该结构体的成员列表
//variable-list为该结构体的变量列表
下面给出一个结构体定义的例子:
cpp
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
上面代码中定义了名为Stu的结构体,其中结构体成员变量有名字、年龄、性别、学号。
3.结构体的定义和初始化
cpp
//代码1:变量的定义
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//代码2:初始化。
struct Point p3 = {10, 20};
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s1 = {"zhangsan", 20};//初始化
struct Stu s2 = {.age=20, .name="lisi"};//指定顺序初始化
//代码3
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
上述三个代码讲述了结构体的定义和初始化,结构体的定义应该注意结构体的结构,不要忘记末尾的分号;结构体的初始化需要在大括号内部进行,并且要对应结构体成员变量的顺序。下面给出更详细的例子便于理解:
cpp
#include <stdio.h>
struct student
{
char name[20];
int age;
float score;
} S4,S5,S6;
struct student S3;
int main()
{
int a;
struct student S1;
struct student S2;
return 0;
}
上述代码展示了6种不同的创建结构体变量的方法,其中a,S1,S2为局部变量,因为它们两个在main函数的大括号内部。S3,S4,S5,S6为全局变量。
4.结构体成员访问操作符
结构体成员访问根据已知条件的不同,分为两种,一种是直接访问;另一种是间接访问。
(1)结构成员直接访问操作符
当我们已知结构体的成员名,这时候就应该用结构体直接访问操作符。结构体成员的直接访问是通过点操作符 . 访问的。点操作符接受两个操作数。使用方式:结构体变量.成员名,下面给出直接访问的例子:
cpp
#include <stdio.h>
struct Point
{
int x;
int y;
}p = {1,2};
int main()
{
printf("x: %d y: %d\n", p.x, p.y);
return 0;
}
(2)结构成员间接访问操作符
有时候我们得到的不是一个结构体变量,而是得到了一个指向结构体的指针名。如下所示: 使用方式:结构体指针->成员名
cpp
#include <stdio.h>
struct Point
{
int x;
int y;
};
int main()
{
struct Point p = {3, 4};
struct Point *ptr = &p;
ptr->x = 10;
ptr->y = 20;
printf("x = %d y = %d\n", ptr->x, ptr->y);
return 0;
}
(3)综合举例
cpp
#include <stdio.h>
#include <string.h>
struct Stu
{
char name[15];//名字
int age; //年龄
};
void print_stu(struct Stu s)
{
printf("%s %d\n", s.name, s.age);
}
void set_stu(struct Stu* ps)
{
strcpy(ps->name, "李四");
ps->age = 28;
}
int main()
{
struct Stu s = { "张三", 20 };
print_stu(s);
set_stu(&s);
print_stu(s);
return 0;
}
本章节主要讲述操作符,具体的结构体知识尽情期待!
十一.操作符的属性
1.优先性
优先级指的是:如果一个表达式包含多个运算符,哪个运算符应该优先执行。各种运算符的优先级是不一样的。
3 + 4 * 5; //上面示例中,表达式 3 + 4 * 5 里面既有加法运算符( + ),又有乘法运算符( * )。由于乘法的优先级高于加法,所以会先计算 4 * 5 ,而不是先计算 3 + 4 。
2.结合性
如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执行顺序。大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行),比如赋值运算符( = )。
5 * 6 / 2; //上面示例中, * 和 / 的优先级相同,它们都是左结合运算符,所以从左到右执行,先计算 5 * 6 , 再计算 / 2 。
3.优先性结合性排序

十二.表达式求值
1.整型提升
(1)整型提升介绍
整型提升(是C/C++等编程语言中的一种隐式类型转换规则,指在表达式中将小于int或unsigned int的整型类型(如char、short)临时提升为int或unsigned int后再参与运算。其目的是优化计算效率,因为CPU通常对int类型的操作更高效。
(2)整型提升的意义

(3)如何进行整型提升?
整型提升规则:
-
有符号整数提升是按照变量的数据类型的符号位来提升的。
-
无符号整数提升,高位补0。
cpp
//负数的整形提升
char c1 = -1;
变量c1的⼆进制位(补码)中只有8个⽐特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,⾼位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的⼆进制位(补码)中只有8个⽐特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,⾼位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//⽆符号整形提升,⾼位补0
2.算术转换
算术转换指在表达式中混合不同类型操作数时,编译器自动将操作数转换为同一类型后再执行运算的规则。遵循隐式类型提升原则,确保计算精度和一致性。当某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。
算术转换的优先级:
bool →char → short → int → unsigned → long → float → double → long double
3.问题表达式解析
本节旨在了解书写表达式时,会出现问题的代码。目的是要我们知道:即使有了操作符的优先级和结合性,我们写出的表达式依然有可能不能通过操作符的属性确定唯⼀的计算路径,那这个表达式就是存在潜在风险的,建议不要写出特别复杂的表达式。
(1)表达式1
//表达式的求值由操作符的优先级决定。
//表达式1: a * b + c * d + e * f
表达式1在计算的时候,由于 * 比 + 的优先级高,只能保证, * 的计算是比 + 早,但是优先级并不能决定第三个 * 比第⼀个 + 早执行。 所以表达式的计算机顺序就可能是下面两种情况:


(2)表达式2
表达式2: c + -- c;
同上,操作符的优先级只能决定自减 -- 的运算在 + 的运算的前面,但是我们并没有办法得知, + 操作符的左操作数的获取在右操作数之前还是之后求值,所以结果是不可预测的,是有歧义的。
(3)表达式3
cpp
//表达式3
int main()
{
int i = 10;
i = i-- - --i * ( i = -3 ) * i++ + ++i;
printf("i = %d\n", i);
return 0;
}
表达式3因为符号之间对应不明确,所以在不同编译器上的结果是不同的,是问题表达式。下面给出不同编译器的结果:

(4)表达式4
cpp
#include <stdio.h>
int fun()
{
static int count = 1;
return ++count;
}
int main()
{
int answer;
answer = fun() - fun() * fun();
printf( "%d\n", answer);//输出多少?
return 0;
}
虽然在大多数的编译器上求得结果都是相同的。 但是上述代码 answer = fun() - fun() * fun(); 中我们只能通过操作符的优先级得知:先算乘法,再算减法。 但是函数调用的先后顺序无法通过操作符的优先级确定。所以是个问题表达式。
(5)表达式5
cpp
//表达式5
#include <stdio.h>
int main()
{
int i = 1;
int ret = (++i) + (++i) + (++i);
printf("%d\n", ret);
printf("%d\n", i);
return 0;
}
这段代码中的第一个 + 在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个 + 和第三个前置 ++ 的先后顺序。下面是gcc和VS2022不同的结果:

