C语言中各种数据在内存中的存储

简介: C语言中各种数据在内存中的存储

前言

不同类型的数据在内存中开辟的空间不同。

他们在内存中是如何存储的呢?


原码,反码,补码的概念

计算机有三种2进制表示方法,都有符号位和数值位两个部分,

  • 符号位用‘0’表示整数,用‘1’表示负数
  • 正数的原反补码都相同
  • 负数的原反补码各不相同
  • 原码:直接将该数按照二进制的方式翻译过来就是原码。符号位为1。
  • 反码:符号位不变,其他位置按位取反即为反码
  • 补码:反码加1得到的数就是补码。

 要记住的是:对于整形数据(包括字符类型数据)来说,内存中存放的是补码

如图

 在计算机系统中,使用补码会方便很多,可以将符号位和数值域统一处理,加法减法也可以统一处理(因为CPU只有加法器)。

如果想进行10减2的运算,但计算机之中只有加法器,转化为10加上(-2)进行计算

符号位进的1超出整型的范围,就被截断了,这样就可以将符号位和数值位进行统一的计算。


整型提升

什么是整型提升呢?

C语言的整型运算总是至少以缺省类型的精度来进行的,为了获取这个精度,表达式中的短整型和字符类型的数据在进行计算时,通常会提升到普通整形。说白了就是将两个短整型数据运算时以整型数据来进行运算,运算完成后得到的结果将发生截断。

整型提升的规则

  • 有符号:按照补码被截断的最高位进行提升,如果最高位是1,就在前边补1,直到变成32位的int类型。
  • 无符号:直接补零,到32位的int类型
    举一个例子
int main()
{
  char a, b, c;
  a = 127;
  //补码为0111 1111
  b = 5;
  //补码为0000 0101
  c = a + b;
  //在进行运算时进行整型提升
  //a提升为0000 0000 0000 0000 0000 0000 0111 1111
  //b提升为0000 0000 0000 0000 0000 0000 0000 0101
  //相加后 0000 0000 0000 0000 0000 0000 1000 0100
  //因为c也是char类型,必将发生截断
  //1000 0100,前边的位置补1
  //补码1111 1111 1111 1111 1111 1111 1000 0100
  //转化为原码,补码减1取反,符号位不变
  //1000 0000 0000 0000 0000 0000 0111 1100
  printf("%d", c);
  //以%d形式打印,结果为-124.
  return 0;
}

注:整型提升和普通的算术类型转换不一样,类型转换是因为两个或多个不同类型的数据进行计算时,将每个数据都转化为同一类型即范围最大的类型,而整形提升尽管计算的数据类型相同,还是可能会发生整型提升。

一个很直观的梨子:

a+b是一个算术表达式,a和b都提升至int型,所以占4个字节。

c为a+b的返回值,发生截断,变回char类型,所以只占一个字节。

 short类型或char类型运算时,会提升至int类型,要将计算结果放在一个char类型或short类型的变量时,发生截断。


  • 来看一题
int main()
{
  char a = -1;
  signed char b = -1;
  unsigned char c = -1;
  printf("%d %d %d", a, b, c);
  return 0;
}

运行结果如何呢?

-1的补码为1111 1111以%d形式打印,原码为补码减1取反,结果为-1,所以a和b的结果相同,皆为-1,而c为无符号类型,原反补相同,就算进行提升,前位也补0,11111111的十进制为255,故c的打印结果为255.


  • 再看一题
int main()
{
  char a = -128;
  printf("%u\n", a);
  return 0;
}

运行结果如何?

 这里以%u的方式打印,-128在内存中的补码为1111 1111 1111 1111 1111 1111 1000 0000,a是char类型,所以a的补码为1000 0000。%u的意思是以十进制的方式打印无符号整型。在打印1000 0000时必将发生整型提升,整型提升时前边补零或是补一看的是符号位,所以前位补1。

提升后的补码为1111 1111 1111 1111 1111 1111 1000 0000,正数的原反补都相同,换成十进制

放在VS里运行后结果确实如此

int main()
{
  char a = 128;
  printf("%u\n", a);
  return 0;
}

道理相同,128的补码为0000 0000 0000 0000 0000 0000 1000 0000,a的补码为1000 0000,他和a是-128时存进内存的补码一样,a的类型同样为有符号类型,整型提升前位补1,所以打印结果一模一样。


