引入:
初学C语言时,很多人都会被“原码、反码、补码”劝退一波。更麻烦的是,后面学整型截断、整型提升、%d 与 %u 打印时,又绕回到这些二进制转换上,一不小心就乱。所以单独写一篇博客,那这些问题一次性理清楚,也方便忘记的时候来复习
一:原反补
1:正/负整数的原反补
对于一个整数来说,其存储在内存中的二进制位,一定是补码,而我们printf打印一个数,编译器会自动的把该整数在内存中的补码,转化为我们人为可以看得懂的十进制数字,我们将其理解为编译器将补码转化为了原码,再把此时的原码二进制对应的十进制打印给我们查看。
而不管是正整数,负整数,其在内存中存储的都是补码,但是对于正数来说,原反补相同,而负整数才分别拥有原反补,所以只有负数才需要去计算反码和补码,而正整数来说不需要
Q:正整数原反补相同,那到底是三种都是原码?还是三种都是反码或者补码??如果都是都是原码,那之前说的不管是正整数还是负整数其存储在内存中的都是补码,那正整数的原码作为补码存储在内存中,这不是矛盾了吗?
A:一个整数的补码 =
如果 ≥ 0:就是它自身的二进制表示
如果 < 0: 原码按位取反+1
所以对于正整数来说,自身的二进制表示的原码就是补码!
2:有符号/无符号
那在C语言中怎么区分一个整形是正的还是负的呢?
signed代表有符号,比如signed int,代表有符号整形,而unsigned代表无符号,比如unsigned int代表无符号整形,而在大多数编译器中,int就是signed int的简写。
而有符号整形signed int中的正整数和unsigned int 都是正的,原反补相同
而有符号整形signed int中的负整数,才拥有不同的原反补,才需要去分别计算原反补

注:有符号不代表一定是负整数,其中的正整数的高位符号位为0,所以原反补当然相同
3:原反补之间的转化
所以原反补之间的转化,只能是针对负整数而言的
①:原码到补码
②:补码到原码
二:截断和整形提升
1:截断:
将一个int类型的整数存储到char里面,由于char是一个字节大小,对于四个字节大小的整数,会发生截断,只保留整形补码的最后8位(低8位),因为低8位存储的才是有效的信息
2:整形提升
整形提升意义在于:将存储在char中的8位补充到32位
规则(如何提升):对signed char这种有符号char类型,采用左补24个符号位,根据char中的8个比特位的最高位符号位来判断,如截断后的符号位为1,则左补24个1,反之补0;对unsigned char 这种无符号位的采用直接左补24个0
注:整形提升只有char和 short才会进行,此篇博客重点讲char,其次整形提升是编译器自动完成的,我们不用手动去做,但是我们需要理解其中的原理
Q:那什么时候需要整形提升呢?
A:只要是涉及到计算的时候,必须进行整形提升再进行计算,本质深层原因是因为硬件的性质:
三:%d和%u的区别
%d:以十进制的形式打印有符号整形
%u:以十进制的形式打印无符号整形
所以分别用%d和%u对同一个整数进行打印,结果必然是不同的:
实例:对-1分别使用%d和%u打印
#include <stdio.h>
int main() {
int a = -1;
printf("%d\n", a); // 以有符号十进制打印
printf("%u\n", a); // 以无符号十进制打印
return 0;
}

解释:内存中 -1 的补码(32位):11111111 11111111 11111111 11111111
-
%d:把这一串二进制当作有符号数 → 最高位是1 → 负数 →负数的原反补不同,转换为原码→10000000 00000000 00000000 00000001→ 结果是 -1 -
%u:把这一串二进制当作无符号数 → 当做正整数来看→ 原反补相同→ 当前补码所有位都是数值位 → 结果是 4294967295(即 2³² - 1)
所以之前强调的对于一个整数来说,其存储在内存中的二进制位,一定是补码,这句话至关重要
而%d和%u一般不会直接打印一个整形这么简单,而是去打印一个char,须知,char要被%d和%u打印的时候,一定会发生整形提升!!
所以套路如下:先让char接收一个整形,发生截断,然后再用%d和%u去打印char,此时char会发生整形提升,这一套下来,如果不理解原理,那么结果一定是看不懂的!
所有情况如下:

解释:
①:大多数编译器上的char就是signed char的缩写
②:整形提升的时候,需要去看char 的类型,signed还是unsigned?signed则直接左补24个符号位,unsigned则直接左补24个0
③:当我们整形提升之后,我们就不会再管char的类型是否有无符号了!此时若是%d打印,则代表此时整形提升之后的32个位是以有符号的形式去打印,则观察符号位,为0代表正整数,直接打印即可,为1代表负整数,需要将此时的32位转化为原码再打印;而若是%u打印,代表此时是以无符号形式去打印,直接打印即可!
注:其实计算机只会存储补码,计算也是用的补码,而所谓的原码,反码,都是方便我们人为计算和理解从而创造出来的东西,所以上文中任何一处提到的编译器或者计算机会把xx码转化为xx码都是为了方便理解
四:有/无符号char的范围
1:范围的探究
unsigned char的时候,8个比特位,每个都是用来表示自己的大小,所以范围如下:0~255

signed char的时候,8个比特位,只有低1位是用来表示自己的大小,最高位符号位是用来决定是正数还是负数的,所以范围如下:-128~127

