秒懂signed/unsigned char以%d或%u形式打印的核心

文章详细解释了C语言中char类型(无符号和有符号)在存储整数时的截断现象,以及整形提升的规则,特别强调了%d和%u格式化打印时对补码转换的需求。通过实例展示了不同情况下的打印结果和处理方式。

引入:

初学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就得到补码。
总结:原码到补码,取反+1即可

②:补码到原码

第一种:补码-1 再 取反
第二种:补码取反 在 +1
两种都能准确的得到原码,但是第一种可以在计算的过程中得到反码,而第二种不可以

二:截断和整形提升

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:只要是涉及到计算的时候,必须进行整形提升再进行计算,本质深层原因是因为硬件的性质:

①:表达式的整型运算要在CPU的相应运算器件内执⾏,CPU内整型运算器(ALU)的操作数的字节⻓度⼀ 般就是int的字节⻓度,同时也是CPU的通⽤寄存器的⻓度。
②:因此,即使两个char类型的相加,在CPU执⾏时实际上也要先转换为CPU内整型操作数的标准⻓度。
③:通⽤CPU(general-purpose CPU)是难以直接实现两个8⽐特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。所以,表达式中各种⻓度可能⼩于int⻓度的整型值,都必须先转换为 int或unsigned int,然后才能送⼊CPU去执⾏运算

三:%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 char1-128127
short2-32,76832,767
int4-2,147,483,6482,147,483,647
long(Linux 64位)8-9,223,372,036,854,775,8089,223,372,036,854,775,807
long(Windows 64位)4-2,147,483,6482,147,483,647
long long8-9,223,372,036,854,775,8089,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
📜 [ 声明 ]   由于笔者水平有限,文中难免有疏漏或不妥之处,还望读者不吝赐教。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值