Namespaces
Variants

Array declaration

From cppreference.net

数组是一种由连续分配的非空对象序列组成的类型,这些对象具有特定的 element type 。这些对象的数量(数组大小)在数组生命周期内永不改变。

目录

语法

在数组声明的 声明语法 中, type-specifier 序列指定了 element type (必须是完整对象类型),而 declarator 具有以下形式:

[ static (可选) 限定符  (可选) 表达式  (可选) ] 属性说明符序列  (可选) (1)
[ 限定符  (可选) static (可选) 表达式  (可选) ] 属性说明符序列  (可选) (2)
[ 限定符  (可选) * ] 属性说明符序列  (可选) (3)
1,2) 通用数组声明符语法
3) 未指定大小的VLA声明符(仅可出现在函数原型作用域中) 其中
表达式 - 逗号运算符 外的任意表达式,用于指定数组中的元素数量
限定符 - 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]数组

在函数参数列表中,数组声明符内允许使用额外的语法元素:关键字 static 限定符 ,它们可以在尺寸表达式之前以任意顺序出现(即使省略尺寸表达式时也可能出现)。

在每次对函数的 函数调用 中,若数组参数在 [ ] 之间使用了关键字 static ,则实际参数的值必须是一个有效的指针,指向一个数组的首元素,且该数组至少包含 expression 所指定数量的元素:

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

这通常与 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];
    }
}

变长数组

如果 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的作用域将结束其生命周期
}

如果大小为 * ,则该声明用于一个未指定大小的可变长度数组。此类声明只能出现在函数原型作用域中,并声明一个完整类型的数组。实际上,函数原型作用域中的所有可变长度数组声明符都被视为 expression 被替换为 * 的情况。

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];          // 正确:常量已知大小的文件作用域数组
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起)

限定符

若数组类型通过使用 const volatile restrict (C99起) 限定符声明(通过 typedef 实现),则数组类型本身不被限定,但其元素类型被限定:

(C23前)

数组类型与其元素类型始终被视为具有相同限定,但数组类型永远不会被视为 _Atomic 限定。

(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 的规则

_Atomic 不允许应用于数组类型,但允许原子类型的数组。

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; // 正确:可以赋值包含数组成员的结构体

数组到指针转换

任何 左值表达式 的数组类型,在用于除以下情况外的任何上下文时

(since 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 ) 声明的是同一个函数。由于函数的实际参数类型是指针类型,使用数组实参的函数调用会执行数组到指针的转换;被调用函数无法获取实参数组的大小,必须显式传递:

#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++ 文档 关于 数组声明