小言_互联网的博客

数据的存储(1)

297人阅读  评论(0)

深度剖析数据在内存中的存储

本章重点

  1. 数据类型详细介绍
  2. 整形在内存中的存储:原码、反码、补码
  3. 大小端字节序介绍及判断
  4. 浮点型在内存中的存储解析

正文开始

1. 数据类型介绍

前面我们已经学习了基本的内置类型(C语言本身自带的类型):

char        //字符数据类型
short       //短整型
int         //整形
long        //长整型
long long   //更长的整形
float       //单精度浮点数
double      //双精度浮点数
//C语言有没有字符串类型?

以及他们所占存储空间的大小。
1 2 4 4/8 8
4 8

类型的意义:

  1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)。
  2. 如何看待内存空间的视角
    由于数据大小不同,数据类型的范围不同,有了丰富的类型,让我们选择合适的数据类型时更加方便,在一定程度上可以避免内存的浪费。
    其次,数据类型的不同,决定了我们看待数据的角度。
    当是int short我们认为这是一个整数,当看到double float我们认为这是一个小数。

1.1 类型的基本归类

整形家族:

char
   unsigned char
   signed char
short
   unsigned short [int]
   signed short [int]
int
   unsigned int
   signed int
long
   unsigned long [int]
   signed long [int]

unsigned 无符号的
int是整形,short是短整型,而short int 是全称,int可以省略。
每一个字符都有对应的ASCII码值,所以char归类到整形。
unsigned 和signed
生活中数值有正数有负数。
然后有些数值,只有正数,没有负数(身高)
有些数值,有正数也有负数(温度)

在C语言中为了把数值描述更加准确,因此把只有正数没有负数的类型,叫做无符号数字。
有些数值,有正数也有负数,为了区分正负,叫做有符号数字。

在C语言中并没有明确规定char类型是有符号还是无符号,所以更准确的可以把char类型分为char,signed char,unsigned char。其中char是有符号还是无符号是不能明确的,有些编译器认为char是有符号的,有些编译器认为char是无符号的。

然而short int long 是有规定给的,如果只写short int long,编译器默认为是有符号的。
short 等价于signed short
int 等价于signed int
long 等价于signed long

浮点数家族:

float
double

浮点数家族我们暂时不讲。

构造类型:
构造类型也叫做自定义类型

> 数组类型
> 结构体类型 struct
> 枚举类型 enum
> 联合类型 union

为什么数组类型也叫自定义类型?

char arr[6]   //数组元素类型和数组元素个数由自己决定 
char arr[5]
int arr[5]

数组元素类型和数组元素个数由自己决定。所以数组类型我们归为构造类型(自定义类型)。

结构体类型我们比较了解。
而枚举类型和联合体类型不太了解。
这三块内容我们放到第四节,自定义类型详解中进行讲解。

指针类型

int *pi;
char *pc;
float* pf;
void* pv;

空类型:
void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型。

返回类型

void test()
{
  //不需要返回值
}
int main()
{
  return 0;
}

函数的参数

void test(void)
{
  //不需要函数的参数
}
int main()
{
  return 0;
}

指针类型


int main()
{
  void* p = NULL;
  int a = 10;
  void* p1 = &a;  //原本是用int*类型进行存储
  return 0;
}

void* 就像一个垃圾桶,不管什么类型的指针类型,甚至结构体类型的指针都能往里面放。
虽然什么类型的指针都能往void*里面进行存储。但是却不能进行操作。

void* = &p;
p++;  //error
*p;  //error

void*一般用于临时存放地址,可以通过强制类型转换进行使用,至于它的应用,我们以后再说。

2. 整形在内存中的存储

我们之前讲过一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
那接下来我们谈谈数据在所开辟内存中到底是如何存储的?
比如:

int a = 20;
int b = -10;

我们知道为 a 分配四个字节的空间。
那如何存储?
下来了解下面的概念:

2.1 原码、反码、补码

计算机中的整数有三种2进制表示方法,即原码、反码和补码。

三种表示方法均有符号位数值位两部分,最高位为符号位,符号位都是用0表示“正”,用1表示“负”,而数值位
正数的原、反、补码都相同。
负整数的三种表示方法各不相同。

原码
直接将数值按照正负数的形式翻译成二进制就可以得到原码。
反码
将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码
反码+1就得到补码。

举个例子

