十 操作符详解
👉 欢迎阅读这篇文章 👇
目录
1、操作符的分类
- 算术操作符:
+、-、*、/、% - 移位操作符:
<<、>> - 位操作符:
&、|、^ - 赋值操作符:
=、+=、-=、*=、/=、%=、<<=、>>=、&=、 |=、^= - 单⽬操作符:
!、++、--、&、*、+、-、~、sizeof、(类型) - 关系操作符:
>、>=、<、<=、==、!= - 逻辑操作符:
&&、|| - 条件操作符:
? : - 逗号表达式:
, - 下标引用:
[ ] - 函数调用:
( ) - 结构成员访问:
. 、->
上述操作符中部分与二进制有关,下面铺垫一些二进制和进制转换的知识
2、 二进制和进制转换
2进制、8进制、10进制、16进制只是数值的不同表现形式。
比如:数值15的各种进制的不同表现形式
15的2进制:1111
15的8进制:17
15的10进制:15
15的6进制:F
16进制的数值之前写:0x
8进制的数值之前写:0
下面重点介绍二进制:
首先还是要从10进制来看
- 10进制中满10进1
- 10进制中的数字每一位都是0~9组成的
二进制也一样
- 2进制中满2进1
- 2进制中的数字每一位都是0~1组成的
2.1 进制转换
2.1.1 2进制转10进制
10进制的123表示的值是一百二十三,因为10进制的每一位都有权重,10进制的数字从右到左是个位、十位、百位,分别每一位的权重是 10 0 10^0 100、 10 1 10^1 101、 10 2 10^2 102
2进制和10进制是一样的,只不过2进制每一位的权重从右到左分别是 2 0 2^0 20、 2 1 2^1 21、 2 2 2^2 22
比如2进制的1101
2.1.2 10进制转2进制
125转为2进制的过程如下
即为:1111101
2.1.3 2进制转8进制
8进制的数字由0~7组成,0~7的数字,各自写成2进制,最多有3个2进制位就可以了,所以在2进制转8进制的时候,从2进制序列中右边低位开始向左每3个2进制位会换算成一个8进制位,剩余不够3个2进制位的拿0补。
2进制01101011转换为8进制为0153,0开头的数字以表示为8进制数字
2.1.4 8进制转2进制
把8进制数的每一位,都替换成对应的3 位2进制数(不足 3 位的,高位补 0 凑够 3 位)。把所有转换后的2进制数,按原来的顺序从左到右拼起来。
2.1.5 2进制转16进制
16进制的数字是由0~9、a~f组成的0~9, a~f的数字,各⾃写成2进制,最多有4个2进制位就⾜够了,⽐如 f 的⼆进制是1111,所以在2进制转16进制的时候,从2进制序列中右边低位开始向左每4个2进制位换算成1个16进制位,不够4个的拿0补。
2.1.6 16进制转换为2进制
把16进制数的每一位,都替换成对应的4 位2进制数(不足 4 位的,高位补 0 凑够 4 位)。把所有转换后的2进制数,按原来的顺序从左到右拼起来。
3、原码、反码、补码
整数的2进制表示方法有三种,即原码、反码、补码
3.1 有符号整数
有符号整数的三种表示方法均有符号位和数值位,2进制序列中,最高位的1位被当作符号位,剩余的都是数值位。符号位用0表示正,用1表示负。
正整数的原、反、补码都相同
负整数的三种表示方法各不相同
原码:直接将数值按照正负数的形式翻译成二进制就是原码
反码:保持原码的符号位不变,其他位次按位取反即可
补码:反码+1得到补码
由补码得到原码也可以:取反,+1
3.2无符号整数
无符号整数的三种2进制表示相同,没有符号位,每一位都是数值位
对于整形来说:数据存放内存中其实存放的是补码。
4、移位操作符
<< 左移操作符>> 右移操作符
移位操作符是作用于数值的补码,而打印的是数值的原码
注意:移位操作符的操作数只能是整数
4.1左移操作符
移位规则:左边抛弃、右边补0
#include <stdio.h>
int main()
{
int num = 10;
int n = num << 1;
printf("n = %d\n", n);
printf("num = %d\n", num);
return 0;
}


4.2右移操作符
移位规则:⾸先右移运算分两种:
1.逻辑右移:左边⽤0填充,右边丢弃
2.算术右移:左边⽤原该值的符号位填充,右边丢弃
#include <stdio.h>
int main()
{
int num = -10;
int n = num >> 1;
printf("n = %d\n", n);
printf("num = %d\n", num);
return 0;
}