解释:
在下半部分的负数中,除了-128 ,其它的二进制都可以进行补转原去查看值,也就是右面的红色标注值,但是1000 0000不能!此时我们取反+1,得到的原码是0000 0000,因为向高位进1了,1 0000 0000,单char只有8字节,所以此时1舍去了;而设计者规定,1000 0000对应的二进制表示就是-128 ,这是规定!
本质原因:
1000 0000是一个没有合理原码的负数补码,而对于计算机来说,本来就只有补码,它很平常的就把负数补码1000 0000规定为-128,而因为我们人为创造的原码和反码对于这种特殊的负数补码来说,无法转化出同样长度比特位的原码,所以我们才会说这是规定,发现了不兼容,才给它贴上的规定标签。
char的范围是-128~127,这也是为什么下图中的负数最大值的绝对值都要比正数大1,同理
有符号整型范围表(64位系统常见情况)
| 类型 | 字节数 | 最小值 | 最大值 |
|---|---|---|---|
char / signed char | 1 | -128 | 127 |
short | 2 | -32,768 | 32,767 |
int | 4 | -2,147,483,648 | 2,147,483,647 |
long(Linux 64位) | 8 | -9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 |
long(Windows 64位) | 4 | -2,147,483,648 | 2,147,483,647 |
long long | 8 | -9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 |
2:规律的探究
我们知道了
signed char的范围为:-128-127,unsigned char的范围为:0-255
所以规律如下两图:

解释:因为无符号char,所以8位都表示大小,0~255,清晰明了

解释:
0~127的时候,都是没进位到符号位的时候,当试图128的时候,此时int 类型的128的补码
如下:
00000000 00000000 00000000 10000000
此时我们存储的是低8位,1000 0000,但是由于我们是signed char,所以高位为符号位,此时我们规定char下的负数补码 1000 0000为-128,上文已经解释过了!此时我们在不断的+1进二进制位中,就会从-128递减到-1,当-1的二级制为再+1,则为0000 0000,循环回到了值为0的时候
所以,某些题目里面,会考这个循环的正序和倒序,
正序:0~127,-128到-1
倒序:-1~-128,127到0
此时如果我们有一个char类型的数组,大小为1000,
考法1:题目试图存储0~999的值到这个char数组,那么我们只会存储进去0~127,-128到-1的值进前256个数组位置,后面就是重复再存储该顺序的值进入后面的数组位置
考法2:题目试图存储-1~ -1000的值到这个char数组,那么我们只会存储进去-1~-128,127到0的值进前256个数组位置,后面就是重复再存储该顺序的值进入后面的数组位置
五:例题讲解
1:有/无符号char以%d形式打印
#include <stdio.h>
int main()
{
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("a=%d,b=%d,c=%d", a, b, c);
return 0;
}

解释:
①:-1截断之后,在abc中存储的都是低8位补码 1111 1111
②:a b本质都是signed char类型,此时马上要被%d以有符号整形打印了,所以二者都要整形提升,得到的补码均为:
1111 1111 1111 1111 1111 1111 1111 1111
也就是32个1,然后该原码对应的原码为
1000 0000 0000 0000 0000 0000 0000 0001
所以打印结果就是-1
③:对于c,其是unsigned char,所以整形提升左补24个0,此时补码为
0000 0000 0000 0000 0000 0000 1111 1111
此时%d打印,判断符号位为0,则为正数,当前补码就是原码,直接打印二进制表示的值,255
2:有符号以%u打印
#include <stdio.h>
int main()
{
char a = -128;
char b = 128;
printf("a=%u\n", a);
printf("b=%u\n", b);
return 0;
}

解释:
①:a是有signed char类型,此时8位存储的是-128的低8位补码1000 0000,此时即将被%u打印,所以会被整形提升,此时的补码为:
1111 1111 1111 1111 1111 1111 1000 0000
此时%u打印,以无符号整形打印,所以直接视当前补码为原码,直接打印对应的二进制值即可,所以是一个很大的值,a=4294967168
②:b是有signed char类型,此时8位存储的是128的低8位补码1000 0000,所以此题打印结果相同,是因为ab都是signed char类型,此外,存储的8位都相同,都被%u打印
3:signed char范围规律
#include <stdio.h>
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));
return 0;
}

解释:
我们之前说过:
考法1:题目试图存储0~999的值到这个char数组,那么我们只会存储进去0~127,-128到-1的值进前256个数组位置,后面就是重复再存储该顺序的值进入后面的数组位置
考法2:题目试图存储-1~ -1000的值到这个char数组,那么我们只会存储进去-1~-128,127到0的值进前256个数组位置,后面就是重复再存储该顺序的值进入后面的数组位置
显然此题就是考法2,所以我们这个数组肯定会被存满数据,只不过规律是-1~-128,127到0,然后再-1~-128,127到0.......
而strlen计算的是\0之前的数据,\0的ascll码值就是0,所以其会在第一次遇到0的位置停止,每次-1~-128,127到0总共是256个,此时不算末尾的0,则答案为255
4:unsigned char范围规律
#include <stdio.h>
unsigned char i = 0;
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
}
return 0;
}
运行结果:死循环打印 hello world
解释:
因为unsigned char的范围是0~255,当i递增到255的时候,此时因为255=255,仍不停止,进行打印后,255递增为256,对于unsigned char来说,其会变成0重新开始递增,所以死循环
📌 [ 作者 ] shylyly
📃 [ 首次发布 ] 2024.3.6
❌ [ 最新修改 ] 2026.5.2
📜 [ 声明 ] 由于笔者水平有限,文中难免有疏漏或不妥之处,还望读者不吝赐教。
文章详细解释了C语言中char类型(无符号和有符号)在存储整数时的截断现象,以及整形提升的规则,特别强调了%d和%u格式化打印时对补码转换的需求。通过实例展示了不同情况下的打印结果和处理方式。


被折叠的 条评论
为什么被折叠?



