彻底理解 C 语言的数组在内存中到底是怎么存放的!

2024-06-15 10:12

本文主要是介绍彻底理解 C 语言的数组在内存中到底是怎么存放的!,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

图片

在C语言中,数组是经常被用到的重要数据类型,但在实际使用时,往往有很多工程师会出现各种各样的问题,如内存越界、错误的访问、初始化不当等。这其中有很大一个原因是没有彻底理解数组的存储机制,出现了一些非法地址或者非预期元素的访问和引用。因此,今天就来详细讲一下在C语言中,数组到底是怎么存储的。

首先我们来看一个一维数组:

int array[10] = {0};

这是最简单的数组,其内存结构也是最容易理解的,编译器会在内存中划出一段连续的空间用于存储这个数组的元素,并且对于 int 类型来说,每个元素占用的大小为 4 字节。因此,其内存排列如下:

图片

我们可以用下面一段代码验证一下:

#include <stdio.h>int main() {int array[10] = {0};for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++) {printf("array[%d] - %p - %ld\r\n", i, &array[i], (&array[i] - array) * 4);}return 0;
}

上面这段代码会将数组每个元素的元素名,实际地址和相对地址偏移打印出来,我们将其运行,并得到如下结果:

jay@jaylinuxlenovo:~/test$ ./test2 
array[0] - 0x7ffc729ca960 - 0
array[1] - 0x7ffc729ca964 - 4
array[2] - 0x7ffc729ca968 - 8
array[3] - 0x7ffc729ca96c - 12
array[4] - 0x7ffc729ca970 - 16
array[5] - 0x7ffc729ca974 - 20
array[6] - 0x7ffc729ca978 - 24
array[7] - 0x7ffc729ca97c - 28
array[8] - 0x7ffc729ca980 - 32
array[9] - 0x7ffc729ca984 - 36

可以看到输出结果与我们分析的一致,array 数组在内存中处于连续排列,其中下标0为低地址,随着下标的增大,内存地址增大,每次增大的步长为一个元素所占的大小,在这里就是整形大小4个字节。

当然,上面只是一种最简单的情况,相信无须讲解大家都能理解。那么接下来上正题:

多维数组在内存中的排布是什么样的呢?

实践是检验真理的唯一标准,我们直接来写一段代码测试一下:

#include <stdio.h>int main() {int array[4][3] = {0};for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++) {for (int j = 0; j < sizeof(array[0]) / sizeof(array[0][0]); j++) {printf("array[%d][%d] - %p - %ld\r\n", i, j, &array[i][j], (&array[i][j] - &array[0][0]) * 4);}}return 0;
}

以上程序定义了一个四行三列的二维数组:

图片

测试代码中我们以行为主序,依次打印出每一行中每一列的元素信息,到行结尾后再次从下一行开始,直到结束。可以看到运行结果如下:

jay@jaylinuxlenovo:~/test$ ./test2 
array[0][0] - 0x7fff25495070 - 0
array[0][1] - 0x7fff25495074 - 4
array[0][2] - 0x7fff25495078 - 8
array[1][0] - 0x7fff2549507c - 12
array[1][1] - 0x7fff25495080 - 16
array[1][2] - 0x7fff25495084 - 20
array[2][0] - 0x7fff25495088 - 24
array[2][1] - 0x7fff2549508c - 28
array[2][2] - 0x7fff25495090 - 32
array[3][0] - 0x7fff25495094 - 36
array[3][1] - 0x7fff25495098 - 40
array[3][2] - 0x7fff2549509c - 44

很巧,我们选用的打印顺序竟然就是数组内存中排列的顺序!可以看到这里的内存递增方式和一开始的一维数组一样,是按照一个整形元素的大小递增的。当然为了严谨起见,我们调换一下程序中的打印顺序,按列为主序打印:

#include <stdio.h>int main() {int array[4][3] = {0};for (int j = 0; j < sizeof(array) / sizeof(array[0]); j++) {for (int i = 0; i < sizeof(array[0]) / sizeof(array[0][0]); i++) {printf("array[%d][%d] - %p - %ld\r\n", i, j, &array[i][j], (&array[i][j] - &array[0][0]) * 4);}}return 0;
}

可以看到与上例的程序相比,i 和 j 的位置对调了。此时我们看一下运行结果:

jay@jaylinuxlenovo:~/test$ ./test2 
array[0][0] - 0x7ffccc482230 - 0
array[1][0] - 0x7ffccc48223c - 12
array[2][0] - 0x7ffccc482248 - 24
array[0][1] - 0x7ffccc482234 - 4
array[1][1] - 0x7ffccc482240 - 16
array[2][1] - 0x7ffccc48224c - 28
array[0][2] - 0x7ffccc482238 - 8
array[1][2] - 0x7ffccc482244 - 20
array[2][2] - 0x7ffccc482250 - 32
array[0][3] - 0x7ffccc48223c - 12
array[1][3] - 0x7ffccc482248 - 24
array[2][3] - 0x7ffccc482254 - 36

不出所料,此时内存的偏移值不再是顺序增加。并且仔细计算可以发现,同一列的元素如 array[0][0] 和 array[1][0] 之间差了3个整形元素所占的大小 - 12字节,而这正好是一行元素的整体大小。

显然,我们可以得到一个结论:在二维数组中,数组元素是按照行的主序来存储的,也就是内存按行分配,一行分配完再分配下一行,并且是连续的。分配的过程我们可以用下图来形象的表示:

图片

分配完成后即可得到最终的内存排列:

图片

实际上,这种存储方式有一种专业的名词:行优先存储(Row-major order)。如果仔细观察上面的元素地址偏移,可以发现这种存储方式最显著的特点就是先把位于右侧的下标排满

这个特点可以让我们拓展到更高维的数组存储方式。

如果我们定义一个三维数组 array[x][y][z],那么其在内存中的排列方式就是先将z维排满,再将y维排满,最后将x维排满。为了加深理解,我们再结合实际的代码:

#include <stdio.h>int main() {int array[2][3][4] = {0};for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++) {for (int j = 0; j < sizeof(array[0]) / sizeof(array[0][0]); j++) {for (int k = 0; k < sizeof(array[0][0]) / sizeof(array[0][0][0]); k++) {printf("array[%d][%d][%d] - %p - %ld\r\n", i, j, k, &array[i][j][k],(&array[i][j][k] - &array[0][0][0]) * 4);}}}return 0;
}

代码中拓展到了一个三维数组,其内存结构如下:

jay@jaylinuxlenovo:~/test$ ./test2 
array[0][0][0] - 0x7fffef296740 - 0
array[0][0][1] - 0x7fffef296744 - 4
array[0][0][2] - 0x7fffef296748 - 8
array[0][0][3] - 0x7fffef29674c - 12
array[0][1][0] - 0x7fffef296750 - 16
array[0][1][1] - 0x7fffef296754 - 20
array[0][1][2] - 0x7fffef296758 - 24
array[0][1][3] - 0x7fffef29675c - 28
array[0][2][0] - 0x7fffef296760 - 32
array[0][2][1] - 0x7fffef296764 - 36
array[0][2][2] - 0x7fffef296768 - 40
array[0][2][3] - 0x7fffef29676c - 44
array[1][0][0] - 0x7fffef296770 - 48
array[1][0][1] - 0x7fffef296774 - 52
array[1][0][2] - 0x7fffef296778 - 56
array[1][0][3] - 0x7fffef29677c - 60
array[1][1][0] - 0x7fffef296780 - 64
array[1][1][1] - 0x7fffef296784 - 68
array[1][1][2] - 0x7fffef296788 - 72
array[1][1][3] - 0x7fffef29678c - 76
array[1][2][0] - 0x7fffef296790 - 80
array[1][2][1] - 0x7fffef296794 - 84
array[1][2][2] - 0x7fffef296798 - 88
array[1][2][3] - 0x7fffef29679c - 92

