补码编码、有符号无符号转化、字符扩展与截断
1. 无符号数的编码
无符号数的编码其实就是我们平时认知的二进制表示形式,比如
其一般定义为:
对于向量
其转化为无符号变量后的值为
就是基本的数学进制转化表示方法,比较简单。
2. 补码编码
2.1补码(two's complement)
我们不仅仅需要表示无符号的值,有些时候我们也要表示有符号的值,而计算机最常见的表示有符号数的方式为补码形式,补码的表示方式中,把最高位解释成负权,在第1节无符号数的编码时,可以看出向量的各位都是按照正权来解释的。
那么我们下面给出补码的定义:
对向量
其表示的有符号值为:
可以看出除了最高位是按照负权解释外,其余和无符号数的编码相同。
- 对于一个字节的有符号数补码 0xFF = -1;
那么计算机为什么使用补码呢?下面介绍下源码和反码
2.2 原码和反码
原码(Sign-Magnitude):最高位是符号位,用来确定剩下的位是使用正权还是负权,其表示的值为:
- 对于一个字节的有符号数补码 0xFF = -127;
反码(Ones' Complement):除最高有效位的权是 而不是外,其他各位的解释方式和补码是一样的。
从公式可以看出,相同的位向量 使用反码解释得到的值-1就得到了对应的使用补码解释得到的值。
从公式同样可以看出,当最高位是0时,反码和原码解释得到的值是一样的,
- 0b0000 0000 ---> 原码表示0,反码也是表示0.
但当最高位是1时,
- 0b1000 0000 ---> 原码表示0,反码表示-127
也就是说当最高位时1时,补码解释0b1000 0000 == 原码解释 0b1111 1111 (-127),此时可以看出原码解释到反码解释的转化,只需把位图 最高位保持不变,其余各位取反。
细心的朋友可能会发现,对于0b1000 0000 用原码解释得到值0,对于0b0000 0000用原码解释得到的值也是0,这就是原码表示的缺点,就是对于零有两个位图 来表示。
那么反码是否也有这个问题呢,答案是肯定的0b1111 1111 和0b0000 0000 在反码解释中都是0.
2.3 补码和反码的来源
补码的名字为two's complement,反码的名字为ones' complement. 注意这两名字的撇号的位置是不一样的。这归结与补码和反码的来源。
术语补码来源于这样一种情况,即对于非负数x,我们用 计算-x的w位表示。
术语反码来源于这样一种情况,即对于非负数x, 我们用[1111..1]-x来表示-x。
可见看原英文名更为贴切。
其实对于正数,用原码、反码或补码编码,其最终位向量 是一样的。只有负数时,才有区别。而0是原码和反码都有两种表示方式,而补码对所有数都只有一种表示方式。
3.有符号无符号转化
注意一下所讲的有符号全部是用补码表示的。
有符号与无符号的相互转化其实也比较简单,就是位图 保持不变,变化的只是对位图 的解释。当用符号时,就用补码解释,当为无符号时,就用无符号数的编码来解释。虽然位图 是不变的,但是解释出来的值却存在某种关系(此关系不是很重要,自己推一下就行了,本质还是位图 不变)。
3.1 无符号-->有符号
对满足 的 有:
举例(以一个字节为例):
0b1111 1111 当是无符号表示时,它的值为255, 当是有符号值表示时,它的值是-1. ()
推导过程如下:
一个位图 用无符号表示时的值:
一个位图 用有符号表示时的值:
用得到差值为 ,
- 当为0时,差值为0,也就是当最高位为0时,无符号表示的值和有符号补码表示的值相等。
- 当 为1时,差值为 ,所以,有符号值(2) = 无符号值(1) -
3.2 有符号-->无符号
对满足 的 有:
举例(以一个字节为例):
0b1111 1111 当是有符号表示时,它的值为-1, 当是无符号值表示时,它的值是255. ()
推导过程与3.1中类似。
有符号和无符号转化时的坑:
当一个运算数是有符号的,另一个是无符号的,那么c语言会隐式的把有符号的数转为无符号的数,再进行计算,也就是说变成了两个非负数的运算。当是算术运算时,是没有影响的。但是对于逻辑运算就存在很大的坑。
printf("%d\n", -1 > 2U); // 1
printf("%d\n", -1 > 0U); // 1
输出的结果都是true,这显然不满足我们的预期。
4.字符扩展
所谓字符扩展就是,比如当一个1字节的数,转化为2字节的数时,在高位补充数据,那么高位应该填补0还是1呢?
我们先说结论:
- 无符号数,0扩展,即高位补0
- 有符号数,符号位扩展,即高位补扩展前高位的值。
我们举个例子:
#include<stdio.h>
void printByte(void* c, int n){
for(int i = 0; i < n; ++i){
printf("%x ", *((unsigned char*)c+i));
}
printf("\n");
}
int main(){
unsigned char a = 0x12; //0b00010010
short b = (unsigned short)a;
printf("%u\n", b); // 18, 16+2 = 18
printByte(&b, sizeof(b)); // 12 00, (小端)也即使0x0012
signed char a1 = 0x87; // 0b10000111
short b1 = (short)a1;
printf("%d\n", b1); // -121
printByte(&b1, sizeof(b1)); // 87 ff, (小端)也即使0xff87
//注意,当a1位unsigned char时,情况就发生了变化。
unsigned char a2 = 0x87; // 0b10000111
short b2 = (short)a2; // 在此时转化,推测是unsigned char ->unsigned short -> short
printf("%d\n", b2); // 135
printByte(&b2, sizeof(b2)); // 87 00, (小端)也即使0xff87
return 0;
}
5.字符截断
与字符扩展相反,字符截断是,比如当一个2字节的数,转化为1字节的数时,应截断去掉高位数据,那么仅仅去掉高位的数据就行了么,不需要额外的其他操作了吗? 答案是肯定的,只需要把高位去掉,保留低位就行。
- 对于无符号数据,这是显然的。
- 对于有符号数据,同样如此。
int main(){
unsigned short a = 0x1288; //0b0001 0010 1000 1000
unsigned char b = (unsigned char)a;
printf("%u\n", b); // 136, 16+2 = 18
printByte(&b, sizeof(b)); // 88, 也即使0x88
short a1 = 0x1288; //0b0001 0010 1000 1000
char b1 = (char)a1;
printf("%d\n", b1); // -120
printByte(&b1, sizeof(b1)); // 88, 也即使0x88
unsigned short a2 = 0x1288; //0b0001 0010 1000 1000
char b2 = (char)a2;
printf("%d\n", b2); // -120
printByte(&b2, sizeof(b2)); // 88, 也即使0x88
return 0;
}
可以看出,字符截断,其实就是直接了当的,把高位的数据扔掉,只保留低位的数据就行了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具