【C语言】深度剖析数据在内存中的存储

简介: 【C语言】深度剖析数据在内存中的存储

本章重点

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

数据类型介绍

我们已经学习了基本的内置类型

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

他们所占的存储空间大小.特别注意的是long>=int

类型的意义

  1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)
  2. 如何看待内存空间的视角.

类型的基本归类

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

[]当中的内容可以省略

数值

正数和负数:

有些数值只用正数,没有负数(身高)

有些数值有正数也有负数(温度)

浮点型家族

float
double

构造类型

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

数组为什么也是构造类型?

int arr[5]
int arr[6]
char brr[5]

这就说明数组类型是不唯一的,所以也属于构造类型

指针类型

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

空类型

void标识空类型(无类型)

通常应用函数的返回值,函数的参数,指针类型

整型在内存中的存储

一个变量的创建是要在内存中开辟空间的.空间的大小是根据不同的类型决定的,接下来我们讲讲数据在所开辟内存中到底是如何存储的?

比如:

int a = 20;
int b = -10;

我们知道这里分别为a,b开辟四个字节的空间

究竟如何存储,我们首先要了解下面的概念:


原码,反码,补码

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

三种表示方法均有符号位和数值位两部分,符号位都用0表示"正",1表示"负",而数值位正数的原,反,补码相同.

负数的三种表示方法各不相同

原码

直接将数值按照正负数的形式翻译成二进制就可以得到原码

反码

将原码的符号位不变,其它位依次按位取反就可以得到原码

补码

反码加1就可以得到补码

对于整型来说,数据存放到内存中其实存放的是补码

为什么呢?

计算机系统中,数值一律用补码来表示和存储.原因在于,使用补码,可以将符号位和数值域统一处理;

同时,加法和加法也可以统一处理(CPU只有加法器),此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路

原码-->补码
原码符号位不变,其它位按位取反再加1就可以得到补码
补码-->原码
1.补码减1,符号位不变,其它位置取反
2.补码取反加1得到原码

一个数值超过一个字节了,要存储在内存中就会有顺序的问题

假如

0x11223344
这个十六进制数字在内存中到底是怎么存储的呢?,
以下图,到底是哪一个存储方式,首先要了解大端和小端的概念

大端小端介绍

什么是大端小端

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

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

为什么有大端小端

为什么有大小端模式之分那?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元对应一个字节,一个字节8个比特位.但是在C语言中除了8个bite的char之外,还有16个bite的short,另外对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在一个如何将多个字节安排的问题.因此就导致了大端存储模式和小端存储模式.

一个数字权重低的是低位,
例如0x11223344
从右到左权重首先是16^0,16^1,依次升高

小端存储:是指数据的低位保存在内存中的低地址中,数据的高位保存在内存中的高地址

大端存储:是指数据的低位保存在内存中的高地址中,数据的高位保存在内存中的低地址

自己总结的口诀:小同大异

程序设计:

#include <stdio.h>
int main() {
    int a = 1;
    char* p = (char*)&a;
    if(*p == 1) {
        printf("小端\n");
    }else {
        printf("大端\n");
    }
}

思路:

1的十六进制表示为0x00 00 00 01

假设是小端存储就是这个样子:

假设是大端存储,是这个样子:

我们把这个int强转为char只取最低的地址,如果最低位中存放的是1,就是小端存储,如果是0就是大端存储

这里告诉同志们一个题外的知识:

未来在找工作的时候,这一类公司一定要小心:

  1. 你没有投递过这家公司
  2. 但是打电话让你去面试
    这其实都是培训机构,打着招聘的幌子在招生
注意:
1.内存中存放的是补码
2.整型表达式计算使用的内存中补码计算的
3.打印和我们看到的都是原码

练习

第一题
//输出什么
#include <stdio.h>
int main() {
    char a = -1;
    signed char b = -1;
    unsigned char c = -1;
    printf("%d %d %d", a, b, c);
    return 0;
}

