Namespaces
Variants

Array declaration

From cppreference.net

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

目录

语法

声明语法 中,数组声明的 类型说明符 序列指定了 元素类型 (必须是完整对象类型),而 声明符 具有以下形式:

[ static (可选) 限定符  (可选) 表达式  (可选) ] 属性说明序列  (可选) (1)
[ 限定符  (可选) static (可选) 表达式  (可选) ] 属性说明序列  (可选) (2)
[ 限定符  (可选) * ] 属性说明序列  (可选) (3)
1,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'

在函数参数列表中,数组声明符内允许使用额外的语法元素:关键字 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个双精度浮点数等优化
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 指针)
}

这通常与 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 实例的大小在其生命周期内不会改变,但在另一次经过相同代码时,可能会以不同的大小分配。

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

如果大小为 * ,则声明用于未指定大小的 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];          // 正确:文件作用域的常量已知大小数组
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起)

限定符

如果数组类型通过 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; // 正确:可以对包含数组成员的结构体进行赋值

数组到指针转换

任何数组类型的 左值表达式 ,在除以下情况外的任何语境中使用时:

(自 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之前 柔性数组成员 的实现方式)。

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