大小端介绍

大端小端是两种存储的方式,大端储存方式是数据的低位保存到内存的高地址中,数据的高位保存在内存的低地址处,小端存储方式是指数据的地位保存在低地址中,而数据的高位保存在内存的高地址处。

char类型的数据,在内存中只有一个字节,就不存在大小端问题,然而除了一个字节的char字符类型外还有2个字节的short,4个字节的int,等等等等,对于位数大于八位的处理器,由于寄存器的宽度大于一个字节,那么必然会出现如何对多个字节进行安排的问题,这就分化出了大小端问题。

如图详细介绍一下大小端

那么如何知道一个编译器具体是大端还是小端?

这是百度在2015年工程师笔试题里的问题,设计一个小程序判断当前机器的字节序是大端还是小端。

大端的话低位放在高地址,高位放在低地址。小端的话相反。

直接写一个代码调试先看一看

将右上角的列改为1,地址输入取地址a

可以看到低地址储存的是低位,高地址存的是高位。

  • 写一个小程序来判断
int main()
{
  int a = 1;
  char* p = (char*)&a;
  if (*p == 1)
  {
    printf("小端\n");
  }
  else
    printf("大端\n");
  return 0;
}

利用强制类型转换,取地址a取出首地址,即四个字节中的低地址位,强制类型转换为字符类型的指针,解引用后从内容即可判断其为0还是1从而判断大小端。

  • 注意不能直接截断来判断
    例如int b=(char)a;截断从低位开始,如果是很大的数截断会把高位截断,而保存低位。所以用这种方式明显是不行的。

浮点型在内存中的存储

常见的浮点数类型有float,double,long double类型。

浮点数分为整数部分和小数部分,他们在内存中的存储和整型有什么区别?

看代码

int main()
{
  int n = 9;
  float* pfloat = (float*)&n;
  printf("n的值为:%d\n", n);
  printf("*pfloat的值为:%f\n", *pfloat);
  *pfloat = 9.0;
  printf("n的值为:%d\n", n);
  printf("*pfloat的值为:%f", *pfloat);
  return 0;
}

运行后结果如图

num和*pfloat在内存中明明是同一个数,为什么结果差别如此之大,说明了浮点数和整形数据在内存中的储存方式不同。

根据国际标准,任何一个二进制浮点数V都可以用以下方式表示

(-1)^S *M *2^E (-1)^S表示符号位,如果S为0,V为正数,S为1,V为负数。

M表示有效数据,大于等于1,小于2。

2^E表示指数位。

举例

十进制的5.0,二进制形式为101.0,相当于1.01*2^2。用二进制浮点数表示(-1) ^0 * 1.01 * 2 ^2。

在这里S为0,M为1.01,E为2。

类比于十进制123,相当于1.23 * 10^2。

例如

十进制的浮点数5.5

二进制形式可以写作101.1,相当与1.011 * 2^ 2,用(-1)^0 * 1.011 * 2 * 2。

浮点数0.5

二进制形式0.1,相当于1.0 * 2^(-1)。

根据国际标准IEEE(电气和电子工程协会)规定:

对于32位的浮点数float而言,最高的1位是符号位S,接下来的8位是指数E,剩下的23位为M。

对于64位的浮点数double,最高的1位仍为S,接下来的11位是指数E,剩下的52位为有效数字M。

前边已经说过了,M大于1小于2,所以无论什么时候M都可以写作1.XXXXXX的形式,所以在保存M时,只保存1后边的部分,这样可以省出来一位。就比如32位float类型,留给M的只有23位,不保留小数点前的1的话,就可以保存24位有效数字。

对于指数E

首先E为一个无符号整数(unsigned int)

如果E为8位,他的取值范围为0~255;如果E位11位,它的取值范围为0

~2047。但是就像上述所说的0.5,E是可能出现负数的,所由IEEE规定,存入内存时,E的真实值必须要加上一个中间数,对于8位的E,这个中间数是127,对于11位E,这个中间数是1023。

比如2^10的E为10,在保存为32位的浮点数时,在内存中保存的数是10+127=137。即10001001。


  • 指数E从内存中取出还分为三种情况
  1. 指数E不全0或不全为1。

