数组基础
从内存的角度学习数组的基础知识。
简介
到目前为止,我们使用的是独立的变量。在本节课中,我们将把前四章介绍的信息应用到数组上。
作用域
数组的作用域遵循与变量相同的原则。
存在全局作用域和局部作用域:
- 在全局作用域中的数组在整个程序中都可见。
- 在局部作用域中的数组仅在声明它的函数内部可见。
- 全局数组如果未初始化,则存储在
.bss段中;如果已初始化,则存储在.data段中。 - 局部非静态数组存储在栈上,并在函数返回时通过栈清理过程被清除。
- 局部静态数组存储在
.data或.bss段中,且仅在声明它的函数内部可见(即具有有限作用域的全局变量)。
查看以下代码,其中定义了多个数组。我们将探索它们存储在哪些段中。
代码中的第4行、第5行和第9行有注释,我们将逐个取消注释,观察输出的变化。
注意: 我们忽略十六进制值和文件名列,因为它们不重要。
1#include <stdio.h> 2#include <stdlib.h> 3 4//int globalArr[5]; 5//int globalArr2[5] = {1,2,3,4}; 6 7int main() 8{ 9 //int localArr[5]; 10 11 system("size main"); 12 return 0; 13}
运行代码以获取基准数据:
1 text data bss dec 2 1186 560 8 1754
取消注释第4行,重新运行代码:
1 text data bss dec 2 1186 560 40 1786
globalArr 是一个全局未初始化数组。它将被存储在 .bss 段中,程序启动时会被默认初始化为 0。
注意只有 .bss 段的大小发生了变化。
注释掉第4行,取消注释第5行,重新运行代码:
1 text data bss dec 2 1186 580 4 1770
globalArr2 是一个全局已初始化数组。编译器会将其存储在 .data 段中。注意只有 .data 段的大小发生了变化。
注释掉第5行,取消注释第9行,重新运行代码:
1 text data bss dec 2 1186 560 8 1754
注意没有任何段的大小发生变化,因为 localArr 存储在栈上,不会影响 .data 和 .bss 段。
我们可以通过以下代码进一步探讨已初始化和未初始化全局数组之间的区别。我们保留之前的定义并打印它们的内容。
注意:未初始化的数组会默认被初始化为 0。
1#include <stdio.h> 2 3int globalArr[5]; 4int globalArr2[5] = {1,2,3,4}; 5 6int main() 7{ 8 printf("未初始化全局数组的内容: "); 9 for(int i = 0; i < 5; i++) 10 { 11 printf("%d ", globalArr[i]); 12 } 13 printf("\n"); 14 15 printf("已初始化全局数组的内容: "); 16 for(int i = 0; i < 5; i++) 17 { 18 printf("%d ", globalArr2[i]); 19 } 20 printf("\n"); 21 22 return 0; 23}
数组大小
回想一下,变量的大小等于其数据类型的大小。
我们使用 sizeof 操作符来获取这个大小。
在下面的代码中,我们计算整型变量 x 的大小。
1#include <stdio.h> 2 3int main() 4{ 5 int x; 6 printf("x 的大小: %u 字节\n", sizeof(x)); 7 return 0; 8}
注意: 数组的大小是其数据类型大小乘以数组元素的数量。
这很自然,可以这样理解:
假设我们有如下代码:
1int x, y, z, w;
每个变量占用 4 字节,共 4 个变量,因此总共占用 4 × 4 = 16 字节。
我们可以把数组看作一组变量的集合,从而得出上述规则。
1int arr[4]; 2// 可以视为 3arr[0], arr[1], arr[2], arr[3] 4 4b 4b 4b 4b
4 × 4 = 16 字节
让我们通过代码来验证这一点:声明两个数组并调用 sizeof:
1#include <stdio.h> 2 3int main() 4{ 5 int arr[4]; 6 double arr2[10]; 7 printf("arr 的大小: %u 字节\n", sizeof(arr)); 8 printf("arr2 的大小: %u 字节\n", sizeof(arr2)); 9 return 0; 10}
- 对于
arr,类型是int,int的大小为 4 字节,数组有 4 个元素,因此总大小为 4 × 4 = 16 字节。 - 对于
arr2,类型是double,double的大小为 8 字节,数组有 10 个元素,因此总大小为 8 × 10 = 80 字节。
练习
利用我们刚刚学到的知识,尝试编写一个宏来获取数组中的元素数量。在查看答案之前,请拿一张纸,从数组大小公式出发,尝试推导出数组元素数量的公式。
数组大小的公式为:
1ARRAY_SIZE = sizeof(类型) × 元素数量
从中解出元素数量:
1元素数量 = ARRAY_SIZE / sizeof(类型)
而 ARRAY_SIZE 是通过 sizeof(array) 计算得到的:
1元素数量 = sizeof(array) / sizeof(类型)
我们可以写成这样:
1#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(类型))
但问题在于:在宏内部如何知道 arr 的类型(及其大小)?我们可以为不同类型创建多个函数,如 intArrSize、doubleArrSize,但这既繁琐又难以维护。
如果我们把数组看作一组变量,那么数组中的每一个元素(array[i])都是与数组相同类型的变量。
例如:
1int arr[5]; 2// arr[0] 是一个 int 3// arr[1] 是一个 int 4... 5// arr[4] 是一个 int
由于每个数组至少有一个元素,我们可以保证 arr[0] 存在,并且其类型与数组相同。
这里所说的“相同类型”指的是基础数据类型相同——虽然数组类型是 int[5],而变量类型是 int,但我们关注的是基本数据类型。
因此,我们的基本宏变为:
1#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0]))
现在,sizeof(arr[0]) 给出了数组中一个元素的大小(在本例中是 int)。
1#include <stdio.h> 2 3#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0])) 4 5int main() 6{ 7 int arr[4]; 8 double arr2[10]; 9 printf("arr 的大小: %ld 个元素\n", ARRAY_SIZE(arr)); 10 printf("arr2 的大小: %ld 个元素\n", ARRAY_SIZE(arr2)); 11 return 0; 12}
效果完美!
注意: 使用宏时要小心,避免在宏中编写复杂的表达式,否则可能引入难以追踪的错误。
内存布局
让我们画一张栈上数组的内存示意图。我们绘制如下函数:
1func() 2{ 3 int x; 4 int arr[3]; 5}
忽略返回类型或可能的参数,这些目前并不重要。
正如我们在获取大小时所见,数组被分配为一块连续的内存区域,足以容纳所有元素。
它并不是作为栈上的独立变量分别分配的。