整数在内存中的存储

一、整数在内存中的存储

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补码的优势

使用补码来表示和存储整型数据具有以下几个显著的优势:

  1. 统一性:补码允许我们将符号位和数值位统一处理,简化了计算机内部的逻辑设计。

  2. 简化运算:由于计算机的算术逻辑单元(ALU)主要设计为执行加法操作,使用补码可以使得加法和减法操作统一,无需额外的硬件支持来处理减法。减去一个数可以转换为加上该数的补码。

  3. 无符号溢出:补码表示法没有正负溢出的概念,只有数值溢出。当发生溢出时,补码仍然能正确表示结果的符号。

  4. 转换简便:补码与原码之间的转换过程简单,不需要复杂的硬件电路。原码转换为补码仅需要按位取反(得到反码)然后加1,而补码转换回原码则是减1然后按位取反。

  5. 直接支持位运算:补码表示法直接支持位运算,如按位与(AND)、按位或(OR)和按位异或(XOR),这些操作对于底层系统编程和硬件设计至关重要。

1.2.3实例说明

假设我们有一个8位的整数,其补码表示如下:

  • 正数 +30000 0011
  • 负数 -31111 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;
}
  1. int check() { ... }:定义了一个名为check的函数,返回类型为int

  2. int i = 1;:在check函数内部,声明一个int类型的变量i并初始化为1

  3. return (*(char*)&i);:将i的地址转换为char*类型,然后解引用得到i的第一个字节。由于iint类型,其大小通常至少是4个字节。这里通过强制类型转换,只获取了i的最低字节。

  4. int ret = check();:调用check函数,并将返回值赋给int类型的变量ret

  5. 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;
}
  1. char a = -1;:声明一个char类型的变量a并初始化为-1。在大多数现代计算机上,char是有符号的,并且通常占用1个字节。如果使用标准的补码表示法,-1将被存储为0xFF(即255的十六进制表示)。

  2. signed char b = -1;:声明一个signed char类型的变量b并初始化为-1signed char明确表示这是一个有符号的字符类型,其行为与char相同(除非char被定义为无符号,但这是不常见的)。

  3. unsigned char c = -1;:声明一个unsigned char类型的变量c并初始化为-1。由于unsigned char是无符号的,它不能表示负数。因此,-1将被解释为一个非常大的正数,具体来说,是无符号char能表示的最大值,即255

  4. printf("a=%d,b=%d,c=%d", a, b, c);:使用printf函数打印变量abc的值。由于这些变量都被初始化为-1,但类型不同,它们在内存中的表示和打印出来的结果会有所不同。ab将打印出-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;
}
  1. int* ptr1 = (int*)(&a + 1);:这里尝试将数组a的地址与1相加,然后强制类型转换为int*指针赋给ptr1。这种操作是不正确的,因为&a + 1不会得到数组下一个元素的地址,而是得到数组首地址之后的第一个整数的地址,这通常不是有效的地址,因为数组的地址被解释为了数组首元素的地址。此外,这种类型转换也是不安全的,可能导致未定义行为。

  2. int* ptr2 = (int*)((int)a + 1);:这里将数组a的地址强制类型转换为整型然后加1,再转换回int*指针赋给ptr2。这同样是错误的,因为(int)a + 1实际上是将数组的首地址转换为整数然后加1,这并不会得到数组中下一个元素的地址,而是得到一个随机的整数,再转换回指针。

  3. printf("%x %x", ptr1[-1], *ptr2);:使用printf函数以十六进制格式打印ptr1[-1]*ptr2的值。由于ptr1ptr2都指向了无效的地址,这将导致未定义行为,可能打印出随机值,或者程序崩溃。

这期我们很粗略的讲解了一下整型在内存中的存储,有什么问题都可以留言,如果对您有帮助就点个赞吧!

  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值