int main()
{  int a = 20;
//00000000 00000000 00000000 00010100 原码
//00000000 00000000 00000000 00010100 反码
//00000000 00000000 00000000 00010100 补码
  int b = -10;
//10000000 00000000 00000000 00001010 原码
//11111111 11111111 11111111 11110101 反码
//11111111 11111111 11111111 11110110 补码

  return 0;
}

对于有符号的数字来说,最高位是符号位。

对于整形来说:数据存放内存中其实存放的是补码。
为了验证数据在内存中是以补码的形式存在。
打开调试-窗口-内存
查看b的数据
f6 ff ff ff 十六进制
经过计算正好就是对应的二进制补码,不过是倒着进行存储。

但是为什么以补码的形式存放?
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;
同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

cpu只有加法器,如何计算减法?

int main()
{
  int c = 1-1;
  //1 + (-1)在计算减法的时候,把减号和后面的数字看成一个整体进行相加。
//补码相加
//  00000000 00000000 00000000 00000001
//  11111111 11111111 11111111 11111111
//相加为
//100000000 00000000 00000000 00000000
由于是int类型,最高位无法存储,所以保留的就是0。
  return 0;
}

原码与补码的转换
原码到补码,有一种方式,原码取反、加一得到补码
补码到原码有两种方式
1 补码减一、取反得到原码
2 补码取反、加一得到原码
方式二举例
-1的
补码11111111 11111111 11111111 11111111
10000000 00000000 00000000 00000000
  10000000 00000000 00000000 00000001

这也就是为什么上面说补码与原码相互转换,其运算过程是相同的。

int main()
{
  int a = 0x12345678;
  return 0;
}
//在内存窗口显示为
//78 56 34 12

一个十六进制位是四个二进制位,两个十六进制位是八个二进制位。一个字节,八个比特位。所以两个十六进制位是一个字节。所以八个十六进制位刚好四个字节。

所以是以字节为单位进行存储。

至于刚刚提到的是什么,下面我们介绍*大小端介绍**

2.2 大小端介绍

2.2.1什么大端小端:

大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地
址中。

2.2.2 为什么有大端和小端:

个人理解

内存中的存储单元是一个字节,所以我们以字节讨论存储顺序问题。
当一个数值超过一个字节了,要存储到内存中,就有顺序的问题。

0x11223344
低地址---------->高地址

11 22 33 44/22 11 33 44 /44 33 22 11
理论上随意存储,只要最后能有办法还原成0x11223344就可以。

但是如果随意存储的话,存储的顺序很难记忆,并且不符合常理。
所以为了简单,方便。
数据的存储留下两种方法要么
11 22 33 44 要么44 33 22 11
这就是所谓的大小端。

对于一个十进制数字123,3是个位,1是百位,也就是3在低位,1在高位。
对于0x11223344 44是低字节的数据,11是高字节的数据。
小端字节序存储就相当于倒着进行存储。
大端字节序就相当于正着进行存储。

vs编译器环境下就是小端字节序存储。

我们常见的x86是小端模式。

补充:
我们是以字节为单元讨论数据存储顺序。
数值有不同的表现形式,10进制,2进制,8进制。
其中16进制是1-9- a-f组成
其中a-f代表10-15

其中f(15)的二进制是1111

所以对一个二进制数字例如
1111 0000 1111 1011
每四位转换为一个十六进制数字。

所以一个十六进制位代表四个二进制位。两个十六进制是八个二进制位,也就是一个字节。

补充:

1内存中存放的是补码
2整形表达式计算式用的内存中补码计算的。
3打印和我们看到的都是原码

百度2015年系统工程师笔试题:
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)

小端字节序存储:
把一个数值的低位字节的内容存在低地址处,高位字节的内容,存放在高地址处。
大端字节序存储:
把一个数值的低位字节的内容存在高地址处,高位字节的内容,存放在低地址处。

程序:
对于int a= 1;
十六进制为0x 00 00 00 01
如果是小端存储为 01 00 00 00
如果是大端存储为 00 00 00 01
判断首元素地址指向一个字节的元素是0或者1就好了

int main()
{
  int a = 1;
  char* p = (char*)&a;

  if(*p==1)
  {
    printf("小端\n");
  }
  else
    printf("大端\n");
  
  return 0;
}

如果将代码封装成函数

