文章目录
- 前言
- 本文旨在系统性地介绍C语言中底层程序设计。
- 一、二进制和进制转换
- 1.1、二进制转十进制(权重法)
- 1.2、十进制转二进制(短除法)
- 1.3、 二进制转八进制
- 1.4、二进制转十六进制
- 二、原码、反码、补码
- 2.1、有符号整数
- 2.2、无符号整数
- 三、浮点数的存储
- 3.1、浮点数存的过程
- 3.2、浮点数取的过程
- 3.3、比较浮点数相等
- 3.4、浮点数与整数存储对比
- 四、大小端字节序
- 4.1、小端字节序(存储)模式
- 4.2、大端字节序(存储)模式
- 4.3、网络字节序
- 4.4、大小端的判断
- 五、移位操作符
- 5.1、左移操作符
- 5.2、右移操作符
- 六、位操作符:&、|、^、~
前言
本文旨在系统性地介绍C语言中底层程序设计。
一、二进制和进制转换
二进制、八进制、十进制、十六进制只是同一个数值的不同书写方式。
C语言中:八进制以 0 开头(如 017),十六进制以 0x 开头(如 0xF),二进制的 0b 前缀是 C23 新增特性。
1.1、二进制转十进制(权重法)
数字的每一位都有一个“权重”,它决定这一位在数值中所占的分量,该位的值 = 该位数字 × 该位权重。 权重 = 进制 ^ 位数(从右向左从 0 开始)。
以二进制 1101 为例:
| 二进制位 | 1 | 1 | 0 | 1 |
|---|---|---|---|---|
| 权重 | 23 | 22 | 21 | 20 |
| 权重值 | 8 | 4 | 2 | 1 |
| 求值 | 1 * 8 | 1 * 4 | 0 * 2 | 1 * 1 |
1 * 8 + 1 * 4 + 0 * 2 + 1 * 1 = 13
1.2、十进制转二进制(短除法)
方法:不断除以 2,记录余数,从下往上读取。
以十进制 125 为例:
| 被除数 | 除数 | 商 | 余数 |
|---|---|---|---|
| 125 | ÷ 2 | 62 | 1 |
| 62 | ÷ 2 | 31 | 0 |
| 31 | ÷ 2 | 15 | 1 |
| 15 | ÷ 2 | 7 | 1 |
| 7 | ÷ 2 | 3 | 1 |
| 3 | ÷ 2 | 1 | 1 |
| 1 | ÷ 2 | 0 | 1 |
结果:从下往上读取余数:1111101
1.3、 二进制转八进制
8进制的数字每⼀位是 0~7。
核心规则:从右向左,每 3 位二进制数对应 1 位八进制数(因为 23=8), 剩余不够3个2进制位的直接换算。
以二进制 01101011为例:
| 二进制序列 | 0 1 | 1 0 1 | 0 1 1 |
|---|---|---|---|
| 对应八进制 | 1 | 5 | 3 |
1.4、二进制转十六进制
16进制的数字每⼀位是0~ 9, A~F。
核心规则:从右向左,每 4 位二进制数对应 1 位十六进制数(因为 24=16),剩余不够4个⼆进制位的直接换算。
以二进制 01101011为例:
| 二进制序列 | 0 1 1 0 | 1 0 1 1 |
|---|---|---|
| 对应十六进制 | 6 | B |
二、原码、反码、补码
整数的2进制表示方法有三种,原码、反码、补码。
整数在内存中统一使用补码表示和存储。
原因说明
①符号位统一处理 :符号位和数值位一起运算,无需单独判断正负
②加减法统一 :CPU只有加法器,减法可通过补码转为加法
③硬件简化 :原码↔补码的转换规则相同,无需额外电路
2.1、有符号整数
有符号整数的三种表示方法均有符号位和数值位两部分,2进制序列中,最高位的1位是被当做符号位,剩余的都是数值位。符号位都是用0表示“正”,用1表示“负”。
正整数的原、反、补码都相同。
负整数的三种表示方法各不相同,需要进行计算(方法如下)
原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:反码+1就得到补码。
补码算出原码,有2种方法:①原路返回:补码-1,再取反,得到原码;②补码取反,+1,也能得到原码。所以原码->补码,补码->原码 的计算方法是可以统⼀的。
2.2、无符号整数
无符号整数的三种 2 进制表示相同,没有符号位,每⼀位都是数值位。
三、浮点数的存储
浮点数类型包括 float、double、long double,精度和范围依次增大。浮点数在 <float.h> 中定义了取值范围,运算时注意精度误差。
根据国际标准IEEE(电气和电子工程协会) 754,任意⼀个二进制浮点数V的表示形式:
V = (−1)S∗ M ∗ 2E
| 部分 | 含义 |
|---|---|
| S | 符号位:0 为正,1 为负 |
| M | 有效数字(1 ≤ M < 2) |
| E | 指数位 |
IEEE 754 浮点数存储结构
| 类型 | 总位数 | 符号位 S | 指数位 E | 有效数字 M |
|---|---|---|---|---|
| float(32位) | 32 | 1 | 8 | 23 |
| double(64位) | 64 | 1 | 11 | 52 |
3.1、浮点数存的过程
①有效数字 M 的存储规则:由于 1≤M<2,M 的整数部分始终为 1,因此只存储小数部分,读取时自动补回 1。节省 1 位有效数字,float 实际精度提升到 24 位有效数字。
②指数E 的存储规则:E 是无符号整数,但科学计数法中的指数可以为负数。因此 IEEE 754 规定:存储时 E 的真实值必须加上一个中间数(偏移量)。float类型,E位数8,取值范围0 ~ 255,偏移量127;double类型,E位数11,取值范围0 ~ 2047,偏移量1023。
浮点数可能无法精确保存,如 1.2 在二进制中无限循环,无法精确保存,因此存在误差。
3.2、浮点数取的过程
①E 不全为 0 且不全为 1(规格化数)
规则:
指数真实值 = E 存储值 - 偏移量(float: 127,double: 1023)
有效数字 M = 1.xxxxxx(整数部分的 1 自动补回)
示例:
0.5 在 float 中的存储
0.5 → 二进制:0.1 → 规格化:1.0 × 2⁻¹
S = 0(正数)
E 存储值 = -1 + 127 = 126 = 01111110
M = 0(小数部分,补全 23 位)
存储:0 01111110 00000000000000000000000
②E 全为 0(非规格化数)
规则:
指数真实值 = 1 - 127(float)或 1 - 1023(double)
有效数字 M = 0.xxxxxx(不再补回整数部分的 1)
用途:表示 ±0 和非常接近 0 的极小数字。
示例:
存储:0 00000000 00100000000000000000000
S = 0(正数)
E = 0(全为 0)
M = 00100000000000000000000
表示:0.001 × 2⁻¹²⁶(一个极小的正数)
③ E 全为 1(特殊值)
规则:
如果 M 全为 0 → 表示 ±∞(正负由 S 决定)
如果 M 不为 0 → 表示 NaN(Not a Number,非法数值)
示例:
1 11111111 00000000000000000000000 → -∞
0 11111111 00010000000000000000000 → NaN(M 不全为 0)
float 取值范围图
3.3、比较浮点数相等
浮点数不能直接用 == 比较,因为存储有精度误差。正确做法是使用 fabs() 判断差值是否小于一个允许的误差范围(如 1e-9)。
代码示例:
#include<stdio.h>#include<math.h>// fabs() 函数intmain(){doublea=0.1;doubleb=0.2;doublec=0.3;doubleeps=1e-9;// 误差范围(容差)if(fabs((a+b)-c)<eps){printf("==\n");// 输出 ==}else{printf("!=\n");}return0;}3.4、浮点数与整数存储对比
代码示例:
#include<stdio.h>intmain(){// 整数 9 的存储// 内存中(32位):00000000 00000000 00000000 00001001// 按 int 解读 = 9intn=9;// 用 float 指针指向 n 的内存// 将同一块内存按 float 方式解读float*pFloat=(float*)&n;// 按 int 方式打印 → 9printf("n 的值为:%d\n",n);// 9// 按 float 方式解读整数 9 的内存// 9 的二进制:00000000 00000000 00000000 00001001// 按 float 解读:S=0,E=00000000(全0),M=...1001// E全0 → 非规格化数 → 极小值 → 0.000000printf("*pFloat 的值为:%f\n",*pFloat);// 0.000000// 将浮点数 9.0 写入 n 的内存// 9.0 = 1.001 × 2³// S=0,E=3+127=130=10000010,M=00100000000000000000000// 内存:0 10000010 00100000000000000000000// 按 int 解读:1091567616*pFloat=9.0;// 按 int 方式打印同一块内存 → 1091567616printf("n 的值为:%d\n",n);// 1091567616// 按 float 方式打印 → 9.000000printf("*pFloat 的值为:%f\n",*pFloat);// 9.000000return0;}四、大小端字节序
4.1、小端字节序(存储)模式
数据的低位字节保存在内存的低地址处,高位字节保存在高地址处。
小端序把最低位字节放在最低地址,让 CPU 做加法时可以直接从低地址开始计算,无需跳转,简化硬件设计,提升计算效率。
4.2、大端字节序(存储)模式
数据的高位字节保存在内存的低地址处,低位字节保存在高地址处。
示例:0x11223344 在内存中的存储
| 地址 | 小端存储 | 大端存储 |
|---|---|---|
| 低地址 | 44(低位) | 11(高位) |
| 高地址 | 11(高位) | 44(低位) |
4.3、网络字节序
为了避免不同计算机之间数据传输时发生混乱,TCP/IP协议族规定大端序为“网络字节序”。当数据在网络上传输时,发送方计算机会将数据从其主机字节序转换成网络字节序(大端),接收⽅再将其从网络字节序转换回自己的主机字节序。
4.4、大小端的判断
关键:&i 获取的是低地址,(char*)&i 读取低地址处的 1 个字节。
如果低地址处是 01 → 小端;如果是 00 → 大端。
代码示例:
#include<stdio.h>intcheck_sys(){inti=1;// 取 i 的低地址,强转为 char*(读取1个字节)return*(char*)&i;}intmain(){intret=check_sys();if(ret==1)printf("小端\n");elseprintf("大端\n");return0;}五、移位操作符
移位操作符只能对整数进行操作,对二进制位(补码)进行左移或右移。
| 操作符 | 名称 | 语法 | 说明 |
|---|---|---|---|
| << | 左移 | a << b | 将 a 的二进制位(补码)向左移动 b 位 |
| >> | 右移 | a >> b | 将 a 的二进制位(补码)向右移动 b 位 |
约束条件:①b >= 0 移动位数不能为负数;②b < 类型宽度 移动位数不能大于等于类型宽度(如 int 为 32 位)。
注:违反约束 未定义行为。
5.1、左移操作符
移位规则:左边丢弃,右边补 0
| 左操作数类型 | 条件 | 行为 | 标准定义 |
|---|---|---|---|
| unsigned | 任意 | 低位补 0,结果 ≡ a × 2b(mod (a的类型最大值 + 1)) | 完全定义 |
| signed(≥ 0) | 不溢出 | 结果为 a × 2b | 完全定义 |
| signed(≥ 0) | 溢出 | 未定义行为 | 未定义 |
| signed(< 0) | 任意 | 未定义行为 | 未定义 |
代码示例:
#include<stdio.h>intmain(){intnum=10;// 0000 1010intn=num<<1;// 0001 0100 = 20printf("n = %d\n",n);// 20printf("num = %d\n",num);// 10(原值不变)return0;}5.2、右移操作符
移位规则:①逻辑右移:左边补 0,右边丢弃;②算术右移:左边补符号位(正数补 0,负数补 1),右边丢弃。
| 左操作数类型 | 条件 | 行为 | 标准定义 |
|---|---|---|---|
| unsigned | 任意 | 逻辑右移(高位补 0),结果 = floor(a / 2ᵇ) | 完全定义 |
| signed(≥ 0) | 任意 | 算术右移(高位补 0),结果 = floor(a / 2ᵇ) | 完全定义 |
| signed(< 0) | 任意 | 实现定义(通常是算术右移,高位补符号位) | 实现定义 |
代码示例:
#include<stdio.h>intmain(){// 无符号数:逻辑右移unsignedintnum=10;// 0000 1010unsignedintn=num>>1;// 0000 0101 = 5printf("n = %u\n",n);// 5printf("num = %u\n",num);// 10return0;}对有符号数进行移位操作时,先将其转换为无符号数,再移位,即可获得标准定义的确定行为,避免“实现定义”或“未定义行为”。
代码示例:
#include<stdio.h>intmain(){intx=-8;// 有符号数unsignedintux=(unsignedint)x;// 转换为无符号数ux>>=1;// 确定的逻辑右移(高位补0)printf("x = %d\n",x);// -8printf("ux = %u\n",ux);// 2147483644(逻辑右移结果)return0;}六、位操作符:&、|、^、~
位操作符对整数的二进制位(补码)进行逐位运算,操作数必须是整数。
| 操作符 | 名称 | 形式 | 规则 |
|---|---|---|---|
| & | 按位与 | a & b | 对应位:有0则0,全1才1 |
| | | 按位或 | a | b | 对应位:有1则1,全0才0 |
| ^ | 按位异或 | a ^ b | 对应位:相同为0,相异为1 |
| ~ | 按位取反 | ~a | 所有位取反:0变1,1变0 |
代码示例:
#include<stdio.h>intmain(){inta=5;// 二进制补码(32位):0000 0000 0000 0000 0000 0000 0000 0101intb=3;// 二进制补码(32位):0000 0000 0000 0000 0000 0000 0000 0011// 1. 按位与 &// 0000 0000 0000 0000 0000 0000 0000 0101 (a)// 0000 0000 0000 0000 0000 0000 0000 0011 (b)// -----------------------------------------// 0000 0000 0000 0000 0000 0000 0000 0001 = 1intand=a&b;printf("5 & 3 = %d\n",and);// 1// 2. 按位或 |// 0000 0000 0000 0000 0000 0000 0000 0101 (a)// 0000 0000 0000 0000 0000 0000 0000 0011 (b)// -----------------------------------------// 0000 0000 0000 0000 0000 0000 0000 0111 = 7intor=a|b;printf("5 | 3 = %d\n",or);// 7// 3. 按位异或 ^// 0000 0000 0000 0000 0000 0000 0000 0101 (a)// 0000 0000 0000 0000 0000 0000 0000 0011 (b)// -----------------------------------------// 0000 0000 0000 0000 0000 0000 0000 0110 = 6intxor=a^b;printf("5 ^ 3 = %d\n",xor);// 6// 4. 按位取反 ~// ~a = 1111 1111 1111 1111 1111 1111 1111 1010 (补码)// 符号位为1 → 负数// 补码转原码:取反 +1// 原码:1000 0000 0000 0000 0000 0000 0000 0110 = -6intnot=~a;printf("~5 = %d\n",not);// -6return0;}