(按照32位的float来说)取出直接指数E的值直接减去127。得到E的真实值后,将M前边的1补上,再根据S是0还是1,判断这个数的正负。

例如前边所说的0.5,二进制形式为0.1,正数部分必须为1,小数点前进一位。变为1.0*2^(-1)。

E的值为(-1)+127为126,表示为01111110。1.0去除1,只留下0,补齐至23位,就是23个零。而且因为他是正数。S为0.

在内存中为0 0111 1110 0000 0000 0000 0000 0000 000。

2. 当E为全0的情况

如果E为全零,那他原来的值就是-127。那将是一个超级小的数字,2的10次方就是1024,2的-127次方接近于无穷小,这时直接判定E的值为1-127。有效数字M不再加上第一位的1,而是还原为0.XXXXX的小数,这样做是为了表示±0,以及接近于0的很小的数字。

3. 当E全为1

如果E全为1,那么在没有加上中间值时,E得值为128,2的128次幂是一个很大很大的数,如果有效数字M全部为零,就表示无穷大,是正无穷还是负无穷取决于S。


现在再来解决上边的疑问。

int n = 9;

float* pfloat = (float*)&n;

printf(“n的值为:%d\n”, n);

printf(“*pfloat的值为:%f\n”, *pfloat);

*pfloat = 9.0;

printf(“n的值为:%d\n”, n);

printf(“*pfloat的值为:%f”, *pfloat);

return 0;

  • 以%d形式打印n我想就不用说了。
  • 在内存中n=9的二进制为0000 0000 0000 0000 0000 0000 0000 1001

因为pfloat强制类型转化为float类型的指针,就是把该二进制序列看作浮点数的形式用%f的形式打印。

0 00000000 00000000000000000001001

用浮点数的储存方法解读,S为0,E为全零,是一个很小很小的数,而%f打印默认只能保留到小数点后六位。故打印结果为0.000000。

  • *pfloat=9.0。

这个时候就变为了浮点数的形式储存9.0。

9.0的浮点数形式储存,S等于0,1.001 *2^3,E加上127,即130,二进制形式为10000010,M的001,后边全部补0。

综合为 0 10000010 0010 0000 0000 0000 0000 000

对比上边的十进制结果发现确实如此。

  • 最后以%f的形式打印浮点数,相当于用浮点数的储存方式取出数据,在打印,结果为9.000000。

到了这里一切就都柳暗花明了。本章完成,如果哪里有错误的话还请大家指出。

目录
相关文章
|
20天前
|
监控 算法 应用服务中间件
“四两拨千斤” —— 1.2MB 数据如何吃掉 10GB 内存
一个特殊请求引发服务器内存用量暴涨进而导致进程 OOM 的惨案。
|
18天前
|
C语言
【c语言】动态内存管理
本文介绍了C语言中的动态内存管理,包括其必要性及相关的四个函数:`malloc`、``calloc``、`realloc`和`free`。`malloc`用于申请内存,`calloc`申请并初始化内存,`realloc`调整内存大小,`free`释放内存。文章还列举了常见的动态内存管理错误,如空指针解引用、越界访问、错误释放等,并提供了示例代码帮助理解。
29 3
|
19天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
41 1
|
24天前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
26天前
|
监控 Java easyexcel
面试官:POI大量数据读取内存溢出?如何解决?
【10月更文挑战第14天】 在处理大量数据时,使用Apache POI库读取Excel文件可能会导致内存溢出的问题。这是因为POI在读取Excel文件时,会将整个文档加载到内存中,如果文件过大,就会消耗大量内存。以下是一些解决这一问题的策略:
61 1
|
20天前
|
存储 C语言
【c语言】字符串函数和内存函数
本文介绍了C语言中常用的字符串函数和内存函数,包括`strlen`、`strcpy`、`strcat`、`strcmp`、`strstr`、`strncpy`、`strncat`、`strncmp`、`strtok`、`memcpy`、`memmove`和`memset`等函数的使用方法及模拟实现。文章详细讲解了每个函数的功能、参数、返回值,并提供了具体的代码示例,帮助读者更好地理解和掌握这些函数的应用。
18 0
|
存储 程序员 C语言
程序员之路:C语言中存储类别
程序员之路:C语言中存储类别
133 0
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
32 3
|
4天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
19 6
|
24天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
33 10