int check_sys()
{
  int a = 1;
  char* p = (char*)&a;

  if(*p==1)
  {
    return 1;
  }
  else
  {
    return 0;
  }
}
int main()
{
  if(check_sys()==1)
    printf("小端\n");
  else
    printf("大端\n");
  return 0;
}

 

对代码进行简化

int check_sys()
{
  int a = 1;
  if(*(char*)&a==1)
  {
    return 1;
  }
  else
  {
    return 0;
  }
}
int main()
{
  if(check_sys()==1)
    printf("小端\n");
  else
    printf("大端\n");
  return 0;
}

 

再次优化

int check_sys()
{
  int a = 1;
  return *(char*)&a;  //返回1表示小端,返回0表示大端
}
int main()
{
  if(check_sys()==1)
    printf("小端\n");
  else
    printf("大端\n");
  return 0;
}

2.3 练习

代码一

//输出什么?
#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=-1,b=-1,c=225

先看char类型的-1存储
先写出来-1的原码反码补码
10000000 00000000 00000000 00000001 -1的原码
11111111 11111111 11111111 11111110 -1的反码
11111111 11111111 11111111 11111111 -1的补码
但是由于存储到char类型中,只有8个比特位,所以会发生截断。只把最右边八个1进行存储
11111111

再看signed char 类型-1的存储。
11111111 11111111 11111111 11111111 -1的补码
但是由于存储到char类型中,只有8个比特位,所以会发生截断。只把最右边八个1进行存储
11111111

再看unsigned char 类型-1的存储。
11111111 11111111 11111111 11111111 -1的补码
但是由于存储到char类型中,只有8个比特位,所以会发生截断。只把最右边八个1进行存储
11111111

虽然存储的一样,但是char 和signed char 把最高位当成符号位,unsigned char的八个比特位都是数值位。
打印的时候是%d ,对应的是整形,而我们是char类型,需要发生整形提升。
char a存储的是11111111进行整形提升。
整形提升按照符号位补充。
整形提升完为
11111111 11111111 11111111 11111111这是补码
11111111 11111111 11111111 11111110这是反码
10000000 00000000 00000000 00000001这是原码

所以打印出来是-1
因为char 在vs中等价于signed char
所以signed char 打印出来也是-1

对于unsigned中存储的11111111
先进行整形提升,由于是无符号的,所以直接补充0
00000000 00000000 00000000 11111111 这是补码
00000000 00000000 00000000 11111111 这是原码
所以打印出来是225

补充
16进制打印 %x
8进制打印 %o
有符号数和无符号数的取值范围如何定?
有符号位
一个字节,八个比特位,所能存储的所有数字
00000000 0
00000001 1
00000010 2
00000011 3
00000100 4
···
01111111 127
10000000 -128
10000001 -127
···
11111110 -2
11111111 -1

2^8是256
其中对于10000000直接规定为-128
因为-123的二进制数字为
原码110000000
反码101111111
补码110000000
存储到8个比特位中,就是10000000

所以char 和signed char的取值为-128~127

无符号
8个比特位都是数值位
11111111就是255
所以对于一个unsigned char来说,范围是从0~255

由此类推
signed short
0000000000000000 0
···
0111111111111111 32767
1000000000000000 -32768
···
1111111111111111 -1

所以signed short 的取值范围是-32768~32767

unsigned short 的取值范围是 0~65535

代码二

2.
#include <stdio.h>
int main()
{
    char a = -128;
    printf("%u\n",a);
    return 0;
}
//-----------------
//编译器运行结果为
//4294967168

%u是打印无符号整形,认为内存中存放的补码对应的是一个无符号数。
%d 是打印有符号整形,认为内存中存放的补码对应的是一个有符号的数。

-128对应的32位比特位
10000000 00000000 00000000 10000000原码
11111111 11111111 11111111 01111111反码
11111111 11111111 11111111 10000000补码
存放在char a 中的数据为
10000000
将%u打印则进行整形提升,至于整形提升补0还是1看数据存放的数据类型,而不是要打印的格式
整形提升
因为有符号,就补充最高位
11111111 11111111 11111111 10000000
所以这个就是整形提升后的补码
再回归原码
因为打印的为无符号的%u,所以原码就是补码
11111111 11111111 11111111 10000000打印的十进制数字为4294967168

这个就是我们今天讲的关于整形存储还有大小端的问题,不过我们的浮点数存储还没有讲解,留到下次再分享。


转载:https://blog.csdn.net/m0_71545436/article/details/128098322
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场