又因为题目中定义的是char所以要发生截断,只保留前8个比特位所以-1就变为:

内存中存的也是如上图所示,打印的时候因为是%d,是十进制的整型,所以要发生整型提升,变为:

所以a和b的值都是-1,对于c的整型提升

第二题
#include <stdio.h>
int main() {
    char a = -128;
    printf("%u\n", a);
    return 0;
}
//%u  是打印无符号整型,认为内存中存放的补码对应是一个无符号数
//%d  是打印有符号整型,认为内存中存放的补码对应是一个有符号数 

内存中存放的是:

整型提升后:

最后是无符号打印,所以是

注意

我在看到这道题目的时候,误区整形提升看的是定义的时候,规定的是否是有无符号,不是看的打印时候规定的是有符号还是无符号的,%u打印的时候当成的是无符号,但实际还是一个有符号的数字

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

首先发生截断:

最后结算的结果(其中的补码是整型提升得到的):

又因为这是个正数,所以原码,反码,补码相同

第四题
#include <stdio.h>
int main() {
    int i = -20;
    unsigned int j = 10;
    printf("%d\n", i + j);
    return 0;
}

因为这是个正数,正数的原码,反码,补码相同,

最后将两个数的补码相加,得到后,转化为原码就可以了

最后讲一下,char类型数的范围

这里的二进制都是补码

注意

1.计算机中存的都是补码,显示出来的是原码
2.整型提升的时候看的是定义的时候是否有符号位
3.对于少于4个字节的数据类型,首先要按照int类型来求原码,反码,补码,最后截断存放到对应的地址中
4.正数的原码,反码,补码相同

最后的最后就是整型提升的方法:

整型提升是C程序设计语言中的一项规定:在表达式计算时,各种整形首先要提升为int类型,如果int类型不足以表示则要提升为unsigned int类型;然后执行表达式的运算。

提升规则:如果有符号按照符号补全如(10001111整型提升为11111111111111111111111110001111)

如果无符号整形提升,则直接补0

第五题
#include <stdio.h>
int main() {
    unsigned int i;
    for(i = 9; i >= 0; i--) {
        printf("%u\n",i);
    }
}

这个题目中定义的是一个无符号的i,说明i永远都大于等于0,所以这是一个死循环.

第六题
#include <stdio.h>
#include <string.h>
int main() {
    char a[1000];
    int i;
    for(int i = 0; i < 1000; i ++) {
        a[i] = -1 - i;
    }
    printf("%d\n", strlen(a));
    return 0;
}

我们知道strlen是计算字符串的长度,并且遇到0停止,又因为是char类型,所以只能存放8个比特位,这是一个,看下面这个图片可以知道0到-1之间有225个数字

第七题
#include <stdio.h>
unsigned char i = 0;
int main() {
    for(int i = 0; i <= 255; i ++) {
        printf("hello world!");
    }
    return 0;
}

因为这是一个无符号的i,所以他的最大值只能是255,所以这是一个死循环.

浮点数在内存中的存储

一个例子
#include <stdio.h>
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);
}

运行结果是

至于为什么是这样,我们要先了解浮点数在内存中到底是怎么存储的

浮点数存储规则

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

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

举例来说:

可以看成S = 0 , M = 1.011, E = 2;

IEEE754的规定

对于32的浮点数,最高的一位是符号位S,接着8位是指数E,剩下的23位为有效数字M

对于64位的浮点数,最高的一位是符号位S,接着的11个位置是指数E,剩下的52位为有效数字M

IEEE 754对有效数字M和指数E,还有一些特别的规定

前面说过1<=M<2,也就是说,M可以写成1.xxxxxx的形式,其中xxxxx表示小数部分

IEEE 754规定,在计算机内部保存M时,默认这个数的第一总是1,因此可以被舍去,只保存后面的小数部分,等到读取的时候,再把第一位的1加上去.这样做的目的是为了节省一位有效数字.以32为浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字