警告:对于移位运算符,不要移动负数位
例如:
int num = 10;
num >> -1;//error
5、位操作符:&、|、^、~
位操作符有:
&:按位与|:按位或^:按位异或~:按位取反
注:他们的操作数必须是整数。
5.1 &:按位与
对应的补码二进制位,有0则为0,同时为1才为1
int main()
{
int a = 10;
int b = -7;
int c = a & b;
//a中的补码:00000000000000000000000000001010
//b中的补码:11111111111111111111111111111001
//c中的补码:00000000000000000000000000001000
printf("%d", c);//8
return 0;
}
5.2|:按位或
对应的补码二进制位,有1则为1,同时为0才为0
int main()
{
int a = 10;
int b = -7;
int c = a | b;
//a中的补码:00000000000000000000000000001010
//b中的补码:11111111111111111111111111111001
//c中的补码:11111111111111111111111111111011
//c中的原码:10000000000000000000000000000101
printf("%d", c);//-5
return 0;
}
5.3^:按位异或
对应的补码二进制位,相同则为0,不同则为1
int main()
{
int a = 10;
int b = -7;
int c = a ^ b;
//a中的补码:00000000000000000000000000001010
//b中的补码:11111111111111111111111111111001
//c中的补码:11111111111111111111111111110011
//c中的原码:10000000000000000000000000001101
printf("%d", c);//-13
return 0;
}
5.4~:按位取反
对应的补码二进制位,1变为0,0变为1
int main()
{
int a = 0;
//a中的补码:00000000000000000000000000000000
//按位取反后a中的补码:11111111111111111111111111111111
//按位取反后a中的原码:10000000000000000000000000000001
printf("%d", ~a);//-1
return 0;
}
5.5应用1
不创建临时变量,实现两个整数的交换。
可以想到一种方法
#include <stdio.h>
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("a = %d b = %d\n", a, b);
a = a + b;
b = a - b;
a = a - b;
printf("a = %d b = %d\n", a, b);
return 0;
}
这种方法当数很大的时候,可能存在潜在问题,a+b可能会溢出。
由按位异或可以得到
- 0 ^ a a a= a a a
- a a a ^ a a a= a a a
- 按位异或满足交换律: a a a ^ b b b ^ a a a= a a a ^ a a a ^ b b b= b b b
可以使用按位异或的方法进行交换,代码如下:
#include <stdio.h>
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("a = %d b = %d\n", a, b);
a = a ^ b;
b = a ^ b;//a ^ b ^ b = a
a = a ^ b;//a ^ b ^ a = b
printf("a = %d b = %d\n", a, b);
return 0;
}

5.6练习
练习1:编写代码实现:求⼀个整数存储在内存中的⼆进制中1的个数。
方法1:
首先想到一位一位的判断是否为1,类比10进制的时候的%10、/10得到最后一位判断后再消去最后一位,2进制就可以利用%2、/2的方法。
代码:
//方法1
#include <stdio.h>
int main()
{
int a = 13;
//00000000000000000000000000001101原码
//00000000000000000000000000001101补码
int count = 0;
while (a)
{
if (a % 2 == 1)
count++;
a /= 2;
}
printf("%d", count);//3
return 0;
}
这种方法的问题:
- 无法处理负数,假设为-1,由负数的取模运算可知,最终结果的正负取决于第一个运算数,这里就会得到-1,永远不会满足if条件,就不会计数,导致出错。

- 效率较低
方法2:
想要得到一个数的最后一位可以使用n & 1的方法,这里就可以利用循环的方法,按位与1后,将这个数进行右移,把每一位都化成最后一位,进行判断。
代码如下:
#include <stdio.h>
int main()
{
int n = 13;
//00000000000000000000000000001101原码
//00000000000000000000000000001101补码
int count = 0;//计数
for (int i = 0;i < 32;i++)
{
if ((n >> i) & 1 == 1)
count++;
}
printf("%d", count);
return 0;
}
这种方法需要循环32次,效率还是较低
方法3:
利用n = n & (n-1),这个表达式的作用是去掉二进制中最右边的1。当最终为0时,就去掉了所有的1,去掉几次就与几个0,不想要将所有32位数字都循环。
代码如下:
#include <stdio.h>
int main()
{
int n = 13;
//00000000000000000000000000001101原码
//00000000000000000000000000001101补码
int count = 0;//计数
while (n)
{
count++;
n = n & (n - 1);
}
printf("%d", count);
return 0;
}
练习2:⼆进制位置0或者置1
编写代码将13⼆进制序列的第5位修改为1,然后再改回0
13的2进制序列: 00000000000000000000000000001101
将第5位置为1后:00000000000000000000000000011101
将第5位再置为0:00000000000000000000000000001101
分析:
- 为了将第5位置为1(0变1),就要考虑按位或,按位或的另一个操作数的第5位应该是1,其他位是0就不会改变其他位的数字,即:
00000000000000000000000000010000,这个数字是1<<4。 - 为了将第5位再置为0(1变0),就要考虑按位与,按位与的另一个操作数的第5位就应该是0,其他的就应该是1,即:
11111111111111111111111111101111,这个数字是~1<<4。
代码如下:
#include <stdio.h>
int main()
{
int n = 13;
n = n | (1 << 4);
printf("%d\n", n);
n = n & ~(1 << 4);
printf("%d\n", n);
return 0;
}

