整数的2进制表示方法有三种,原码、反码和补码 原码、反码和补码是用于表示有符号整数的三种方式。
下面我们来举一个例子:
假设我们使用8位表示有符号整数:
反码和补码的存在是为了解决原码的加减法运算问题。使用补码,可以通过简单的加法运算来实现有符号整数的加减法,而不需要单独处理符号位。补码的另一个重要特性是,一个数的补码加上它的补码应该等于零。
在计算机中,通常使用补码来表示和存储有符号整数,因为它可以简化算术运算。
在内存中,整数的存储通常是以二进制形式表示的。整数占用的存储空间取决于其数据类型的位数。在大多数系统中,整数通常以补码形式存储。
例如,在C语言中,常见的整数类型如下:
char:通常占用1个字节(8位),可以表示-127到127之间的整数(带符号)或0到255之间的整数(无符号)。 short:通常占用2个字节(16位),可以表示-32767到32767之间的整数(带符号)或0到65535之间的整数(无符号)。 int:通常占用4个字节(32位),可以表示-2147483647到2147483647之间的整数(带符号)或0到4294967295之间的整数(无符号)。 long long:通常占用8个字节(64位),可以表示更大范围的整数。 整数在内存中的存储是直接以其二进制表示形式存储的。例如,十进制数19在内存中的存储形式可能是00010011(假设使用8位的存储空间)。整数的存储形式还取决于计算机的字节序,即大端序(高位字节存储在低地址)或小端序(高位字节存储在高地址)。
我们以一个数据为开始,来观察它在内存中的存储
#include <stdio.h>
int main()
{
int a = 0x11223344;
return 0;
}
我们会发现,在内存中,它是倒着存储的。由此,引出大小端:
在大端字节序中,整数的高位字节存储在内存的低地址处,而低位字节存储在内存的高地址处。换句话说,整数的最高有效位存储在最低的地址,最低有效位存储在最高的地址。这种方式符合我们阅读整数的习惯,也使得多字节整数在内存中的表示更加直观。 而在小端字节序中,整数的低位字节存储在内存的低地址处,高位字节存储在内存的高地址处。整数的最高有效位存储在最高的地址,最低有效位存储在最低的地址。相比大端字节序,小端字节序在内存中的表示可能会更加符合硬件架构的特点,但是在习惯方面可能会有些令人困惑。
上述例子中(0x11223344):其中0x11是最高有效的字节,0x44是最低有效的字节。 当表示为大端字节序时,0x11223344会被存储为:
0x11 0x22 0x33 0x44
而在小端字节序时,0x11223344会被存储为:
0x44 0x33 0x22 0x11
有排序之分,是因为内存的存储以字节为单位,每个字节占八个比特位,而像整形为四个字节,在存储中必然会有排序问题,
那么,如何判断当前编译器环境下的大小端顺序呢?
#include <stdio.h>
int main() {
int n = 1;
if (*(char *)&n == 1) {
printf("小端\n");
} else {
printf("大端\n");
}
return 0;
}
这段代码的原理是,在内存中使用一个整型变量n,然后通过将n的地址强制转换为指向char类型的指针,接着对这个char类型指针所指向内存的内容进行判断。如果这个地址的第一个字节存储的是1,那么说明这个系统是小端序;如果第一个字节存储的是0,那么说明这个系统是大端序。
我们以一道题为例,来展开我们的内容:
#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;
}
请问上述代码的输出结果是什么?
我们知道,char占一个字节,而打印的为整数为四个字节,这里就要引入整形提升的知识点
整形提升是指将较小的整数类型转换为较大的整数类型的过程,在c语言中,当对较小的整数类型进行算朑运算时,这些值会被自动提升为较大的整数类型再进行运算。这是为了避免精度丢失和提升计算的精度。 举个例子,如果有一个char类型的变量和一个int类型的变量,进行加法运算的时候,char类型的值会被提升为int类型再进行运算。这是因为int类型通常比char类型更大,所以把char类型提升为int类型可以避免精度丢失。 另外,如果有一个int类型的变量和一个unsigned int类型的变量进行运算,int类型的值会被提升为unsigned int类型再进行运算,这是为了避免带符号数和无符号数混合运算时的问题。 那么整形提升的规则是什么呢? 1. 有符号整数提升是按照变量的数据类型的符号位来提升的 2. 无符号整数提升,高位补0 eg:
//负数的整形提升
char c1 = -1;
变量c1的⼆进制位(补码)中只有8个⽐特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,⾼位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的⼆进制位(补码)中只有8个⽐特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,⾼位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//⽆符号整形提升,⾼位补0
所以,回到上面的那道题
#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;
}
a的原码如下:
10000001
得到反码补码:
1111110
1111111
打印时,转换为整形,发生整形提升:
111111111111111111111111
得到原码为:
100000000000000000000001
所以a打印值为-1; 同理,b打印值也为-1; 而对于c: 无符号字符的范围是 0 到 255。当你将 -1 赋值给无符号字符时,它会被转换为无符号数,即 255(内存中的表示为 11111111),其转换如下: -1 是一个整数字面值,它通常由编译器当作 int 类型处理,因此它在内存中的表示(假设 int 是32位)按照补码将是 11111111 11111111 11111111 11111111,它代表十进制中的 -1。 ** 当这个 -1 被赋值给一个 unsigned char 变量时,它需要转换成一个无符号的8位值。 unsigned char 类型仅使用值的低8位,进行了截断,所以 -1 的低8位是 11111111。 这8位被直接截断并复制到 unsigned char 类型的变量 c 中。 由于 c 是一个 unsigned char 类型,这8位 11111111 就被解释为无符号整数值,即 255。在无符号数中,11111111 的二进制表示就是十进制中的 255。
所以对c进行整形提升后,用0补位
00000000000000000000000011111111
符号位为0; 则原返补码相同,则打印值为255;
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n",a);
return 0;
}
这里出现了一个类型不匹配的问题。%u 是用来打印无符号整数的格式说明符,而 a 是有符号的 char 类型。在这种情况下,会发生隐式的整形提升。
整形提升规则表明当 char 类型(假设它是有符号的)参与到表达式中时,它会提升为 int 类型以执行运算。所以,-128 会被提升为 int 类型的 -128。
在32位系统上,-128 的 int 表示为:
11111111 11111111 11111111 10000000 (补码表示)
然而,由于使用了 %u 这个无符号整数的格式说明符,printf 将会把 a 的值当作无符号数来解释和打印。 所以打印结果如下
4294967168
#include <stdio.h>
int main()
{
char a = 128;
printf("%u\n",a);
return 0;
}
首先,需要注意的是,char 类型的范围通常是 -128 到 127(假设 char 是有符号的且占用1个字节)。当你尝试将 128 赋值给 char 类型的变量时,会发生溢出。在这种情况下,128 实际上会被解释为有符号 char 的 -128(补码形式是 10000000)
进行整形提升后,-128 会被提升为 int 类型的 -128。在32位系统上,-128 的 int 表示为:
11111111 11111111 11111111 10000000
我们会发现,结果与例题二相同:
4294967168
在C语言中,char
和 unsigned char
类型的数据大小由其位数定义,通常是 8 位或者 1 字节。这意味着这些类型可以有固定数量的可能值,它们的表示范围是有界的,因此当它们的值超出这个范围时会出现周期性的回绕行为——这通常称为溢出。这里是 char
和 unsigned char
溢出的行为规律:
char
当我们讨论 char
的周期和规律时,假设 char
类型是有符号的,并且我们使用的机器是 8 位字符的系统。
char
增加超过 127 时,它会回绕到 -128,进一步增加则继续从 -127 向上增加;当 char
减少低于 -128 时,它会回绕到 127,进一步减少则继续从 126 向下减少。unsigned char
unsigned char
类型总是无符号的,也通常是 8 位。
unsigned char
增加超过 255 时,它会回绕到 0,进一步增加则继续从 1 向上增加;当 unsigned char
减少低于 0 时(在C中通过操作导致负数赋值给无符号类型),它会回绕到 255,进一步减少则继续从 254 向下减少。考虑以下情况,我们对 char
和 unsigned char
类型的变量递增或递减操作:
char
类型递增:char
变量,例如 char c = 120;
。c
,一直到它接近边界值 127。c
达到 127 并且再次递增时,它变成 -128(回绕)。unsigned char
类型递增:unsigned char
变量,例如 unsigned char uc = 250;
。uc
,一直到它接近边界值 255。uc
达到 255 并且再次递增时,它变成 0(回绕)。这种周期性行为是底层数据类型和算术操作直接的结果。这也说明了为什么在实际编程中很重要的一点,那就是确保不会意外地造成数据类型溢出,因为这会导致不可预期的行为。
在此基础上,我们看下面的例题:
#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;
}
我们了解到,char类型的数据在从-1一直减,知道-128,再减一得到127,继续减,得到0,形成循环 而strlen在遇到’\0’结束,所以上述的结果则为,128+127=255;
#include <stdio.h>
unsigned char i = 0;
int main()
{
for(i = 0;i<=255;i++)
{
printf("hello world\n");
}
return 0;
}
这道题,首先i为无符号char类型的数据,在i从零加到255后,再加一发生溢出,得到0,所以i恒小于255,程序陷入死循环。
#include <stdio.h>
int main()
{
int a[4] = { 1, 2, 3, 4 };
int *ptr1 = (int *)(&a + 1);
int *ptr2 = (int *)((int)a + 1);
printf("%x,%x", ptr1[-1], *ptr2);
return 0;
}
我们下面好好分析这个题 &a取出整个数组的地址,加一跳过整个数组。指针由其实位置指向末尾
以16进制的结果打印,假设为小端存储,则上述图形可转化如下:
ptr【-1】;即为*(ptr-1), 此时ptr减一指向04起始位置,解引用,打印的结果即为
4
而对于ptr2 int* ptr2 = (int*)((int)a + 1); 这行代码使 ptr2 指向数组 a[0](即数字 1)的首地址向前移动了 1 个字节。因此,ptr2 实际上指向的是 a[0] 的第二个字节。由于 ptr2 是 int* 类型的指针,当你对其进行解引用操作(*ptr2)时,它将尝试读取四个字节作为一个整数。
所以,*ptr2 解引用后的值应该是 0x02000000
同样以一道例题为开始
#include <stdio.h>
int main()
{
int n = 9;
float *pFloat = (float *)&n;
printf("n的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
return 0;
}
你认为结果是什么呢? 我们运行结果如下
n的值为:9
*pFloat的值为:0.000000
num的值为:1091567616
*pFloat的值为:9.000000
所以为什么第三行第四行的结果可能与预想不同呢? 这正是因为浮点数在内存中存储的特殊性
浮点数在内存中的存储遵循IEEE 754标准,是目前最广泛使用的浮点数表示方法。
任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式: V=(-1)S×M×2E. (-1)S表示符号位,当S=0,V为正数;当S=1,V为负数 M 表示有效数字,M是大于等于1,小于2的 2E表示指数位 单精度浮点数(32位):包括1位符号位,8位指数位,和23位尾数位。
双精度浮点数(64位):包括1位符号位,11位指数位,和52位尾数位。
举例来说: ⼗进制的5.0,写成⼆进制是 101.0 ,相当于 1.01×2^2 。 那么可以得出S=0,M=1.01,E=2。 在计算机内部保存M时,默认这个数的第⼀位总是1,因此可以被舍去,只保存后⾯的 xxxxxx部分。⽐如保存1.01的时候,只保存01,等到读取的时候,再把第⼀位的1加上去。 我们只需要存储后面的部分,即 (01)。我们将这个二进制数扩展到23位,剩下的位用0填充。所以尾数位为 (01000000000000000000000)。 符号位首位则为0; 至于E,E为无符号整数,如果E为8位,它的取值范围为0-255;如果E为11位,它的取值范围为0~2047,但是指数位可能出现负数,所以IEEE 754规定,存⼊内存时E的真实值必须再加上 ⼀个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。 所以实际存储的指数值为 (2 + 127 = 129)。129的二进制形式为 (10000001) 将这些部分组合起来,得到5.0的IEEE 754单精度浮点数表示为:
0 10000001 01000000000000000000000
当从内存中取出IEEE 754标准浮点数的指数部分时,可以将其分为以下三种情况 E不全为0或不全为1 这意味着这些指数值代表了有效的浮点数。在解析指数时,需要从其值中减去偏移量以得到实际的指数值。 对于32位的单精度浮点数,偏移量是127。如果指数的8位不全是0或1,例如10000001,则实际指数 (E = 129 - 127 = 2)。 对于64位的双精度浮点数,偏移量是1023。如果指数的11位不全是0或1,则类似地减去1023得到实际的指数值。 再将有效数字M前加上第⼀位的1 E全为0 这种情况下,浮点数表示接近于0的非常小的数 这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第⼀位的1,而是还原为0.xxxxxx的小数 特殊值 如果指数部分全部为1,则表示该数为特殊值。在这种情况下,尾数的不同值表示不同的特殊情况:
无穷大:如果尾数全为0,那么该值表示无穷大。符号位决定了是正无穷还是负无穷。 非数:如果尾数不全为0,那么该值表示非数.
最后,回到开始的那道题
#include <stdio.h>
int main()
{
int n = 9;
float *pFloat = (float *)&n;
printf("n的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
return 0;
}
9以整型的形式存储在内存中,得到如下⼆进制序列:
0000 0000 0000 0000 0000 0000 0000 1001
将 9 的⼆进制序列按照浮点数的形式拆分,得到第⼀位符号位s=0,后面8位的指数 E=00000000 , 最后23位的有效数字M=000 0000 0000 0000 0000 1001。 由于指数E全为0,所以符合E为全0的情况。 因此,V是⼀个很小的接近于0的正数,所以用十进制小数表示就是0.000000。 浮点数9.0 等于⼆进制的1001.0,即换算成科学计数法是:1.001×2^3 9.0 = (−1)0×(1.001) × 23 第⼀位的符号位S=0,有效数字M等于001后⾯再加20个0,凑满23位,指数E等于3+127=130,即10000010
0 10000010 001 0000 0000 0000 0000 0000
这个浮点数在被当做整形的时候,得到的值即为1091567616
感谢观看!求点赞和关注!