至于指数E情况比较复杂

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

这意味着如果E是8位,它的取值范围0~255;如果E是11位它的取值范围0 ~ 2047,但是我们知道科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须加上一个中间数,对于8位的E这个中间数是127,对于11位的E这个中间数是1023.比如2^10,在保存成32位浮点数的时候,必须保存成10+127=137,然后从E内取出还分为3种情况:

E不全为0或E不全为1

这是浮点数就采用下面的规则来表示,即指数E的计算减去127(或1023),得到真实值,再将有效数字M前加上第一位的1

E全为0

这是浮点数的指数E等于1-127(或者1-1023),即为真实值,是一个接近0的很小的数字

E全为1

这是表示正负无穷大

解释前面的例子

9是以整型的规则存进内存,所以表示补码的形式存储

但是如果直接转换为float的形式,就不一样了

这是E全为0的情况,实际的E是-127所以这是一个无穷小的数字,打印的结果就是0

接着题目又以float的形式存入一个9.0,所以用浮点型的规则打印的时候是9.0,但是用整型的规则打印就是这样

这串数字如果用整型来打印就是1091567616,所以才会造成例子中的情况

这里要特别注意不能忘记E在存入的时候要加上一个中间值,否则结果会错误.

看一个情况

当打印后面的时候,好像不能很好的打印出来,这就是因为在凑小数的时候,很难凑整,所以会造成精度丢失.

相关文章
|
2天前
|
存储 程序员 编译器
C 语言中的数据类型转换:连接不同数据世界的桥梁
C语言中的数据类型转换是程序设计中不可或缺的一部分,它如同连接不同数据世界的桥梁,使得不同类型的变量之间能够互相传递和转换,确保了程序的灵活性与兼容性。通过强制类型转换或自动类型转换,C语言允许开发者在保证数据完整性的前提下,实现复杂的数据处理逻辑。
|
5天前
|
传感器 人工智能 物联网
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发,以及面临的挑战和未来趋势,旨在帮助读者深入了解并掌握这些关键技术。
27 6
|
6天前
|
存储 数据建模 程序员
C 语言结构体 —— 数据封装的利器
C语言结构体是一种用户自定义的数据类型,用于将不同类型的数据组合在一起,形成一个整体。它支持数据封装,便于管理和传递复杂数据,是程序设计中的重要工具。
|
12天前
|
存储 编译器 数据处理
C 语言结构体与位域:高效数据组织与内存优化
C语言中的结构体与位域是实现高效数据组织和内存优化的重要工具。结构体允许将不同类型的数据组合成一个整体,而位域则进一步允许对结构体成员的位进行精细控制,以节省内存空间。两者结合使用,可在嵌入式系统等资源受限环境中发挥巨大作用。
35 11
|
2月前
|
监控 算法 应用服务中间件
“四两拨千斤” —— 1.2MB 数据如何吃掉 10GB 内存
一个特殊请求引发服务器内存用量暴涨进而导致进程 OOM 的惨案。
|
1月前
|
C语言
【c语言】动态内存管理
本文介绍了C语言中的动态内存管理,包括其必要性及相关的四个函数:`malloc`、``calloc``、`realloc`和`free`。`malloc`用于申请内存,`calloc`申请并初始化内存,`realloc`调整内存大小,`free`释放内存。文章还列举了常见的动态内存管理错误,如空指针解引用、越界访问、错误释放等,并提供了示例代码帮助理解。
45 3
|
2月前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
64 1
|
4天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
18 1
|
6月前
|
程序员 C语言 C++
【C语言基础】:动态内存管理(含经典笔试题分析)-2
【C语言基础】:动态内存管理(含经典笔试题分析)
|
6月前
|
程序员 编译器 C语言
【C语言基础】:动态内存管理(含经典笔试题分析)-1
【C语言基础】:动态内存管理(含经典笔试题分析)