6、单目操作符
单目操作符有这些:!、++、--、&、*、+、-、~、sizeof、(类型)
其中这些操作符只有&、*还没有介绍,这两个在介绍完指针后再介绍。
7、逗号表达式
exp1,exp2,wxp3,...expN
逗号表达式,就是用逗号隔开的多个表达式。
逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
例:
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);
//c=13
8、下标访问[]、函数调用( )
8.1下标引用操作符
操作数:一个数组名+一个索引值(下标)
int arr[10];//创建数组
arr[9]= 10;//使用下标引用操作符
//[]的操作数是arr和9
8.2函数调用操作符
接受⼀个或者多个操作数:第⼀个操作数是函数名,剩余的操作数就是传递给函数的参数。至少有一个操作数。
9、结构成员访问操作符
9.1结构体
C语言提供了很多的内置类型,如char、short、int、long、float、double但是只有这些内置类型不够描述一些复杂的对象。比如要描述一个学生,需要描述名字、年龄、学号、⾝⾼、体重等。C语言为了解决这个问题,定义了结构体这一数据类型,让程序员可以⾃⼰创造适合的类型。
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。如:标量、数组、指针,甚⾄是其他结构体。
9.1.1结构的声明
struct tag
{
member-list;
}variable-list;
struct是结构体的关键字tag是自定义的名字member-list成员列表可以是一个或者多个- 最后的分号不要丢
variable-list是定义的变量,可有可无。
描述一个学生:
struct student//结构体类型声明
{
char name[20];//姓名
int age;//年龄
float score;//分数
//这里不允许初始化,只是类型的声明
};
9.1.2结构体变量的定义和初始化
结构体变量的定义
struct student//声明类型
{
char name[20];//姓名
int age;//年龄
float score;//分数
}s1,s2,s3;
//全局变量
struct student s4;
struct student s5;
int main()
{
//局部变量
struct student s6;
struct student s7;
struct student s8;
return 0;
}
上方代码定义了8个变量
结构体变量的初始化
struct student
{
char name[20];//姓名
int age;//年龄
float score;//分数
};
int main()
{
//局部变量
struct student s1 = {"zhangsan",18,85.0f};//初始化
struct student s2 = { "lisi",19,90.0f };//初始化
struct student s3 = { .age=18,.name="wangwu",.score=86.0f};//指定顺序初始化
return 0;
}
结构体嵌套
struct student//结构体声明
{
char name[20];//姓名
int age;//年龄
float score;//分数
};
struct test//结构体声明
{
int x;
int y;
struct student s;//结构体嵌套
};
int main()
{
//局部变量
struct student s1 = { "zhangsan",18,85.0f };//初始化
struct student s2 = { "lisi",19,90.0f };//初始化
struct student s3 = { .age = 18,.name = "wangwu",.score = 86.0f };//指定顺序初始化
struct test t1 = { 10,20,{"liliu",20,90.0f} };//结构体嵌套初始化
struct test t2 = { .s.name = "liuwu",.s.age = 18,.s.score = 80.0f,.x = 10,.y = 20 };;//结构体嵌套初始化,指定顺序初始化
return 0;
}
9.2结构成员访问操作符
9.2.1结构成员的直接访问
结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数。如下所⽰:打印定义的结构体成员
struct student
{
char name[20];//姓名
int age;//年龄
float score;//分数
};
struct test
{
int x;
int y;
struct student s;
};
int main()
{
//局部变量
struct student s1 = { "zhangsan",18,85.0f };//初始化
struct student s2 = { "lisi",19,90.0f };//初始化
struct student s3 = { .age = 18,.name = "wangwu",.score = 86.0f };//指定顺序初始化
struct test t1 = { 10,20,{"liliu",20,90.0f} };//结构体嵌套初始化
struct test t2 = { .s.name = "liuwu",.s.age = 18,.s.score = 80.0f,.x = 10,.y = 20 };//结构体嵌套初始化,指定顺序初始化
//结构体成员的打印
printf("%s ,%d ,%.1f\n", s1.name, s1.age, s1.score);//.操作符访问成员,操作数位变量名和结构体成员
printf("%s ,%d ,%.1f\n", s2.name, s2.age, s2.score);
printf("%s ,%d ,%.1f\n", s3.name, s3.age, s3.score);
printf("%d ,%d ,%s ,%d ,%.1f\n", t1.x, t1.y, t1.s.name, t1.s.age, t1.s.score);
return 0;
}

