C 参考手册
- C 语言
- C 的历史
- 基本概念
- 表达式
- 声明
- 初始化
- 函数
- 语句
- 静态断言
- 字符常量
- 函数声明
- 函数定义
- 转义序列
- 翻译阶段
- 标识符
- 作用域
- 生存期
- 查找与命名空间
- ASCII 码表
- 类型
- 遵从性
- 算术类型
- restrict 类型限定符
- 类型
- 对象与对齐
- 主函数
- 未定义行为
- 内存模型
- if 语句
- switch 语句
- for 循环
- while 循环
- do-while 循环
- continue语句
- break 语句
- goto语句
- return 语句
- 值类别
- 求值顺序
- 整数常量
- 浮点常量
- 字符串字面量
- 复合字面量
- 常量表达式
- 隐式转换
- 成员访问运算符
- 逻辑运算符
- 比较运算符
- 算术运算符
- 赋值运算符
- 自增/自减运算符
- 其他运算符
- sizeof 运算符
- _Alignof 运算符
- 转型运算符
- C 运算符优先级
- 泛型选择
- 标量初始化
- 数组初始化
- 结构体与联合体初始化
- 指针声明
- 数组声明
- 枚举
- 存储类指定符
- const 类型限定符
- volatile 类型限定符
- 结构体声明
- 联合体声明
- 位域
- _Alignas
- typedef 声明
- 原子类型
- 外部及试探性定义
- inline 函数指定符
- _Noreturn 函数指定符
- 变长参数
- 内联汇编
- 可分析性
- 替用运算符及记号
- C 关键词
- 预处理器
- C 标准库头文件
- 类型支持
- 程序支持工具
- 变参数函数
- 错误处理
- 动态内存管理
- 日期和时间工具
- 字符串库
- 算法
- 数值
- 文件输入/输出
- 本地化支持
- 原子操作库
- 线程支持库
- 实验性 C 标准库
- 有用的资源
- 符号索引
- 注释
数组声明
数组是由连续无空隙分配的,拥有特定元素类型的对象构成的。这些对象的数目数量(数组大小)在数组生存期间决不改变。
语法
在数组声明的声明文法中,类型指定序列指明元素类型(必须是一个完整对象类型),而声明器拥有形式:
[ static (可选) 限定符(可选) 表达式(可选) ]
|
(1) | ||||||||
[ 限定符(可选) static (可选) 表达式(可选) ]
|
(2) | ||||||||
[ 限定符(可选) * ]
|
(3) | ||||||||
限定符 | - | 任何无逗号运算符的表达式,表明数组中的元素数量 |
表达式 | - | 任意 const 、 restrict 或 volatile 限定符的混合,只允许出现于函数参数列表中;它们对数组参数所被转换得的指针类型赋予限定
|
float fa[11], *afp[17]; // fa 是 11 个 float 组成的数组 // afp 是 17 个指向 float 的指针组成的数组
解释
有几种数组类型变体:已知常量大小的数组、变长度数组,以及未知大小数组。
已知常量大小数组
若数组声明器中的 表达式 为整数常量表达式,拥有大于零的值,且元素类型是一种拥有已知常量大小的类型(即元素不是 VLA ) (C99 起),则声明器声明已知常量大小的数组:
int n[10]; // 整数常量是常量表达式 char o[sizeof(double)]; // sizeof 是常量表达式 enum { MAX_SZ=100 }; int n[MAX_SZ]; // 枚举常量是常量表达式
已知常量大小的数组可以用数组初始化器提供它们的初始值:
int a[5] = {1,2,3}; // 声明 int[5] 初始化为 1,2,3,0,0 char str[] = "abc"; // 声明 char[4] 初始化为 '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 个 double 的优化 int main(void) { double a[10] = {0}, b[20] = {0}; fadd(a, b); // OK 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 的生命期在其声明离开作用域时结束)。 VLA 实例的大小不会在其生存期改变,但在另一次经过同一代码时,它可能被分配不同大小。 { 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]; // OK:文件作用域的已知常量大小数组 void fvla(int m, int C[m][m]); // OK:原型作用域 VLA VLA 必须拥有自动存储期。指向 VLA 的指针,但不是 VLA 自身亦可拥有静态存储期。 VM 类型不能拥有链接。 void fvla(int m, int C[m][m]) // OK :块作用域/自动存储期到 VLA 的指针 { typedef int VLA[m][m]; // OK :块作用域 VLA int D[m]; // OK :块作用域/自动存储期 VLA // static int E[m]; // 错误:静态存储期 VLA // extern int F[m]; // 错误:拥有链接的 VLA int (*s)[m]; // OK:块作用域/自动存储期 VM // extern int (*r)[m]; // 错误:拥有链接的 VM static int (*q)[m] = &B; // OK :块作用域/自动存储期 VM } 可变修改的类型不能是结构体或联合体的成员。 struct tag { int z[n]; // 错误: VLA 结构体成员 int (*y)[n]; // 错误: VM 结构体成员 }; |
(C99 起) |
若编译器定义宏常量 __STDC_NO_VLA__ 为整数常量 1 ,则不提供 VLA 或 VM 。 |
(C11 起) |
未知大小数组
若忽略数组声明器中的 表达式,则它声明一个未知大小数组。除了函数参数列表中的情况(这种数组被转换成指针),而且当初始化器可用时,这种类型是一个不完整类型(注意拥有未指定大小的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)); // 如同 d 是 d[8]
|
(C99 起) |
限定符
若数组类型声明拥有 const
、 volatile
、 restrict
(C99 起) 或 _Atomic
(C11 起) 限定符(可以通过使用 typedef
),则数组类型无限定,但其元素类型有限定:
typedef int A[2][3]; const A a = {{4, 5, 6}, {7, 8, 9}}; // const int 的数组的数组 int* pi = a[0]; // 错误: a[0] 拥有类型 const int*
赋值
数组类型的对象不是可修改左值,尽管它们可以取地址,它们不能出现于赋值运算符的左侧。不过,拥有数组成员的结构体是可修改左值,并可以赋值:
int a[3] = {1,2,3}, b[3] = {4,5,6}; int (*p)[3] = &a; // OK ,可以取地址 // a = b; // 错误,a 是数组 struct { int c[3]; } s1, s2 = {3,4,5}; s1 = s2; // OK :可以赋值拥有数组成员的结构体
数组到指针转换
任何数组类型的左值表达式,当用于异于
|
(C11 起) |
的语境时,会经历到指向其首元素指针的隐式转换。结果不是左值。
若声明数组为 register
,则尝试这么做的程序的行为未定义。
当数组类型用于函数参数列表是,它会转换成对应的指针类型: int f(int a[2]) 和int f(int* a) 声明同一个函数。因为函数实际参数类型为指针类型,使用数组参数的函数调用会进行一个数组到指针转换;参数数组的大小不为被调用函数可得,而必须显式传递:
void f(int a[], int sz) // 实际上声明 void f(int* a, int sz) { for(int i = 0; i < sz; ++i) printf("%d\n", a[i]); } int main(void) { int a[10]; f(a, 10); // 转换成 int* ,传递指针 }
多维数组
当数组的元素是另一个数组时,我们称数组是多维的:
// 2 个元素为 3 个 int 的数组的数组 int a[2][3] = {{1,2,3}, // 可视作行主导排列的 {4,5,6}}; // 2x3 矩阵
注意当应用数组到指针转换是,多维数组被转换成指向其首元素的指针,例如指针到首行:
int a[2][3]; // 2x3 矩阵 int (*p1)[3] = a; // 指向首个 3 个元素行的指针 int b[3][3][3]; // 3x3x3 立方体 int (*p2)[3][3] = a; // 指向首个 3x3 平面的指针
多维数组可以在每一维度可变修改: int n = 10; int a[n][2*n]; |
(C99 起) |
注解
不允许零长度数组,即使一些编译器作为扩展提供它们(典型例子是 C99 前的柔性数组成员实现)。
若 VLA 的大小 表达式 拥有副效应,则保证会正确产生副效应,除非它们是 sizeof 表达式的一部分,而结果不依赖副效应:
int n = 5; int m = 7; size_t sz = sizeof(int (*)[n++]); // n 可以自增也可以不自增
引用
- C11 standard (ISO/IEC 9899:2011):
- 6.7.6.2 Array declarators (p: 130-132)
- C99 standard (ISO/IEC 9899:1999):
- 6.7.5.2 Array declarators (p: 116-118)
- C89/C90 standard (ISO/IEC 9899:1990):
- 3.5.4.2 Array declarators