【C语言进阶】数据在内存中的存储

在这里插入图片描述


和光同尘_我的个人主页

且视他人之疑目如盏盏鬼火,大胆地去你的夜路。 --《病隙碎笔》史铁生

🕯️前言

这一章节的我们主要探究数组在内存中如何进行存储、运算,还是挺简单的,那我们就开始吧⌨️

1. 整形

1.1.整形家族

  • 整形家族的类型有有符号和无符号的区分

char (字符存储的是ASCII码值,是整形)
  unsigned char
  signed char
short
  unsigned short [int]
  signed short [int]
int
   unsigned int
   signed int
long
  unsigned long [int]
  signed long [int]
long long

整形表示的范围在<limits.h>中定义:

#define SHRT_MIN    (-32768)
#define SHRT_MAX      32767
#define USHRT_MAX     0xffff

#define INT_MIN     (-2147483647 - 1)
#define INT_MAX       2147483647
#define UINT_MAX      0xffffffff

#define LONG_MIN    (-2147483647L - 1)
#define LONG_MAX      2147483647L
#define ULONG_MAX     0xffffffffUL

#define LLONG_MAX     9223372036854775807i64
#define LLONG_MIN   (-9223372036854775807i64 - 1)
#define ULLONG_MAX    0xffffffffffffffffui64
  • 从中可以看出,对于short、int、long、longlong类型,VS中都默认为singned
  • 比较特殊的是char类型,<limit.h>中是这样定义的
#define SCHAR_MIN   (-128)
#define SCHAR_MAX     127
#define UCHAR_MAX     0xff

#ifndef _CHAR_UNSIGNED
    #define CHAR_MIN    SCHAR_MIN
    #define CHAR_MAX    SCHAR_MAX
#else
    #define CHAR_MIN    0
    #define CHAR_MAX    UCHAR_MAX
#endif
  • 其中#ifndef _CHAR_UNSIGNED说明宏_CHAR_UNSIGNED不存在 则编译#ifndef#endif直接的代码

为什么会有这种定义呢?

  • ANSI C 提供了3种字符类型,分别是char、signed char、unsigned char。而不是像short、int一样只有两种(int默认就是unsigned int).
    C标准中对char是 Impementation Defined,即未明确定义,不同的编译器对char有不同定义
    VS编译器、x86上的GCC定义char为signed char
    而arm-linux-gcc却把char定义为unsigned char

2.2. 整形在内存中的存储

2.2.1. 原码、反码、补码

计算机中的整数有三种2进制表示方法,即原码、反码和补码
三种表示方法均由符号位数值位两部分,符号位用0表示“正”,用1表示“负”
数值位:
  正数的原、反、补码都相同
  负整数的三种表示方法各不相同
  通过补码获得原码可以-1后取反,也可以取反后+1

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

举个栗子:

  • 对于整形来说:数据存放内存中其实存放的是补码

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

cpu中只有加法器,那怎么计算相减呢
我们以1-1为例,实际计算的是1-(-1)

1+(-1)
   00000000 00000000 00000000 00000001 --> 1的补码
   11111111 11111111 11111111 11111111 --> -1的补码
   //相加
   00000000 00000000 00000000 00000000 --> 等于0

2.2.2. 大小端

为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元 都对应着一个字节,一个字节为8 bit。但是在C语言中除了8 bit的char之外,还有16 bit的short 型,32 bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32 位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就出现了大端存储模式和小端存储模式。

字节序-以字节为单位讨论存储顺序
小端字节序存储:把一个数据的低位字节的内容存放在低地址处高位字节内容存放在高地址处
大端字节序存储:把一个数据的低位字节的内容存放在高地址处高位字节内容存放在低地址处
在这里插入图片描述

  • 如何判断当前环境下的大小端?

我们定义一个int类型的数据,初始化为1
那么它在内存中的存储有以下两种情况
在这里插入图片描述
而后我们将其强制类型转换为char类型,打印
在这里插入图片描述
由此可以看出,如果打印1,就是小端存储;打印0,就是大端存储

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

2.2.3. 几道练习题

