数组声明

来自cppreference.com
< c‎ | language

数组是由连续无空隙分配的,拥有特定元素类型的对象构成的。这些对象的数目数量(数组大小)在数组生存期间决不改变。

语法

在数组声明的声明文法中,类型指定序列指明元素类型(必须是一个完整对象类型),而声明器拥有形式:

[ static(可选) 限定符(可选) 表达式(可选) ] 属性说明符序列(可选) (1)
[ 限定符(可选) static(可选) 表达式(可选) ] 属性说明符序列(可选) (2)
[ 限定符(可选) * ] 属性说明符序列(可选) (3)
1,2) 生成通常数组语法
3) 声明未指定大小的 VLA (只能出现于函数原型作用域) 其中
表达式 - 任何无逗号运算符的表达式,表明数组中的元素数量
限定符 - 任意 constrestrictvolatile 限定符的混合,只允许出现于函数参数列表中;它们对数组参数所转换得到的指针类型赋予限定
属性说明符序列 - (C23)可选的属性列表,应用到被声明的数组
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'

在函数参数列表中,附加性语法元素允许出现于数组声明器内:关键词 static限定符 ,它们可以以任意顺序先于大小表达式出现(它们甚至可以在大小表达式被忽略时出现)。

在每个到数组参数类型在 [] 使用关键词 static 的函数的函数调用中,实际参数的值必须是一个指向数组首地址的合法指针,该数组元素数目至少为 表达式 的结果:

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 指针)
}

这通常用于 restrict 类型限定符:

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 结束其生存期
}

若大小是 * ,则声明是对于未指定大小的 VLA 的。这种声明只能出现于函数原型作用域,并声明一个完整类型的数组。其实,所有函数原型作用域中的 VLA 声明器都被处理成如同用 * 替换 表达式

void foo(size_t x, int a[*]);
void foo(size_t x, int a[x]) 
{
    printf("%zu\n", sizeof a); // 大小同sizeof(int*)
}

非常量长度数组与从它们派生的类型(指向它们的指针,等等)被通称为“可变修改类型”( 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; // 错误 :静态存储期指针需要静态存储期的B来初始化,
                             // 但无法定义static int B[m];
    static int (*p)[m] = NULL;
    p = &B;
}

可变修改的类型不能是结构体或联合体的成员。

struct tag {
    int z[n]; // 错误: VLA 结构体成员
    int (*y)[n]; // 错误: VM 结构体成员
};
(C99 起)

若编译器定义宏常量 __STDC_NO_VLA__ 为整数常量 1 ,则不支持 VLA 或 VM 类型。

(C11 起)
(C23 前)

若编译器定义宏常量 __STDC_NO_VLA__ 为整数常量 1 ,则不支持拥有自动存储期的 VLA 对象。

对 VM 类型及拥有分配存储期的 VLA 的支持是强制的。

(C23 起)

未知大小数组

若忽略数组声明器中的 表达式,则它声明一个未知大小数组。除了函数参数列表中的情况(这种数组被转换成指针),而且当初始化器可用时,这种类型是一个不完整类型(注意拥有未指定大小的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 起)

限定符

若数组类型声明拥有 constvolatile 、或 restrict (C99 起) 限定符(可以通过使用 typedef ),则数组类型无限定,但其元素类型有限定:

(C23 前)

始终认为数组类型与其元素类型有等同的限定,除了始终不认为数组类型有 _Atomic 限定:

(C23 起)
typedef int A[2][3];
const A a = {{4, 5, 6}, {7, 8, 9}}; // const int 的数组的数组
int* pi = a[0]; // 错误: a[0] 拥有类型 const int*
void *unqual_ptr = a; // C23 前 OK ; C23 起错误
// 注: clang 即使在 C89-C17 模式也应用 C++/C23 中的规则

不允许应用 _Atomic 到数组类型,尽管允许原子类型的数组。

typedef int A[2];
// _Atomic A a0 = {0};    // 错误
// _Atomic(A) a1 = {0};   // 错误
_Atomic int a2[2] = {0};  // OK
_Atomic(int) a3[2] = {0}; // OK
(C11 起)

赋值

数组类型的对象不是可修改左值,尽管它们可以取地址,它们不能出现于赋值运算符的左侧。不过,拥有数组成员的结构体是可修改左值,并可以赋值:

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 a[3] = {1,2,3};
int* p = a;
printf("%zu\n", sizeof a); // 打印数组大小
printf("%zu\n", sizeof p); // 打印指针大小

当数组类型用于函数参数列表时,它会转换成对应的指针类型: 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] = b; // 指向首个 3x3 平面的指针

若支持 VLA ,则 (C11 起)多维数组可以在每一维度可变修改:

int n = 10;
int a[n][2*n];
(C99 起)

注解

不允许零长度数组,即使一些编译器作为扩展提供它们(典型例子是 C99 前的柔性数组成员实现)。

若 VLA 的大小 表达式 拥有副作用,则保证会正确产生副作用,除非它们是 sizeof 表达式的一部分,而结果不依赖副效应:

int n = 5;
size_t sz = sizeof(int (*)[n++]); // n 可以自增也可以不自增

引用

  • C17 标准(ISO/IEC 9899:2018):
  • 6.7.6.2 Array declarators (第 94-96 页)
  • C11 标准(ISO/IEC 9899:2011):
  • 6.7.6.2 Array declarators (第 130-132 页)
  • C99 标准(ISO/IEC 9899:1999):
  • 6.7.5.2 Array declarators (第 116-118 页)
  • C89/C90 标准(ISO/IEC 9899:1990):
  • 3.5.4.2 Array declarators

参阅