【C语言操作符终极指南】万字总结:从二进制到表达式求值,全方位解析+避坑指南
前言
C语言操作符
是程序设计的基石,深刻影响代码的效率与正确性。本文系统梳理从二进制基础到表达式求值的核心知识,深入解析移位、位运算等高级技巧,帮助开发者夯实基础、避开常见陷阱。
文章目录
- 前言
- 一、额外知识补充
- 1.1 二进制和进制转换
- 1.1.1 二进制转十进制
- 1.1.2 十进制转二进制
- 1.1.3 二进制转八进制
- 1.1.4 二进制转十六进制(互逆)
- 1.2 反码、补码、原码
- 二、移位操作符
- 2.1 左移动操作符
- 2.2 右移操作符
- 2.3赋值
- 三、位操作符
- 3.1 按位与、按位或
- 3.2 按(二进制)位异或
- 3.3 按位取反
- 3.4 应用场景
- 3.5 二进制改0或1小程序
- 四、常见的操作符
- 4.1 逗号表达式的妙用
- 4.2 下标引用操作符
- 4.3 函数调用操作符
- 五、成员访问操作符
- 5.1 补充知识:结构体
- 5.2 点操作符(直接访问)
- 5.3 箭头操作符(间接访问)
- 六、操作符的属性
- 七、表达式求值
- 7.1 整型提升
- 7.2 算数转化
- 八、问题表达式解析
- 8.1 `a*b+c*d+e*f`
- 8.2 `c+--c`
- 8.3 函数调用表达式
- 8.4 赋值
- 8.5 总结
- 结语
一、额外知识补充
1.1 二进制和进制转换
其实2进制、8进制、10进制、16进制是数值的不同表示形式而已。
每一位在进制中的权重,相加就是数字(所在进制中)本身,在16进制中不好区分10是0还是1,于是用A/a表示10
1.1.1 二进制转十进制
这种方法适用于任何进制转十进制的情况
1.1.2 十进制转二进制
算法1:
算法2:(以12为例,根据权重去凑12,适用于数字较小的情况)
1.1.3 二进制转八进制
互逆:每一个八进制位还原成3个二进制位
1.1.4 二进制转十六进制(互逆)
与二进制转八进制类似
互逆:每一个八进制位还原成4个二进制位
总结:
1.2 反码、补码、原码
只针对整数
整数有三种表示形式:即反码、补码、原码
有符号整数这三种表示形式都有符号位和数值位,最高的二进制序列为符号位,0表示正,1表示负,其中正整数这三种表示形式相同(反码、补码、原码),负整数这三种表示形式各不相同
无符号整数这三种表示形式只有数值位
原码:直接将数值翻译成二进制得到的就是原码
下面以-10为例:(负数需要计算)
反码:符号位不变,其他位按位取反
补码:反码加一
以10(正数为例)三种表示形式都是一样的
为啥要介绍这三种表示形式?
整数在内存的计算存放的是补码,计算机里面运算的也是补码,所以了解才能更好的计算,因为补码可以将符号位和数值位结合起来,并且加法和减法可以统一处理(CPU只能计算加法),但是减法可以转换为加法,补码和原码转换时计算过程是一样的
验证举例:用补码的好处
假设用计算机用的是原码来计算,最高位无论变不变都是错的
当时用的是补码时,能够解决这个问题
为啥补码和反码运算过程是一样的?
补码有两种方式得到原码,如图所示
二、移位操作符
移动的是二进制位,操作对象是整数
2.1 左移动操作符
左边抛弃,右边补零
(移动的次数不能太大,否则没意义,不能是负数,没有定义)
以整数6为例,先算出补码(与原码相同)、往左移,右边补零,算出新的补码(与新的原码相同),但是以%d
打印出来以原码的形式,所以还要转变为原码(在这里正数的原、反、补码相同,不需要算)
以-6为例
从上面看,左移一位有乘二的效果
2.2 右移操作符
分两种,常见的是第二种——算术右移
(移动的次数不能太大,否则没意义,不能是负数,没有定义)
VS用的是算术右移
右移一位不一定有除二的效果,-1右移还是负一
2.3赋值
移位操作符跟+、-、*、/一样不会对本身造成影响,如果想改变可以与赋值一起
三、位操作符
操作的是二级制的位,面对对象是整数
3.1 按位与、按位或
联想:只是面对对象不一样,一个二级制、一个表达式
按位与举例:
按位或举例:
3.2 按(二进制)位异或
计算规则:相同为0,相异为1
3.3 按位取反
所有二进制位取反(包括符号位)
以零为例
3.4 应用场景
交换a和b的值
传统方法:
方法1:(效率高,可读性好)
方法: 会越界
进阶:可以把a^b
当成一把钥匙,遇到a
变成b
,遇到b
变成a
需要知识:
- 与自己按位异或等于零
- 与零按位异或等于本身
#include<stdio.h>
int main()
{int a = 3;int b = -5;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;
}
求一个数在存储在内存中二进制中1的个数(补码中二进制1的个数)
类比十进制(%
10得到最后一位,/
10得到前几位)
传统方法:(用unsigned
更全面,可以把它当作无符号数,可以包括负数)
#include<stdio.h>
int main()
{unsigned int a = 0;scanf("%d", &a);int count = 0;while (a){if (a % 2 == 1){count++;}a /= 2;}printf("%d", count);return 0;
}
进阶:
需要知识:
- 按位与1的结果可以确定最后一位是否为0还是1
- 移位操作符不会改变本身
a>>
时,a不变 - 把内存里的补码当作数组
#include<stdio.h>
int main()
{int n;scanf("%d", &n);int i = 0;int count = 0;for (i = 0; i < 32; i++){if (((n >> i) & 1) == 1){count++;}}printf("%d", count);return 0;
}
高阶:(偏算法)
原因:当最低为0时像前面借一位,前一位就变了,而按位与正好去除掉右边的1
#include<stdio.h>
int main()
{int n;scanf("%d", &n);int count=0;while (n){n = n & (n - 1);count++;}printf("%d", count);return 0;
}
如何判断一个数是否是2的次方
可以判断里面有多少个1,用高阶的话判断count
是否等于1,但是繁琐了
只要有这一个条件就行,不需要算count
3.5 二进制改0或1小程序
原码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void Fuc(int x)
{printf("******请选择: *******\n");printf("***0. 将第%d位改成0***\n", x);printf("***1. 将第%d位改成1***\n", x);
}void menu()
{printf("*****菜 单*****\n");printf("****1. play****\n");printf("****0. exit****\n");
}
int Fuc1(int x,int y)
{return (x & (~(1 << (y - 1))));
}
int Fuc2(int x,int y)
{return (x | (1 << (y - 1)));
}
int main()
{int input;do{menu();printf("请输入>:");scanf("%d", &input);switch (input){case 1:printf("输入要改的数>:");int a = 0;scanf("%d", &a);printf("从右往左,要改二进制的第几位?\n");printf("请输入>:");int n = 0;scanf("%d", &n);int b;do{Fuc(n);printf("请选择>:");scanf("%d", &b);switch (b){case 0:{printf("%d\n", Fuc1(a,n));break;}case 1:{printf("%d\n", Fuc2(a,n));break;}default:printf("请重新输入\n");break;}} while (b != 1 && b != 0);break;case 0:printf("成功退出\n");break;default:printf("请重新输入:\n");break;}} while (input);return 0;
}
四、常见的操作符
4.1 逗号表达式的妙用
逗号表达式可以简化
(这样写代码不容易乱)
4.2 下标引用操作符
操作数:一个函数名+一个索引值(下标)
4.3 函数调用操作符
可以接受一个或多个操作数,第一个是函数名,剩余的是传递给函数的参数
函数调用操作符最少几个操作数?
一个,可以有一个函数名
五、成员访问操作符
5.1 补充知识:结构体
结构体的声明(结构体类型创建)
变量的创建
初始化:.
操作符
注意:
只有在声明变量的同时才能进行初始化,这种初始化方式被称为 “初始化列表”
错误示范:
正确示范:
当结构体里面有结构体,初始化也没有任何问题
5.2 点操作符(直接访问)
操作数是结构体变量和成员名
嵌套体结构(点完再点)
5.3 箭头操作符(间接访问)
操作数是结构体指针和成员名
六、操作符的属性
先看优先级,如果相同,再看结合性
圆括号优先级最高,可以改变其他运算符的优先级,最好使用()
详细间https://en.cppreference.com/w/c/language/operator_precedence.html
七、表达式求值
7.1 整型提升
C语言中整形算术运算总是以缺省整形类型的精度来计算,针对的是大小小于整形的,比如char、short
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种被称为整型提升
原因:
提升过程(深入底层)
- 有符号位的整数提升按照变量的数据类型的符号位来提升
- 无符号提升,最高位补零
7.2 算数转化
针对的是大小大于整形的
八、问题表达式解析
8.1 a*b+c*d+e*f
优先级是相邻的才有,对这个表达式来说,有两种求解路径
解决方法:
- 加括号
- 分行拆开写
8.2 c+--c
左边的c
什么时候准备好是不确定的,是一开始放在寄存器里的存到c
,还是等到--c
运行完了以后在存放到c
里面
解决方法:
- 不要放两个相同的变量
- 分行拆开写
8.3 函数调用表达式
#include <stdio.h>
int fun()
{static int count = 1;return ++count;
}
int main()
{int answer;answer = fun() - fun() * fun();printf("%d", answer);
}
乘法比加法肯定是先算,但是函数什么时候调用不确定,因此也有两种求解路径
8.4 赋值
int main()
{int i = 1;int ret = (++i) + (++i) + (++i);printf("%d\n", ret);printf("%d\n",i);return 0;
}
在VS和gcc上运行不同
原因:
这段代码中的第一个+在执行的时候,第三个++是否执行,这个是不确定的,因为依靠操作符的优先级和结合性是无法决定第一个+和第三个前置++的先后顺序。
8.5 总结
即使有了操作符的优先级和结合性,我们写出的表达式依然有可能不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在潜在风险的,建议不要写出特别复杂的表达式
结语
掌握操作符不仅关乎语法正确,更关系到代码质量与运行效率。希望本文能成为你C语言学习路上的得力助手,在实践中融会贯通,写出更加优雅高效的代码。