下面的几个程序输出什么?

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=255
是不是有点怪?

  • 虽然我们初始化的都是-1,但是abc的数据类型不同,我们打印时按照%d的格式,即十进制的形式打印有符号整数,所以才会有这样的差异

  • -1的原码、反码、补码分别是
    1000000 00000000 0000000 0000001
    11111111 111111111 11111111 11111110
    11111111 111111111 11111111 11111111

  • 而要存放在char类型的数据中,char类型只有8个字节,所以会发生截断
    11111111 -a、b、c、中存储的补码

  • 前面我们说过VS编译器定义char为signed char
    %d:十进制的形式打印有符号整数打印char类型的数据,会进行整形提升
    对于a和b来说都是signed char,整形提升符号位为1,高位补1

    11111111111111111111111111111111 –a、b整形提升后的补码
    所以a、b打印的是-1

  • 而c是unsigned char,高位补0
    00000000000000000000000011111111 –c整形提升后的补码
    所以c打印的为255

#include <stdio.h>
int main()
{
  char a = -128;
  printf("%u\n",a);
  return 0;
}

答案是:4294967168
是不是比上一道更离谱了🤣

  • -128的原码、反码、补码:
    10000000 00000000 00000000 10000000
    111111111 111111111 111111111 011111111
    111111111 111111111 111111111 10000000

  • 存放在char类型的数据中,发生截断
    10000000

  • %u:无符号十进制整数打印,a是signed char,高位补符号位,整形提升
    111111111 111111111 111111111 10000000 –补码
    以无符号形式打印,所以补码也是原码,对应结果就是 4294967168

unsigned char i;
for(i = 9; i >= 0; i--)
{
    printf("%u ",i);
}

在这里插入图片描述

通过前面两题相信大家已经能猜到这种情况了

  • i是无符号的整形,不会小于0,死循环

那为什么等于0后继续减1得到的是255呢?
在这里插入图片描述

  • 借助这两张图,我们就能快速理解char的数据会在什么时候发生变化了

2. 浮点型数据

常见的浮点数:

3.14159
1E10
浮点数家族包括: float、double、long double 类型。
浮点数表示的范围:float.h中定义

2.1. 浮点数存储规则

根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
(-1)^S * M * 2^E

  • (-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数。
    M表示有效数字,大于等于1,小于2。
    2^E表示指数位。

对于32位的浮点数,最高的1位是符号位S,接着的8位是指数E,剩下的23位为有效数字M。
在这里插入图片描述
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。

在这里插入图片描述
指数E从内存中取出还可以再分成三种情况

  • E不全为0或不全为1

这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将 有效数字M前加上第一位的1。 比如: 0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为 1.0*2^(-1),其阶码为-1+127=126,表示为 01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进 制表示形式为:

  • E全为0

这时,浮点数的指数E等于1-127(或者1-1023)即为真实值, 有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于 0的很小的数字。

  • E全为1

这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)

2.2. 浮点数存储的例子

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;
}

我们定义一个int类型的变量n并初始化为9
再将其取地址,强制类型转化成 一个folat* 类型的指针pFloat
分别打印n的值和pFloat指向的值
此时打印的结果:

n的值为:9
*pFloat的值为:0.000000

  • n在内存中的存储:
    0 00000000 00000000000000000001001
  • 对应float类型的S E M
    0 00000000 00000000000000000001001
    S E        M
    0 -126     0.00000000000000000001001
  • 表示的浮点数即为:
    (-1)^0 * 0.00000000000000000001001 * 2^-126
  • E在内存中是全0,所以表示的就是±0

而后我们赋值*pFloat = 9.0;
此时打印的结果:

n的值为:1091567616
*pFloat的值为:9.000000

  • 浮点数形式表示9
    1001.0
    1.001 * 2^3
  • 对应的S E M:
    (-1)^0 * 1.001 * 2^3
    S=0 E=3 M=1.001
  • *pFloat在内存中的存储:
    0 10000010 00100000000000000000000

🗝️总结

  • 这样的话大家是不是就对程序内存中的运行过程理解更深了呢😎
本节完~~,如果你在实现过程中遇到任何问题,欢迎在评论区指出或者私信我!💕

新人博主创作不易,如果有收获可否👍点赞✍评论⭐收藏一下?O(∩_∩)O

THANKS FOR WATCHING
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

和光同尘Viloet

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值