有几种不同类型的数组,具体取决于它们如何声明和在哪里声明。
固定长度数组
固定长度数组必须在编译时确定其大小。一旦定义,就无法更改固定长度数组的大小。
固定长度数组可以通过以下方式之一进行声明:
T a[N];
T a[N] = { };
char_type a[N] = "string literal";
T a[] = { };
char_type a[] = "string literal";
在前三种情况下,
N
必须是一个常量表达式,其值必须在编译时已知。在前三种情况下,数组的大小取自
N
;在后两种情况下,它取自初始化列表中的元素数或字符串字面值的大小。
固定长度数组的初始内容取决于其存储期限和是否提供了初始化程序。
如果数组具有静态存储期(意味着它在任何函数体外的文件作用域中声明,或者带有static关键字),并且没有提供初始化程序,则所有数组元素都将初始化为0(对于标量)或NULL(对于指针)。如果T是聚合类型,例如结构体或数组类型,则聚合的每个成员都将初始化为0或NULL。联合类型也将被清零。
如果数组具有自动存储期(意味着它在没有static关键字的函数或块内部声明),并且没有提供初始化程序,则数组的内容是不确定的-基本上是垃圾。
如果数组使用初始化列表声明(无论存储期如何),则数组元素的初始值与初始化程序相对应。如果初始化程序中的元素比数组少(例如,N为10,但您只初始化了前5个元素),则剩余的元素将初始化为静态存储期的数组。也就是说,给定声明
int a[10] = {0, 1, 2}
如果数组没有显式初始化,那么它的初始内容为
{0, 1, 2, 0, 0, 0, 0, 0, 0, 0}
。
包含字符串值的定长数组可以使用字符串字面量进行初始化。C语言支持“宽字符”字符串,因此
char_type
可以是
char
或
wchar_t
。对于常规初始化器列表,规则相同,只是
N
(如果指定)必须至少比字符串长度多1,以便考虑到字符串终止符。
这意味着:
char a[10] = "test";
将被初始化为{'t', 'e', 's', 't', 0, 0, 0, 0, 0, 0}
char a[] = "test";
将被初始化为{'t','e','s','t',0}
。
静态存储期数组的存储方式使它们在程序加载时立即可用,并且直到程序退出才被释放。这通常意味着它们存储在类似于.data
或.bss
之类的内存段中(或者对于您的系统使用的任何可执行文件格式的等效处理方式)。
自动存储期数组的存储方式是在块或函数进入时分配和在块或函数退出时释放(实际上,不管它们是否被限制在函数内的较小范围内,它们可能会在函数进入时分配)- 这通常转换为堆栈,虽然它不必须如此。
变长数组
变长数组是在C99中添加的 - 它们的行为与固定长度数组大多数相似,除了它们的大小是在运行时确定的; N
不必是编译时常量表达式:
int n;
printf( "gimme the array size: ");
scanf( "%d", &n );
T a[n]; // for any type T
与其名称所暗示的相反,在变长数组定义后,您无法更改其大小。 "变长"只是意味着大小在编译时不固定,并且可以从定义到定义发生变化。
由于它们的大小直到运行时才确定,因此不能在文件作用域或使用
static
关键字声明变长数组,也不能使用初始化程序列表声明变长数组。 VLAs的空间管理方式完全取决于实现;它可能(通常也是)来自堆栈,但据我所知,也可能来自其他地方。
动态数组实际上并不是如我们所说的"数组",至少在我们用来管理它们的对象的数据类型方面不是。 动态数组是在运行时使用
malloc
、
calloc
或
realloc
之一分配的,而该存储保持到调用
free
为止。
T *p = malloc( sizeof *p * N ); // where N may be either a compile-time or
// run-time expression
...
free( p );
可以使用 realloc
库函数来重新调整动态数组的大小,如下所示:
T *tmp = realloc( p, sizeof *p * new_size );
if ( tmp )
p = tmp;
虽然数组元素本身的内存来自堆(或其他动态内存池),但指针变量 p
的内存将根据其存储期限 (static
或 auto
) 从 .bss
或 .data
段或堆栈中分配。
使用 malloc
或 realloc
分配的内存未被初始化,该内存的内容将为不确定值。使用 calloc
分配的内存将被初始化为零。
数组 vs. 指针
在某些时候,有人会告诉你:“数组只是一个指针”。那个人是不正确的。
当你声明一个数组(无论是定长还是变长),会为该数组的元素和 仅此而已 分配足够的存储空间,不会为任何元数据(如数组长度或指向第一个元素的指针)分配存储空间。给定以下声明:
T a[N];
那么存储看起来会像这样:
+---+
a: | | a[0]
+---+
| | a[1]
+---+
| | a[2]
+---+
...
+---+
| | a[N-1]
+---+
除了数组元素本身(或者更准确地说,对象a是数组的元素),没有单独的对象a,表达式a不能作为赋值的目标。
但是……
表达式a[i]被定义为*(a+i);也就是说,给定指针值a,从该地址偏移i个元素(而不是字节!),然后解引用结果。但如果a不是指针,那怎么行呢?
像这样——除非它是sizeof或一元&运算符的操作数,或者是在声明中用作数组初始化程序的字符串文字,类型为“N个元素的T数组”的表达式将被转换(“衰减”)为类型为“指向T的指针”的表达式,并且表达式的值将是数组的第一个元素的地址。
这有几个含义:
- 表达式a、&a和&a[0]将产生相同的值(数组的第一个元素的地址),但表达式的类型将不同(T*、T(*)[N]和T*);
- 下标运算符[]对数组表达式和指针表达式都适用(实际上,它被定义为在指针表达式上工作);
- 当你将数组表达式传递给函数时,你实际上传递的是指针值,而不是整个数组;
对于动态数组,情况是不同的。给定以下代码行:
T *p = malloc( sizeof *p * N );
那么你的存储看起来会像这样:
+---+
p: | | ---+
+---+ |
... |
+------+
|
V
+---+
| | p[0]
+---+
| | p[1]
+---+
...
+---+
| | p[N-1]
+---+
在这种情况下,
p
是一个单独的对象,与数组不同。因此,
&p
不会给你与
p
和
&p[0]
相同的值,并且它的类型将是
T **
而不是
T (*)[N]
。另外,由于
p
只是一个指针变量,所以你可以给它赋一个新值(尽管如果你在没有首先使用
free
释放它指向的内存的情况下这样做,你将创建一个内存泄漏)。
类似地,
sizeof p
不会像
sizeof a
一样工作;它只会返回指针变量的大小,而不是指针指向的分配内存的大小。
int *m_array
是一个指针(Pointer),而不是一个数组(Array)。 - Michiint array[size]
是一个可变长数组。此外,您可能会对realloc
函数感兴趣。 - user3386109malloc
函数创建一个数组。使用malloc
函数可以向操作系统请求一定量的内存,如果成功获取到内存,则将指针指向该内存地址。 - Michimalloc
创建一个数组。 - M.Mmalloc
创建数组。 - Johann Gerell