掌握了上述方法后,即使是四维,五维甚至更高维的数组,也能立刻搞明白其内存排列的方式。相信在后续处理数组的场合中,大家能够根据数组内存的排列方式写出更高效、稳健的代码。

图片

这篇关于彻底理解 C 语言的数组在内存中到底是怎么存放的!的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1063158

相关文章

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

C语言中的数据类型强制转换

《C语言中的数据类型强制转换》:本文主要介绍C语言中的数据类型强制转换方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C语言数据类型强制转换自动转换强制转换类型总结C语言数据类型强制转换强制类型转换:是通过类型转换运算来实现的,主要的数据类型转换分为自动转换

利用Go语言开发文件操作工具轻松处理所有文件

《利用Go语言开发文件操作工具轻松处理所有文件》在后端开发中,文件操作是一个非常常见但又容易出错的场景,本文小编要向大家介绍一个强大的Go语言文件操作工具库,它能帮你轻松处理各种文件操作场景... 目录为什么需要这个工具?核心功能详解1. 文件/目录存javascript在性检查2. 批量创建目录3. 文件

C语言实现两个变量值交换的三种方式

《C语言实现两个变量值交换的三种方式》两个变量值的交换是编程中最常见的问题之一,以下将介绍三种变量的交换方式,其中第一种方式是最常用也是最实用的,后两种方式一般只在特殊限制下使用,需要的朋友可以参考下... 目录1.使用临时变量(推荐)2.相加和相减的方式(值较大时可能丢失数据)3.按位异或运算1.使用临时

使用C语言实现交换整数的奇数位和偶数位

《使用C语言实现交换整数的奇数位和偶数位》在C语言中,要交换一个整数的二进制位中的奇数位和偶数位,重点需要理解位操作,当我们谈论二进制位的奇数位和偶数位时,我们是指从右到左数的位置,本文给大家介绍了使... 目录一、问题描述二、解决思路三、函数实现四、宏实现五、总结一、问题描述使用C语言代码实现:将一个整

C++原地删除有序数组重复项的N种方法

《C++原地删除有序数组重复项的N种方法》给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度,不要使用额外的数组空间,你必须在原地修改输入数组并在使用O(... 目录一、问题二、问题分析三、算法实现四、问题变体:最多保留两次五、分析和代码实现5.1、问题分析5.

C语言字符函数和字符串函数示例详解

《C语言字符函数和字符串函数示例详解》本文详细介绍了C语言中字符分类函数、字符转换函数及字符串操作函数的使用方法,并通过示例代码展示了如何实现这些功能,通过这些内容,读者可以深入理解并掌握C语言中的字... 目录一、字符分类函数二、字符转换函数三、strlen的使用和模拟实现3.1strlen函数3.2st

Go语言中最便捷的http请求包resty的使用详解

《Go语言中最便捷的http请求包resty的使用详解》go语言虽然自身就有net/http包,但是说实话用起来没那么好用,resty包是go语言中一个非常受欢迎的http请求处理包,下面我们一起来学... 目录安装一、一个简单的get二、带查询参数三、设置请求头、body四、设置表单数据五、处理响应六、超

电脑开机提示krpt.dll丢失怎么解决? krpt.dll文件缺失的多种解决办法

《电脑开机提示krpt.dll丢失怎么解决?krpt.dll文件缺失的多种解决办法》krpt.dll是Windows操作系统中的一个动态链接库文件,它对于系统的正常运行起着重要的作用,本文将详细介绍... 在使用 Windows 操作系统的过程中,用户有时会遇到各种错误提示,其中“找不到 krpt.dll”

C语言中的浮点数存储详解

《C语言中的浮点数存储详解》:本文主要介绍C语言中的浮点数存储详解,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、首先明确一个概念2、接下来,讲解C语言中浮点型数存储的规则2.1、可以将上述公式分为两部分来看2.2、问:十进制小数0.5该如何存储?2.3 浮点