一、整数在内存中的存储
1.1原码、反码与补码
在计算机科学中,整数的二进制表示方法是一个基础而重要的概念。这些表示方法包括原码、反码和补码。如果您对这些概念不太熟悉,下面我们将简要介绍它们。
1.1.1原码(True Form)
- 定义:原码是最直观的表示方法,其中最高位用作符号位(0表示正数,1表示负数),其余位表示数值本身。
- 示例:对于正整数
5
,其原码表示为0000 0101
;对于负整数-5
,其原码表示为1000 0101
。
1.1.2反码(Complement Form)
- 定义:反码是根据原码得到的。对于正数,其反码与原码相同;对于负数,符号位保持不变,其余位按位取反。
- 示例:对于负整数
-5
,其反码表示为1111 1010
(除了符号位外,其他位取反)。
1.1.3补码(Complement Representation)
- 定义:补码也是根据原码得到的。对于正数,其补码与原码相同;对于负数,其补码是反码加1。
- 示例:继续使用
-5
的例子,其补码表示为1111 1011
(在反码的基础上加1)。
1.1.4符号位与数值位
- 符号位:在这些表示方法中,最高位被用作符号位,用于区分整数的正负。
- 数值位:除了符号位之外的所有位,用于表示数值的大小。
1.2 二进制对整型存储的意义
1.2.1整型数据的存储形式
在计算机系统中,整型数据在内存中的存储采用的是补码(Two's Complement)形式。这意味着无论是正数还是负数,它们都以补码的形式存在。
1.2.2补码的优势
使用补码来表示和存储整型数据具有以下几个显著的优势:
-
统一性:补码允许我们将符号位和数值位统一处理,简化了计算机内部的逻辑设计。
-
简化运算:由于计算机的算术逻辑单元(ALU)主要设计为执行加法操作,使用补码可以使得加法和减法操作统一,无需额外的硬件支持来处理减法。减去一个数可以转换为加上该数的补码。
-
无符号溢出:补码表示法没有正负溢出的概念,只有数值溢出。当发生溢出时,补码仍然能正确表示结果的符号。
-
转换简便:补码与原码之间的转换过程简单,不需要复杂的硬件电路。原码转换为补码仅需要按位取反(得到反码)然后加1,而补码转换回原码则是减1然后按位取反。
-
直接支持位运算:补码表示法直接支持位运算,如按位与(AND)、按位或(OR)和按位异或(XOR),这些操作对于底层系统编程和硬件设计至关重要。
1.2.3实例说明
假设我们有一个8位的整数,其补码表示如下:
- 正数 +3:
0000 0011
- 负数 -3:
1111 1101
在这个例子中,可以看到正数和负数的补码表示方式,以及它们在内存中的存储形式。
1.2.4结论
补码的使用是现代计算机系统中整型数据存储的标准方法,它提供了一种高效、简洁且统一的方式来处理整数的存储和运算。
二、大小端字节序和字节序判断
2.1引子
我们先来看看下面这段简单的代码:
#include<stdio.h>
int main()
{
int a = 0x44332211;
return 0;
}
我们调试一下看看运行结果
我们可以看到一个奇怪的现象,a中0x44332211这个数字是按照字节倒着存储的。要解释这个问题就不得不讲到我们的大小端了。
2.2 什么是大小端
定义
大小端是指计算机存储多字节数据类型(如整数、浮点数等)时字节的排列顺序。它决定了在内存中多字节数据的字节如何组织。
类型
- 大端(Big-endian):大端模式下,一个多字节值的最高位字节(即“大”端)存储在最低的内存地址处,其余字节按照大小递减的顺序存储。
- 小端(Little-endian):小端模式下,最低位字节(即“小”端)存储在最低的内存地址处,其余字节按照大小递增的顺序存储。
示例
以一个32位的整数0x12345678
为例:
- 在大端模式下,内存中的存储顺序为:
0x12 0x34 0x56 0x78
- 在小端模式下,内存中的存储顺序为:
0x78 0x56 0x34 0x12
2.3 为什么要有大小端
历史原因
大小端的存在主要是由于历史原因和不同计算机架构的设计选择。不同的计算机系统可能会采用不同的字节序,这取决于它们的设计者如何决定存储数据。
兼容性
- 大端模式:一些传统的计算机系统,如IBM的大型机,使用大端模式。大端模式在网络传输中较为常见,因为网络协议通常使用大端模式。
- 小端模式:现代大多数个人计算机和服务器使用的是小端模式,因为它在某些操作中可能更加高效。
性能考虑
- 在大端模式下,多字节数据的最高有效字节(MSB)总是从最低的内存地址开始,这可能使得某些类型的计算稍微高效一些。
- 在小端模式下,由于最低有效字节(LSB)在内存中的低位,这可能使得位操作和某些类型的循环更加方便。
抽象的必要性
大小端的概念也体现了计算机科学中对硬件细节的抽象。程序员在编写程序时通常不需要关心这些底层细节,因为高级语言和编译器会处理这些差异。
跨平台通信
了解大小端的概念对于处理跨平台数据交换非常重要。不同平台的程序在交换数据时需要考虑到字节序的差异,以确保数据的正确解释。
2.3练习
我们来试着自己设计一个小程序来判断当前机器的字节序
#include<stdio.h>
int check()
{
int i = 1;
return (*(char*)&i);
}
int main()
{
int ret = check();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
-
int check() { ... }
:定义了一个名为check
的函数,返回类型为int
。 -
int i = 1;
:在check
函数内部,声明一个int
类型的变量i
并初始化为1
。 -
return (*(char*)&i);
:将i
的地址转换为char*
类型,然后解引用得到i
的第一个字节。由于i
是int
类型,其大小通常至少是4个字节。这里通过强制类型转换,只获取了i
的最低字节。 -
int ret = check();
:调用check
函数,并将返回值赋给int
类型的变量ret
。 -
if (ret == 1) { ... } else { ... }
:根据ret
的值判断系统是大端还是小端。如果ret
等于1
,则表示第一个字节是1
,这是小端模式的特征;否则,是大端模式。
这里还给大家留下一个巧妙的实现方法,留给大家思考一下。下期内容会给大家详细讲解
int check()
{
union
{
int i;
char c;
}un;
un.i = 1;
return un.c;
}
int main()
{
int ret = check();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
我们接下来做几个小练习检测一下本期内容的学习成功
//代码1
#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 a = -1;
:声明一个char
类型的变量a
并初始化为-1
。在大多数现代计算机上,char
是有符号的,并且通常占用1个字节。如果使用标准的补码表示法,-1
将被存储为0xFF
(即255的十六进制表示)。 -
signed char b = -1;
:声明一个signed char
类型的变量b
并初始化为-1
。signed char
明确表示这是一个有符号的字符类型,其行为与char
相同(除非char
被定义为无符号,但这是不常见的)。 -
unsigned char c = -1;
:声明一个unsigned char
类型的变量c
并初始化为-1
。由于unsigned char
是无符号的,它不能表示负数。因此,-1
将被解释为一个非常大的正数,具体来说,是无符号char
能表示的最大值,即255
。 -
printf("a=%d,b=%d,c=%d", a, b, c);
:使用printf
函数打印变量a
、b
和c
的值。由于这些变量都被初始化为-1
,但类型不同,它们在内存中的表示和打印出来的结果会有所不同。a
和b
将打印出-1
,因为它们是有符号的,而c
将打印出255
,因为它是无符号的。
//代码2
#include<stdio.h>
int main()
{
char a = -128;
printf("%u\n", a);
return 0;
}
//代码3
#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;
}
其实这里都是涉及到整数类型他的范围限制,这里的内存中只存得下-128到127,strlen是统计\0前得数,\0的ASCII码值就是0。
//代码4
#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;
}
-
int* ptr1 = (int*)(&a + 1);
:这里尝试将数组a
的地址与1相加,然后强制类型转换为int*
指针赋给ptr1
。这种操作是不正确的,因为&a + 1
不会得到数组下一个元素的地址,而是得到数组首地址之后的第一个整数的地址,这通常不是有效的地址,因为数组的地址被解释为了数组首元素的地址。此外,这种类型转换也是不安全的,可能导致未定义行为。 -
int* ptr2 = (int*)((int)a + 1);
:这里将数组a
的地址强制类型转换为整型然后加1,再转换回int*
指针赋给ptr2
。这同样是错误的,因为(int)a + 1
实际上是将数组的首地址转换为整数然后加1,这并不会得到数组中下一个元素的地址,而是得到一个随机的整数,再转换回指针。 -
printf("%x %x", ptr1[-1], *ptr2);
:使用printf
函数以十六进制格式打印ptr1[-1]
和*ptr2
的值。由于ptr1
和ptr2
都指向了无效的地址,这将导致未定义行为,可能打印出随机值,或者程序崩溃。
这期我们很粗略的讲解了一下整型在内存中的存储,有什么问题都可以留言,如果对您有帮助就点个赞吧!