使⽤⽅式:结构体变量.成员名
9.2.2结构体成员的间接访问
指针介绍完后更新
10、操作符的属性
C语言的操作符具有两个重要属性:优先级、结合性,这两个属性决定了表达式求值的计算顺序。
- 优先级指的是,如果⼀个表达式包含多个运算符,哪个运算符应该优先执⾏。各种运算符的优先级是不⼀样的。
- 如果两个运算符优先级相同,优先级没办法确定先计算哪个了,这时候就看结合性了,则根据运算符是左结合,还是右结合,决定执⾏顺序。⼤部分运算符是左结合(从左到右执⾏),少数运算符是右结合(从右到左执⾏),⽐如赋值运算符( = )。
运算符的优先级顺序很多,下⾯是部分运算符的优先级顺序(按照优先级从⾼到低排列)
- 圆括号
() - 自增运算符
++、自减运算符-- - 单目运算符
+、- - 乘法
*、除法\ - 加法
+、减法- - 关系运算符
>、<等 - 赋值运算符
=
由于圆括号的优先级最⾼,可以使⽤它改变其他运算符的优先级。
详细的优先级如下,使用时可以查询:
https://zh.cppreference.com/c/language/operator_precedence
11、表达式求值
11.1整型提升
C语言中整型算术运算总是至少以默认整型类型的精度来进行。
为了获得这个精度,表达式中的字符和短整型操作数在使⽤之前被转换为普通整型,也就是说char、short 这种「比 int 小的整型」,只要参与运算,会自动先变成 int 类型这个自动转换过程,称为整型提升。
只有
char / signed char / unsigned charshort / unsigned short
只要它们参与任何运算,立刻提升为 int。
例如:
char a,b,c;
...
a = b + c;
b和c的值先被提升为普通整型,然后再执行加法运算。
加法运算完成后再被截断变回char类型,存储到a中。
如何进⾏整体提升呢?
- 有符号整数提升是按照变量的数据类型的符号位来提升的,也就是在⾼位补充符号位
- ⽆符号整数提升,⾼位补0
负数的整型提升
char c1 = -1;
//char为有符号的
//10000001原码
//11111111补码
//11111111111111111111111111111111 高位补1,整型提升
正数的整形提升
char c1 = 1;
//char 是有符号的
//00000001原码
//00000001补码
//00000000000000000000000000000001 高位补0,整型提升
例:
#include <stdio.h>
int main()
{
char a = 10;
//00000000000000000000000000001010截断
//00001010存储到a中
char b = 120;
//00000000000000000000000001111000截断
//01111000存储b中
char c = 0;
c = a + b;
//a
//00001010
//00000000000000000000000000001010整型提升
//b
//01111000
//00000000000000000000000001111000整型提升
//运算
//00000000000000000000000010000010结果-截断
//10000010存储c中
printf("%d", c);
//10000010再次提升
//11111111111111111111111110000010补码
//10000000000000000000000001111110原码
return 0;
}
上述代码的过程:
a是一个char类型的数据,给他赋值了一个整数13,要先截断成8位二进制序列后储存到a中。b同理。然后执行运算,执行运算之前要先将a和b进行整形提升,提升为32位二进制序列,然后进行运算,运算完成后赋值给c,再次截断为8位二进制序列储存到c中。接着要将c进行传参到printf函数中,需要将c再次整形提升至32位二进制序列后转为原码打印
11.2 算术转换
如果某个操作符的各个操作数属于不同的类型,那么除⾮其中⼀个操作数的转换为另⼀个操作数的类型,否则操作就⽆法进⾏。下⾯的层次体系称为寻常算术转换。
long double
double
float
unsigned long int
long int
unsigned int
int
如果某个操作数的类型在上⾯这个列表中排名靠后,那么⾸先要转换为另外⼀个操作数的类型后执⾏运算。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)