Array declaration
数组是一种由特定 元素类型 的对象组成的连续分配非空序列所构成的类型。这些对象的数量(即数组大小)在数组生命周期内永不改变。
目录 |
语法
在 声明语法 中,数组声明的 类型说明符 序列指定了 元素类型 (必须是完整对象类型),而 声明符 具有以下形式:
[
static
(可选)
限定符
(可选)
表达式
(可选)
]
属性说明序列
(可选)
|
(1) | ||||||||
[
限定符
(可选)
static
(可选)
表达式
(可选)
]
属性说明序列
(可选)
|
(2) | ||||||||
[
限定符
(可选)
*
]
属性说明序列
(可选)
|
(3) | ||||||||
| expression | - | 除 逗号运算符 外的任意表达式,用于指定数组中的元素数量 |
| qualifiers | - |
const
、
restrict
或
volatile
限定符的任意组合,仅允许在函数参数列表中使用;这用于限定该数组参数转换后的指针类型
|
| attr-spec-seq | - | (C23) 可选的 属性 列表,应用于声明的数组 |
float fa[11], *afp[17]; // fa 是一个包含 11 个 float 的数组 // afp 是一个包含 17 个指向 float 的指针的数组
说明
数组类型存在几种变体:已知常量大小的数组、变长数组以及未知大小的数组。
常量已知大小数组
如果数组声明符中的 表达式 是一个值大于零的 整数常量表达式 且元素类型是具有已知常量大小的类型(即元素不是VLA) (since C99) ,则该声明符声明一个常量已知大小的数组:
int n[10]; // 整型常量是常量表达式 char o[sizeof(double)]; // sizeof 是常量表达式 enum { MAX_SZ=100 }; int n[MAX_SZ]; // 枚举常量是常量表达式
已知大小的常量数组可以使用 数组初始化器 来提供初始值:
int a[5] = {1,2,3}; // 声明长度为5的int数组,初始化为1,2,3,0,0 char str[] = "abc"; // 声明长度为4的char数组,初始化为'a','b','c','\0'
|
在函数参数列表中,数组声明符内允许使用额外的语法元素:关键字
在每次对使用
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个双精度浮点数等优化 int main(void) { double a[10] = {0}, b[20] = {0}; fadd(a, b); // 正确 double x[5] = {0}; fadd(x, b); // 未定义行为:数组实参太小 } 如果存在 限定符 ,它们将限定数组参数类型转换后的指针类型: 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]; } } 变长数组如果 表达式 不是 整数常量表达式 ,则声明符用于变长数组。 每次控制流经过该声明时,都会计算 表达式 (其计算结果必须始终大于零),并分配数组(相应地,当声明超出作用域时,VLA 的 生命周期 结束)。每个 VLA 实例的大小在其生命周期内不会改变,但在另一次经过相同代码时,可能会以不同的大小分配。
运行此代码
#include <stdio.h> int main(void) { int n = 1; label:; int a[n]; // 重新分配10次,每次大小不同 printf("数组有 %zu 个元素\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</ |
未知大小的数组
如果在数组声明符中省略
表达式
,则声明一个未知大小的数组。除了在函数参数列表中(此类数组会被转换为指针)以及当存在
初始化器
时,这种类型属于
不完整类型
(注意:使用
*
作为大小声明的未指定大小VLA是完整类型)
(C99起)
:
extern int x[]; // x的类型是“未知大小的int数组” int a[] = {1,2,3}; // a的类型是“3个int的数组”
|
在 结构体 定义中,未知大小的数组可以作为最后一个成员出现(前提是至少存在一个其他命名成员),这种情况被称为 柔性数组成员 的特殊用法。详见 结构体 说明: struct s { int n; double d[]; }; // s.d is a flexible array member struct s *s1 = malloc(sizeof (struct s) + (sizeof (double) * 8)); // as if d was 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 起) - 作为用于 数组初始化 的字符串字面量
| (自 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之前 柔性数组成员 的实现方式)。
如果 VLA 的大小 表达式 具有副作用,除非它是 sizeof 表达式的一部分且其结果不依赖于该表达式,否则保证会产生这些副作用:
int n = 5, m = 5; size_t sz = sizeof(int (*[n++])[m++]); // n 被递增,m 可能被递增也可能不被递增
参考文献
- C23 标准 (ISO/IEC 9899:2024):
-
- 6.7.6.2 数组声明符 (页: TBD)
- C17 标准 (ISO/IEC 9899:2018):
-
- 6.7.6.2 数组声明符 (p: 94-96)
- C11 标准 (ISO/IEC 9899:2011):
-
- 6.7.6.2 数组声明符 (p: 130-132)
- C99标准(ISO/IEC 9899:1999):
-
- 6.7.5.2 数组声明符(页码:116-118)
- C89/C90 标准 (ISO/IEC 9899:1990):
-
- 3.5.4.2 数组声明符
参阅
|
C++ 文档
关于
数组声明
|