Array declaration
数组是一种由连续分配的非空对象序列组成的类型,这些对象具有特定的 element type 。这些对象的数量(数组大小)在数组生命周期内永不改变。
目录 |
语法
在数组声明的 声明语法 中, type-specifier 序列指定了 element type (必须是完整对象类型),而 declarator 具有以下形式:
[
static
(可选)
限定符
(可选)
表达式
(可选)
]
属性说明符序列
(可选)
|
(1) | ||||||||
[
限定符
(可选)
static
(可选)
表达式
(可选)
]
属性说明符序列
(可选)
|
(2) | ||||||||
[
限定符
(可选)
*
]
属性说明符序列
(可选)
|
(3) | ||||||||
| 表达式 | - | 除 逗号运算符 外的任意表达式,用于指定数组中的元素数量 |
| 限定符 | - |
const
、
restrict
或
volatile
限定符的任意组合,仅允许在函数参数列表中使用;用于限定该数组参数转换后的指针类型
|
| 属性说明序列 | - | (C23) 可选的 属性 列表,应用于所声明的数组 |
float fa[11], *afp[17]; // fa 是一个包含11个浮点数的数组 // afp 是一个包含17个指向浮点数指针的数组
说明
数组类型存在几种变体:已知常量大小的数组、变长数组以及未知大小的数组。
常量已知大小数组
如果数组声明符中的 expression 是一个值大于零的 整型常量表达式 ,且元素类型是具有已知常量大小的类型(即元素不是可变长度数组) (C99起) ,则该声明符声明一个常量已知大小的数组:
int n[10]; // 整型常量是常量表达式 char o[sizeof(double)]; // sizeof 是常量表达式 enum { MAX_SZ=100 }; int n[MAX_SZ]; // 枚举常量是常量表达式
已知大小的常量数组可以使用 array initializers 来提供初始值:
int a[5] = {1,2,3}; // 声明被初始化为1,2,3,0,0的int[5]数组 char str[] = "abc"; // 声明被初始化为'a','b','c','\0'的char[4]数组
|
在函数参数列表中,数组声明符内允许使用额外的语法元素:关键字
在每次对函数的
函数调用
中,若数组参数在
void fadd(double a[static 10], const double b[static 10]) { for (int i = 0; i < 10; i++) { if (a[i] < 0.0) return; a[i] += b[i]; } } // 对fadd的调用可执行编译时边界检查 // 同时允许诸如预取10个double等优化 int main(void) { double a[10] = {0}, b[20] = {0}; fadd(a, b); // 正确 double x[5] = {0}; fadd(x, b); // 未定义行为:数组参数过小 } 如果存在 qualifiers ,它们将限定数组参数类型转换后的指针类型: int f(const int a[20]) { // 在此函数中,a 的类型为 const int*(指向 const int 的指针) } int g(const int a[const 20]) { // 在此函数中,a 的类型为 const int* const(指向 const int 的 const 指针) }
这通常与
void fadd(double a[static restrict 10], const double b[static restrict 10]) { for (int i = 0; i < 10; i++) // 循环可被展开和重排序 { if (a[i] < 0.0) break; a[i] += b[i]; } } 变长数组如果 expression 不是 整数常量表达式 ,则该声明符用于声明一个可变长度数组。 每当控制流经过该声明时, expression 会被求值(且必须始终得到一个大于零的值),同时数组会被分配(相应地,当声明离开作用域时,VLA的 lifetime 结束)。每个VLA实例的大小在其生命周期内不会改变,但在再次经过相同代码时,可能会分配不同大小的内存。
运行此代码
#include <stdio.h> int main(void) { int n = 1; label:; int a[n]; // 重新分配10次,每次具有不同大小 printf("The array has %zu elements\n", sizeof a / sizeof *a); if (n++ < 10) goto label; // 离开VLA的作用域将结束其生命周期 }
如果大小为
可变长度数组及其派生类型(指向它们的指针等)通常被称为"可变修改类型"(VM)。任何可变修改类型的对象只能在块作用域或函数原型作用域中声明。 extern int n; int A[n]; // 错误:文件作用域的VLA extern int (*p2)[n]; // 错误:文件作用域的VM int B[100]; // 正确:常量已知大小的文件作用域数组 void fvla(int m, int C[m][m]); // 正确:原型作用域的VLA VLA必须具有自动或已分配的存储期。指向VLA的指针(而非VLA本身)也可以具有静态存储期。任何VM类型都不能具有链接。 void fvla(int m, int C[m][m]) // 正确:块作用域/自动存储期指针指向VLA { typedef int VLA[m][m]; // 正确:块作用域VLA int D[m]; // 正确:块作用域/自动存储期VLA // static int E[m]; // 错误:静态存储期VLA // extern int F[m]; // 错误:具有链接的VLA int (*s)[m]; // 正确:块作用域/自动存储期VM s = malloc(m * sizeof(int)); // 正确:s指向已分配存储中的VLA // extern int (*r)[m]; // 错误:具有链接的VM static int (*q)[m] = &B; // 正确:块作用域/静态存储期VM} } 可变修改类型不能作为结构体或联合体的成员。 struct tag { int z[n]; // 错误:可变长度数组结构体成员 int (*y)[n]; // 错误:可变长度结构体成员 }; |
(C99起) |
|
若编译器将宏常量 __STDC_NO_VLA__ 定义为整型常量 1 ,则表示不支持VLA及VM类型。 |
(C11起)
(C23前) |
|
若编译器将宏常量 __STDC_NO_VLA__ 定义为整型常量 1 ,则不支持具有自动存储期的VLA对象。 对具有分配存储期的VM类型和VLA的支持是强制要求的。 |
(C23起) |
未知大小的数组
如果在数组声明符中省略
expression
,则声明一个未知大小的数组。除了在函数参数列表中(此类数组会被转换为指针)以及当有
initializer
可用时,此类类型属于
incomplete type
(注意:使用
*
作为大小声明的未指定大小VLA是完整类型)
(C99起)
:
extern int x[]; // x的类型是“未知边界的int数组” int a[] = {1,2,3}; // a的类型是“3个int的数组”
|
在 struct 定义内部,未知大小的数组可以作为最后一个成员出现(前提是至少存在一个其他命名成员),这种情况被称为 柔性数组成员 的特殊用法。详见 struct 说明: struct s { int n; double d[]; }; // s.d 是柔性数组成员 struct s *s1 = malloc(sizeof (struct s) + (sizeof (double) * 8)); // 效果等同于 double d[8]
|
(C99起) |
限定符
|
若数组类型通过使用
|
(C23前) |
|
数组类型与其元素类型始终被视为具有相同限定,但数组类型永远不会被视为
|
(C23起) |
typedef int A[2][3]; const A a = {{4, 5, 6}, {7, 8, 9}}; // 由常量整数数组构成的数组 int* pi = a[0]; // 错误:a[0] 的类型为 const int* void* unqual_ptr = a; // C23 前有效;C23 起报错 // 注意:clang 即使在 C89-C17 模式下也遵循 C++/C23 的规则
|
typedef int A[2]; // _Atomic A a0 = {0}; // 错误 // _Atomic(A) a1 = {0}; // 错误 _Atomic int a2[2] = {0}; // 正确 _Atomic(int) a3[2] = {0}; // 正确 |
(C11起) |
赋值
数组类型的对象不是 可修改左值 ,虽然可以获取它们的地址,但不能出现在赋值运算符的左侧。不过,包含数组成员的结构体是可修改左值,可以被赋值:
int a[3] = {1,2,3}, b[3] = {4,5,6}; int (*p)[3] = &a; // 正确,可以获取a的地址 // a = b; // 错误,a是数组 struct { int c[3]; } s1, s2 = {3,4,5}; s1 = s2; // 正确:可以赋值包含数组成员的结构体
数组到指针转换
任何 左值表达式 的数组类型,在用于除以下情况外的任何上下文时
- 作为 取址运算符 的操作数
-
作为
sizeof的操作数 -
作为
typeof和typeof_unqual的操作数 (C23 起) - 作为用于 数组初始化 的字符串字面量
| (since C11) |
经历一次到其首元素指针的 隐式转换 。该结果不是左值。
如果数组被声明为
register
,尝试进行此类转换的程序行为是未定义的。
当数组类型用于函数参数列表时,它会被转换为相应的指针类型: int f ( int a [ 2 ] ) 与 int f ( int * a ) 声明的是同一个函数。由于函数的实际参数类型是指针类型,使用数组实参的函数调用会执行数组到指针的转换;被调用函数无法获取实参数组的大小,必须显式传递:
#include <stdio.h> void f(int a[], int sz) // 实际声明为 void f(int* a, int sz) { for (int i = 0; i < sz; ++i) printf("%d\n", a[i]); } void g(int (*a)[10]) // 指向数组的指针参数不会被转换 { for (int i = 0; i < 10; ++i) printf("%d\n", (*a)[i]); } int main(void) { int a[10] = {0}; f(a, 10); // 将 a 转换为 int*,传递指针 g(&a); // 传递指向数组的指针(无需传递大小) }
多维数组
当数组的元素类型是另一个数组时,该数组被称为多维数组:
// 由2个包含3个整数的数组组成的数组 int a[2][3] = {{1,2,3}, // 可视为2x3矩阵 {4,5,6}}; // 采用行优先布局
注意当应用数组到指针的转换时,多维数组会转换为其首元素的指针,例如指向第一行的指针:
int a[2][3]; // 2x3 矩阵 int (*p1)[3] = a; // 指向首个3元素行的指针 int b[3][3][3]; // 3x3x3 立方体 int (*p2)[3][3] = b; // 指向首个3x3平面的指针
|
多维数组在每个维度都可以被可变修改 (若支持VLA) (自C11起) : int n = 10; int a[n][2*n]; |
(自C99起) |
注释
不允许声明长度为零的数组,尽管某些编译器将其作为扩展提供(通常作为 C99 之前 柔性数组成员 的实现方式)。
如果变长数组的尺寸 表达式 具有副作用,除非它是 sizeof 表达式的一部分且其结果不依赖于该表达式,否则保证会产生这些副作用:
int n = 5, m = 5; size_t sz = sizeof(int (*[n++])[m++]); // n 被递增,m 可能被递增也可能不被递增
参考文献
- C23标准(ISO/IEC 9899:2024):
-
- 6.7.6.2 数组声明符(页:待定)
- C17标准(ISO/IEC 9899:2018):
-
- 6.7.6.2 数组声明符(页码:94-96)
- C11标准(ISO/IEC 9899:2011):
-
- 6.7.6.2 数组声明符(第130-132页)
- C99标准 (ISO/IEC 9899:1999):
-
- 6.7.5.2 数组声明符 (p: 116-118)
- C89/C90标准(ISO/IEC 9899:1990):
-
- 3.5.4.2 数组声明符
另请参阅
|
C++ 文档
关于
数组声明
|