公司动态
C语言:底层程序设计
文章目录前言本文旨在系统性地介绍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 为例二进制位1101权重23222120权重值8421求值1 * 81 * 40 * 21 * 11 * 8 1 * 4 0 * 2 1 * 1 131.2、十进制转二进制短除法方法不断除以 2记录余数从下往上读取。以十进制 125 为例被除数除数商余数125÷ 262162÷ 231031÷ 215115÷ 2717÷ 2313÷ 2111÷ 201结果从下往上读取余数11111011.3、 二进制转八进制8进制的数字每⼀位是 0~7。核心规则从右向左每 3 位二进制数对应 1 位八进制数因为 238 剩余不够3个2进制位的直接换算。以二进制 01101011为例二进制序列0 11 0 10 1 1对应八进制1531.4、二进制转十六进制16进制的数字每⼀位是0~ 9, A~F。核心规则从右向左每 4 位二进制数对应 1 位十六进制数(因为 2416)剩余不够4个⼆进制位的直接换算。以二进制 01101011为例二进制序列0 1 1 01 0 1 1对应十六进制6B二、原码、反码、补码整数的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 2E指数位IEEE 754 浮点数存储结构类型总位数符号位 S指数位 E有效数字 Mfloat32位321823double64位64111523.1、浮点数存的过程①有效数字 M 的存储规则由于 1≤M2M 的整数部分始终为 1因此只存储小数部分读取时自动补回 1。节省 1 位有效数字float 实际精度提升到 24 位有效数字。②指数E 的存储规则E 是无符号整数但科学计数法中的指数可以为负数。因此 IEEE 754 规定存储时 E 的真实值必须加上一个中间数偏移量。float类型E位数8取值范围0 ~ 255偏移量127double类型E位数11取值范围0 ~ 2047偏移量1023。浮点数可能无法精确保存如 1.2 在二进制中无限循环无法精确保存因此存在误差。3.2、浮点数取的过程①E 不全为 0 且不全为 1规格化数规则指数真实值 E 存储值 - 偏移量float: 127double: 1023有效数字 M 1.xxxxxx整数部分的 1 自动补回示例0.5 在 float 中的存储0.5 → 二进制0.1 → 规格化1.0 × 2⁻¹S 0正数E 存储值 -1 127 126 01111110M 0小数部分补全 23 位存储0 01111110 00000000000000000000000②E 全为 0非规格化数规则指数真实值 1 - 127float或 1 - 1023double有效数字 M 0.xxxxxx不再补回整数部分的 1用途表示 ±0 和非常接近 0 的极小数字。示例存储0 00000000 00100000000000000000000S 0正数E 0全为 0M 00100000000000000000000表示0.001 × 2⁻¹²⁶一个极小的正数③ E 全为 1特殊值规则如果 M 全为 0 → 表示 ±∞正负由 S 决定如果 M 不为 0 → 表示 NaNNot a Number非法数值示例1 11111111 00000000000000000000000 → -∞0 11111111 00010000000000000000000 → NaNM 不全为 0float 取值范围图-∞-3.403×10³⁸-1.175×10⁻³⁸-001.175×10⁻³⁸1.175×10³⁸∞3.3、比较浮点数相等浮点数不能直接用 比较因为存储有精度误差。正确做法是使用 fabs() 判断差值是否小于一个允许的误差范围如 1e-9。代码示例#includestdio.h#includemath.h// fabs() 函数intmain(){doublea0.1;doubleb0.2;doublec0.3;doubleeps1e-9;// 误差范围容差if(fabs((ab)-c)eps){printf(\n);// 输出 }else{printf(!\n);}return0;}3.4、浮点数与整数存储对比代码示例#includestdio.hintmain(){// 整数 9 的存储// 内存中32位00000000 00000000 00000000 00001001// 按 int 解读 9intn9;// 用 float 指针指向 n 的内存// 将同一块内存按 float 方式解读float*pFloat(float*)n;// 按 int 方式打印 → 9printf(n 的值为%d\n,n);// 9// 按 float 方式解读整数 9 的内存// 9 的二进制00000000 00000000 00000000 00001001// 按 float 解读S0E00000000全0M...1001// E全0 → 非规格化数 → 极小值 → 0.000000printf(*pFloat 的值为%f\n,*pFloat);// 0.000000// 将浮点数 9.0 写入 n 的内存// 9.0 1.001 × 2³// S0E312713010000010M00100000000000000000000// 内存0 10000010 00100000000000000000000// 按 int 解读1091567616*pFloat9.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 → 大端。代码示例#includestdio.hintcheck_sys(){inti1;// 取 i 的低地址强转为 char*读取1个字节return*(char*)i;}intmain(){intretcheck_sys();if(ret1)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任意未定义行为未定义代码示例#includestdio.hintmain(){intnum10;// 0000 1010intnnum1;// 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任意实现定义通常是算术右移高位补符号位实现定义代码示例#includestdio.hintmain(){// 无符号数逻辑右移unsignedintnum10;// 0000 1010unsignedintnnum1;// 0000 0101 5printf(n %u\n,n);// 5printf(num %u\n,num);// 10return0;}对有符号数进行移位操作时先将其转换为无符号数再移位即可获得标准定义的确定行为避免“实现定义”或“未定义行为”。代码示例#includestdio.hintmain(){intx-8;// 有符号数unsignedintux(unsignedint)x;// 转换为无符号数ux1;// 确定的逻辑右移高位补0printf(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变11变0代码示例#includestdio.hintmain(){inta5;// 二进制补码32位0000 0000 0000 0000 0000 0000 0000 0101intb3;// 二进制补码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 1intandab;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 7intora|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 6intxora^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;}