【C语言】9000字长文操作符详解
简单不先于复杂,而是在复杂之后。
目录
1. 操作符分类
2. 算数操作符
3. 移位操作符
3.1 左移操作符
3.2 右移操作符
4. 位操作符
4.1 按位与 &
4.2 按位或 |
4.3 按位异或 ^
4.4 一道变态的面试题
4.5 练习
5. 赋值操作符
5.1 复合赋值符
6. 单目操作符
6.1 单目操作符介绍
6.2 sizeof 和数组
6.3 ~
6.4 ++ --
6.5 *
6.6 ( )
7. 关系操作符
8. 逻辑操作符
一道360笔试题
9. 条件操作符(三目操作符)
10. 逗号表达式
11. 下标引用、函数调用和结构成员
11.1 [ ]下标引用操作符
11.2 ( )函数调用操作符
11.3 结构成员访问操作符
12. 表达式求值
12.1 隐式类型转换
12.2 算数转换
12.3 操作符的属性
1. 操作符分类
- 算数操作符
- 移位操作符
- 位操作符
- 赋值操作符
- 单目操作符
- 关系操作符
- 逻辑操作符
- 条件操作符
- 逗号表达式
- 下标引用、函数调用和结构成员
2. 算数操作符
+ - * / %
1.除了%操作符之外,其他几个操作符可以用于整数和浮点数
2.对于/操作符如果两个操作数都为整数,执行整数乘法。而只要有浮点数执行的就是浮点数除法。
3.%操作符的两个操作数必须为整数,返回的是整除之后的余数。
3. 移位操作符
<< 左移操作符 >> 右移操作符
注:移位的操作数只能是整数。
二进制
整数的二进制有三种:
1. 原码
2. 反码
3. 补码
正整数的原码,补码,反码相同
负整数的原码,反码,补码需要计算
举个例子:
7
00000000000000000000000000000111 - 原码(最高位是符号位,0表示正数,1表示负数)
00000000000000000000000000000111 - 反码
00000000000000000000000000000111 - 补码
-7
10000000000000000000000000000111 - 原码
111111111111111111111111111111111000 - 反码(原码符号位不变,其他位按位取反)
111111111111111111111111111111111001 - 补码(反码+1就是补码)
整数在内存中存储的是补码。
3.1 左移操作符
移位规则
左边抛弃,右边补0
左移有乘2的效果。
3.2 右移操作符
移位规则
首先右移运算分两种:
1. 逻辑移位: 左边用 0 填充,右边丢弃
2. 算数移位: 左边用该值的符号位填充,右边丢弃
上面这是逻辑移位。
上面这是算术移位
VS2019编译器采用算术右移(是哪种移位规则取决于编译器)
警告:对于移位操作符,不要移动负数位,这个是标准未定义的。
例如:
int num= 10; num>>-1;//error
4. 位操作符
& //按位与 | //按位或 ^ //按位异或
注:它们的操作数必须是整数
4.1 按位与 &
只有两个数的二进制同时为1,结果才为1,否则为0.
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>int main()
{int a = 3;int b = -5;int c = a & b;//00000000000000000000000000000011 - 3的补码//10000000000000000000000000000101 - -5的原码//11111111111111111111111111111010 - -5的反码//11111111111111111111111111111011 - -5的补码//00000000000000000000000000000011 - 3的补码//11111111111111111111111111111011 - -5的补码//00000000000000000000000000000011 - 补码(原码)//%d意味着打印一个有符号的整数printf("%d\\n", c);//3return 0;
}
4.2 按位或 |
参加运算的两个数只要两个数中的一个为1,结果就为1。
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h>int main() {int a = 3;int b = -5;int c = a | b;//00000000000000000000000000000011 - 3的补码//10000000000000000000000000000101 - -5的原码//11111111111111111111111111111010 - -5的反码//11111111111111111111111111111011 - -5的补码//00000000000000000000000000000011 - 3的补码//11111111111111111111111111111011 - -5的补码//11111111111111111111111111111011 - 补码//11111111111111111111111111111010 - 反码//10000000000000000000000000000101 - 原码printf("%d\\n", c);return 0; }
4.3 按位异或 ^
相同为0,相异为1。
0 ^ a = a;a ^ a = 0;//异或操作符支持交换律 a ^ b ^ a = b;
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h>int main() {int a = 3;int b = -5;int c = a ^ b;//00000000000000000000000000000011 - 3的补码//10000000000000000000000000000101 - -5的原码//11111111111111111111111111111010 - -5的反码//11111111111111111111111111111011 - -5的补码//00000000000000000000000000000011 - 3的补码//11111111111111111111111111111011 - -5的补码//11111111111111111111111111111000 - 补码//11111111111111111111111111110111 - 反码//10000000000000000000000000001000 - 原码printf("%d\\n", c);return 0; }
4.4 一道变态的面试题
不能创建临时变量(第三个变量),实现两个数的交换。
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h>int main() {int a = 3;int b = 5;printf("交换前:a = %d,b = %d\\n", a, b);a = a ^ b;//3 ^ 5b = a ^ b;//3 ^ 5 ^ 5a = a ^ b;//3 ^ 5 ^ 3printf("交换后:a = %d,b = %d\\n", a, b);return 0; }
4.5 练习
编写代码实现:求一个整数存储在内存中二进制中1的个数。
(求补码的二进制中1的个数)
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h>//编写代码实现:求一个整数存储在内存中二进制中1的个数。 //(求补码的二进制中1的个数)int main() {int a = 0;int count = 0;scanf("%d", &a);int i = 0;for (i = 0; i < 32; i++){if (a & 1){count++;}a = a >> 1;}printf("这个整数存储在内存中二进制中1的个数为%d", count);return 0; }
我在编写代码的时候写了个bug,即在循环中
a >> 1
操作虽然把a
右移了一位,但是没有将右移后的值赋给a
,导致循环中的a
始终没有变化,最终会导致死循环。所以大家写程序的时候要注意一下,避免写出bug。
5. 赋值操作符
int weight= 120;//体重
weight = 89;//不满意就赋值
double salary = 10000.0;
salary = 20000.0;//使用赋值操作符赋值//赋值操作符可以连续使用
int a = 10;
且int x = 0;
int y = 20;
a = x = y + 1;//连续赋值
//这样的代码感觉怎么样?//同样的语义:
x = y + 1;
a = x;
//这样的写法清新爽朗易于调试。
5.1 复合赋值符
+=
-=
*=
/=
%=
>>=
&=
|=
^=
//这些运算符都可以写作复合的效果。 //比如: int x = 0; x = x + 10; x += 10;//复合赋值 //其他运算符一样的道理,这样更简洁。
6. 单目操作符
6.1 单目操作符介绍
在C语言中,0表示假,非0表示真。
&取出的是变量在内存中的起始地址。
6.2 sizeof 和数组
sizeof 计算类型所创建变量占据空间的大小,还可以计算整个数组的大小。
sizeof 是操作符,不是函数。
strlen 是库函数,用来求字符串长度。
下面做一道练习题
请问:
(1)、(2)两个地方分别输出多少?
(3)、(4)两个地方分别输出多少?
(1)、(2)40 4\\8
(3)、(4)10 4\\8
注:32位环境下(2)(4)为4, 64位环境下为8
6.3 ~
6.4 ++ --
前置++
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h>int main() {int a = 3;int b = ++a;//前置++,先++,后使用//a = a + 1,b = a;printf("%d\\n", a);printf("%d\\n", b);return 0; }
后置++
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h>int main() {int a = 3;int b = a++;//后置++,先使用,后++//b = a, a = a + 1;printf("%d\\n", a);printf("%d\\n", b);return 0; }
-- 同理
6.5 *
* 是解引用操作符 ,它通常用于指针变量的操作中,用于获取指针所指向的变量的值。
6.6 ( )
强制类型转换可以将一个表达式的值转换为指定的类型。强制类型转换使用圆括号和所需类型的名称来指定。例如,
(float) 5
将整数5转换为浮点数类型。
7. 关系操作符
//err if("abc" == "abcdef")//这样写是在比较2个字符串首字符的地址 {}
两个字符串比较i应该用库函数 strcmp
8. 逻辑操作符
逻辑与和逻辑或只关注真假。
一道360笔试题
#include <stdio.h>int main() {int i = 0, a = 0, b = 2, c = 3, d = 4;i = a++ && ++b && d++;//i= a++||++b||d++;printf("a= %d\\nb = %d\\nc = %d\\nd = %d\\n", a, b, c, d);return 0; } //程序输出的结果是什么?
int main() {int i = 0, a = 1, b = 2, c = 3, d = 4;//i = a++ && ++b && d++;i= a++||++b||d++;printf("a= %d\\nb = %d\\nc = %d\\nd = %d\\n", a, b, c, d);return 0; }
&& 左边为假,右边就不计算了
|| 左边为真,右边就不计算了
9. 条件操作符(三目操作符)
表达式1?表达式2:表达式3;
它的含义是,如果expression1的值为真(非零),则整个表达式的值为expression2的值,否则为expression3的值。
1. if (a > 5)b =3; elseb =-3; 转换成条件表达式,是什么样?2.使用条件表达式实现找两个数中较大值。
10. 逗号表达式
int x = 1, y = 2, z; z = (++x, ++y);
在上面的代码中,逗号运算符的左边是
++x
,其将x的值增加1并返回结果(即2)。逗号运算符的右边是++y
,其将y的值增加1并返回结果(即3)。最终的结果是3,这个结果被赋值给变量z。逗号表达式的优先级是最低的,因此在使用逗号表达式时应该使用括号来明确表达式的执行顺序。
11. 下标引用、函数调用和结构成员
11.1 [ ]下标引用操作符
操作数:一个数组名+一个索引值
int arr[10] = {0};//创建数组 arr[9] = 10;//实用下标引用操作符。 [ ]的两个操作数是arr和9。
int main() { int arr[10] = { 0 }; //arr[7] --> *(arr+7) --> (7+arr) -->7[arr] //arr是数组首元素的地址 //arr+7就是跳过7个元素,指向了第8个元素 //*arr+7就是第8个元素arr[7] = 8; 7[arr] = 8;//印证了[]是操作符return 0; }
11.2 ( )函数调用操作符
接收一个或多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
int Add(int x, int y) { return x = y; } int main() {int a = 0; int b = 20; //函数调用 int c = Add(a, b);//() 就是函数调用操作符return 0; }
11.3 结构成员访问操作符
. 结构体对象.成员名
-> 结构体指针->成员名 ‘
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h>struct Stu {char name[20];int age;double score; };void set_Stu(struct Stu ss) {strcpy(ss.name, "zhangsan");ss.age = 20;ss.score = 100.0;}void print_Stu(struct Stu ss) {printf("%s %d %lf\\n", ss.name, ss.age, ss.score); }int main() {struct Stu s = { 0 };set_Stu(s);print_Stu(s);return 0; }
这段代码的问题在于,
set_Stu
函数的参数是按值传递的,因此在函数内部对结构体的修改不会影响调用函数的主函数中的结构体。因此,当在
set_Stu
函数中修改结构体的值后,
print_Stu
函数中输出的结果仍然是初始值。要解决这个问题,可以将
set_Stu
函数的参数改为指向结构体的指针,这样就可以在函数内部修改结构体的值,并且这些修改也会反映在调用函数的主函数中的结构体上。修改后的代
码如下:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h>struct Stu {char name[20];int age;double score; };void set_Stu(struct Stu* ps) {strcpy((*ps).name, "zhangsan");(*ps).age = 20;(*ps).score = 100.0;}void print_Stu(struct Stu ss) {printf("%s %d %lf\\n", ss.name, ss.age, ss.score); }int main() {struct Stu s = { 0 };set_Stu(&s);print_Stu(s);return 0; }
当然,也可以写成下面这种形式:
ps->age等价于(*ps).age
如果把两个函数整合到一起,就节省了拷贝一份实参的空间。
12. 表达式求值
表达式求值的一部分是由操作符的优先级和结合性决定。
同样,有些表达式的操作数在求值的过程中可能需要转换成其他类型。
12.1 隐式类型转换
C语言的整型算数运算总是至少以缺省(默认)整形的类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用前被转换为普通整型,这种转换
称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。
所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
char a,b,c; ... a= b + c;
b和c的值被提升为普通整型,然后再执行加法运算。
加法运算完成之后,结果将被截断,然后存储于a中。
如何来进行整型提升呢?
整型提升是按照变量的数据类型的符号位来提升的。
int main() { char a = 5; //00000000000000000000000000000101 //00000101 - a char b = 126; //00000000000000000000000001111110 //01111110 - bchar c = a + b; //00000000000000000000000000000101 - a //00000000000000000000000001111110 - b //00000000000000000000000010000011 //10000011 - c //11111111111111111111111110000011 - 补码 //11111111111111111111111110000010 //10000000000000000000000001111101 // printf("%d", c);return 0; }
int main() { char a = 0xb6; short b = 0xb600; int c = 0xb6000000;if (a == 0xb6) printf("a"); if (b == 0xb600) printf("b"); // a 和 b 整型提升,值发生改变,所以不打印 //放到表达式中的 char 和 short ,在使用时就会发生整型提升 if (c == 0xb6000000) printf("c"); // c 不会发生整型提升 return 0; }//a,b整形提升之后,值发生了改变,所以表达式 a==0xb6 , b==0xb600 的结果是假//但是c不发生整形提升,则表 达式 c==0xb6000000 的结果是真.
int main() {char c = 1;printf("%u\\n", sizeof(c));//1printf("%u\\n", sizeof(+c));//4printf("%u\\n", sizeof(-c));//4 // c 只要参与表达式运算,就会发生整型提升return 0; }
12.2 算数转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算数转换。
寻常算术转换会将操作数转换为最宽的类型,并保留所有操作数中最大的精度和范围。
警告:
但是算数转换要合理,要不然就会有潜在的问题。
float f= 3.14; int num = f;//隐式转换,会有精度丢失
12.3 操作符的属性
复杂表达式的求值有三个影响的因素:
1. 操作符的优先级
2. 操作符的结合性
3. 是否控制求值顺序
两个相邻的操作符先执行哪个?取决于它们的优先级。
如果两者的优先级相同,取决于它们的结合性。
下面附上操作符的优先级和结合性的表格:
操作 符 |
描述 |
用法示例 |
结果类 型 |
结合 性 |
是否控制求值 顺序 |
() |
聚组 |
(表达式) |
与表达 式同 |
N/A |
否 |
() |
函数调用 |
rexp(rexp,...,rexp) |
rexp |
L-R |
否 |
[ ] |
下标引用 |
rexp[rexp] |
lexp |
L-R |
否 |
. |
访问结构成员 |
lexp.member_name |
lexp |
L-R |
否 |
-> |
访问结构指针成员 |
rexp->member_name |
lexp |
L-R |
否 |
++ |
后缀自增 |
lexp ++ |
rexp |
L-R |
否 |
-- |
后缀自减 |
lexp -- |
rexp |
L-R |
否 |
! |
逻辑反 |
! rexp |
rexp |
R-L |
否 |
~ |
按位取反 |
~ rexp |
rexp |
R-L |
否 |
+ |
单目,表示正值 |
+ rexp |
rexp |
R-L |
否 |
- |
单目,表示负值 |
rexp |
rexp |
R-L |
否 |
++ |
前缀自增 |
++ lexp |
rexp |
R-L |
否 |
-- |
前缀自减 |
-- lexp |
rexp |
R-L |
否 |
* |
间接访问 |
* rexp |
lexp |
R-L |
否 |
& |
取地址 |
& lexp |
rexp |
R-L |
否 |
sizeof |
取其长度,以字节 表示 |
sizeof rexp sizeof(类 型) |
rexp |
R-L |
否 |
(类 型) |
类型转换 |
(类型) rexp |
rexp |
R-L |
否 |
* |
乘法 |
rexp * rexp |
rexp |
L-R |
否 |
/ |
除法 |
rexp / rexp |
rexp |
L-R |
否 |
% |
整数取余 |
rexp % rexp |
rexp |
L-R |
否 |
+ |
加法 |
rexp + rexp |
rexp |
L-R |
否 |
- |
减法 |
rexp - rexp |
rexp |
L-R |
否 |
<< |
左移位 |
rexp |
rexp |
L-R |
否 |
>> |
右移位 |
rexp >> rexp |
rexp |
L-R |
否 |
> |
大于 |
rexp > rexp |
rexp |
L-R |
否 |
>= |
大于等于 |
rexp >= rexp |
rexp |
L-R |
否 |
< |
小于 |
rexp < rexp |
rexp |
L-R |
否 |
<= |
小于等于 |
rexp |
rexp |
L-R |
否 |
操作 符 |
描述 |
用法示例 |
结果类 型 |
结合 性 |
是否控制求值 顺序 |
== |
等于 |
rexp == rexp |
rexp |
L-R |
否 |
!= |
不等于 |
rexp != rexp |
rexp |
L-R |
否 |
& |
位与 |
rexp & rexp |
rexp |
L-R |
否 |
^ |
位异或 |
rexp ^ rexp |
rexp |
L-R |
否 |
| |
位或 |
rexp | rexp |
rexp |
L-R |
否 |
&& |
逻辑与 |
rexp && rexp |
rexp |
L-R |
是 |
|| |
逻辑或 |
rexp || rexp |
rexp |
L-R |
是 |
? : |
条件操作符 |
rexp ? rexp : rexp |
rexp |
N/A |
是 |
= |
赋值 |
lexp = rexp |
rexp |
R-L |
否 |
+= |
以...加 |
lexp += rexp |
rexp |
R-L |
否 |
-= |
以...减 |
lexp -= rexp |
rexp |
R-L |
否 |
*= |
以...乘 |
lexp *= rexp |
rexp |
R-L |
否 |
/= |
以...除 |
lexp /= rexp |
rexp |
R-L |
否 |
%= |
以...取模 |
lexp %= rexp |
rexp |
R-L |
否 |
<<= |
以...左移 |
lexp |
rexp |
R-L |
否 |
>>= |
以...右移 |
lexp >>= rexp |
rexp |
R-L |
否 |
&= |
以...与 |
lexp &= rexp |
rexp |
R-L |
否 |
^= |
以...异或 |
lexp ^= rexp |
rexp |
R-L |
否 |
|= |
以...或 |
lexp |= rexp |
rexp |
R-L |
否 |
, |
逗号 |
rexp,rexp |
rexp |
L-R |
是 |
我最近在读一本经典的书:《C陷阱与缺陷》,我针对里面对优先级的总结画了一张图。
![]()
图中从上到下,从左到右优先级逐层递减。
接下来看一些问题表达式:
总结:
我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那么这个表达式就是存在问题的。