命名空间
变体
操作

数组声明

来自 cppreference.com
< c‎ | 语言

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

内容

[编辑] 语法

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

[ static(可选) 限定符 (可选) 表达式 (可选) ] attr-spec-seq (可选) (1)
[ 限定符 (可选) static(可选) 表达式 (可选) ] attr-spec-seq (可选) (2)
[ 限定符 (可选) * ] attr-spec-seq (可选) (3)
1,2) 通用数组声明符语法
3) 未指定大小的 VLA 声明符(只能出现在函数原型作用域中)
表达式 - 逗号运算符之外的任何表达式,都指定数组中的元素数量
限定符 - 任何组合的constrestrictvolatile限定符,仅允许在函数参数列表中使用;这将限定此数组参数转换到的指针类型
attr-spec-seq - (C23)可选的属性列表,应用于声明的数组
float fa[11], *afp[17]; // fa is an array of 11 floats
                        // afp is an array of 17 pointers to floats

[编辑] 解释

数组类型有几种变体:已知大小的常量数组、可变长数组和未知大小的数组。

[编辑] 已知大小的常量数组

如果数组声明符中的表达式是一个整型常量表达式,其值为大于零并且元素类型是一个具有已知大小的常量类型(即,元素不是 VLA)(自 C99 起),那么该声明符声明一个已知大小的常量数组

int n[10]; // integer constants are constant expressions
char o[sizeof(double)]; // sizeof is a constant expression
enum { MAX_SZ=100 };
int n[MAX_SZ]; // enum constants are constant expressions

已知大小的常量数组可以使用数组初始化器来提供其初始值

int a[5] = {1,2,3}; // declares int[5] initialized to 1,2,3,0,0
char str[] = "abc"; // declares char[4] initialized to '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];
    }
}
// a call to fadd may perform compile-time bounds checking
// and also permits optimizations such as prefetching 10 doubles
int main(void)
{
    double a[10] = {0}, b[20] = {0};
    fadd(a, b); // OK
    double x[5] = {0};
    fadd(x, b); // undefined behavior: array argument is too small
}

如果存在限定符,它们将限定数组参数类型转换到的指针类型

int f(const int a[20])
{
    // in this function, a has type const int* (pointer to const int)
}
int g(const int a[const 20])
{
    // in this function, a has type const int* const (const pointer to const int)
}

这通常与restrict类型限定符一起使用

void fadd(double a[static restrict 10],
          const double b[static restrict 10])
{
    for (int i = 0; i < 10; i++) // loop can be unrolled and reordered
    {
        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]; // re-allocated 10 times, each with a different size
   printf("The array has %zu elements\n", sizeof a / sizeof *a);
   if (n++ < 10)
       goto label; // leaving the scope of a VLA ends its lifetime
}

如果大小为*,则该声明用于大小未指定的 VLA。此类声明只能出现在函数原型作用域中,并且声明一个完整类型的数组。实际上,函数原型作用域中的所有 VLA 声明符都将被视为表达式被替换为*

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

可变长数组及其派生类型(指向它们的指针等)通常被称为“可变修饰类型”(VM)。任何可变修饰类型的对象只能在块作用域或函数原型作用域中声明。

extern int n;
int A[n];            // Error: file scope VLA
extern int (*p2)[n]; // Error: file scope VM
int B[100];          // OK: file-scope array of constant known size
void fvla(int m, int C[m][m]); // OK: prototype-scope VLA

VLA 必须具有自动或分配的存储期。指向 VLA 的指针(但不包括 VLA 本身)也可能具有静态存储期。没有 VM 类型可以具有链接。

void fvla(int m, int C[m][m]) // OK: block scope/auto duration pointer to VLA
{
    typedef int VLA[m][m]; // OK: block scope VLA
    int D[m];              // OK: block scope/auto duration VLA
//  static int E[m]; // Error: static duration VLA
//  extern int F[m]; // Error: VLA with linkage
    int (*s)[m];     // OK: block scope/auto duration VM
    s = malloc(m * sizeof(int)); // OK: s points to VLA in allocated storage
//  extern int (*r)[m]; // Error: VM with linkage
    static int (*q)[m] = &B; // OK: block scope/static duration VM}
}

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

struct tag
{
    int z[n]; // Error: VLA struct member
    int (*y)[n]; // Error: VM struct member
};
(自 C99 起)

如果编译器将宏常量__STDC_NO_VLA__定义为整型常量1,则不支持 VLA 和 VM 类型。

(自 C11 起)
(直到 C23)

如果编译器将宏常量__STDC_NO_VLA__定义为整型常量1,则不支持具有自动存储期的 VLA 对象。

支持具有分配的存储期的 VM 类型和 VLA 是强制性的。

(自 C23 起)

[编辑] 未知大小的数组

如果数组声明符中的表达式被省略,它将声明一个未知大小的数组。除了在函数参数列表中(此类数组被转换为指针)以及当存在初始化器时,此类类型是一个不完整类型(请注意,使用*作为大小声明的未指定大小的 VLA 是一个完整类型)(自 C99 起)

extern int x[]; // the type of x is "array of unknown bound of int"
int a[] = {1,2,3}; // the type of a is "array of 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 起)

[编辑] 限定符

如果数组类型使用constvolatilerestrict(自 C99 起)限定符声明(这可以通过使用typedef来实现),则数组类型不会被限定,但其元素类型会被限定

(直到 C23)

数组类型及其元素类型始终被视为具有相同的限定符,但数组类型绝不会被认为是_Atomic限定的。

(自 C23 起)
typedef int A[2][3];
const A a = {{4, 5, 6}, {7, 8, 9}}; // array of array of const int
int* pi = a[0]; // Error: a[0] has type const int*
void* unqual_ptr = a; // OK until C23; error since C23
// Notes: clang applies the rule in C++/C23 even in C89-C17 modes

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

typedef int A[2];
// _Atomic A a0 = {0};    // Error
// _Atomic(A) a1 = {0};   // Error
_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; // okay, address of a can be taken
// a = b;            // error, a is an array
struct { int c[3]; } s1, s2 = {3,4,5};
s1 = s2; // okay: can assign structs holding array members

[编辑] 数组到指针转换

任何左值表达式,只要其类型为数组,在任何其他上下文中使用时

(自 C11 起)

都会进行隐式转换为指向其第一个元素的指针。结果不是左值。

如果数组被声明为register,则尝试进行此类转换的程序的行为是未定义的。

int a[3] = {1,2,3};
int* p = a;
printf("%zu\n", sizeof a); // prints size of array
printf("%zu\n", sizeof p); // prints size of a pointer

当数组类型用在函数参数列表中时,它会被转换为相应的指针类型:int f(int a[2])int f(int* a) 声明相同的函数。由于函数的实际参数类型是指针类型,因此使用数组参数的函数调用会执行数组到指针的转换;被调用函数无法获得参数数组的大小,必须显式传递。

#include <stdio.h>
 
void f(int a[], int sz) // actually declares void f(int* a, int sz)
{
    for (int i = 0; i < sz; ++i)
        printf("%d\n", a[i]);
}
 
void g(int (*a)[10]) // pointer to array parameter is not transformed
{
    for (int i = 0; i < 10; ++i)
        printf("%d\n", (*a)[i]);
}
 
int main(void)
{
    int a[10] = {0};
    f(a, 10); // converts a to int*, passes the pointer
    g(&a);    // passes a pointer to the array (no need to pass the size)
}

[编辑] 多维数组

当数组的元素类型是另一个数组时,则称该数组为多维数组。

// array of 2 arrays of 3 ints each
int a[2][3] = {{1,2,3},  // can be viewed as a 2x3 matrix
               {4,5,6}}; // with row-major layout

请注意,当应用数组到指针的转换时,多维数组会被转换为指向其第一个元素的指针,例如,指向第一行的指针。

int a[2][3]; // 2x3 matrix
int (*p1)[3] = a; // pointer to the first 3-element row
int b[3][3][3]; // 3x3x3 cube
int (*p2)[3][3] = b; // pointer to the first 3x3 plane

多维数组可以在每个维度上进行可变修改 如果支持 VLA(自 C11 起)

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

[编辑] 备注

不允许零长度数组声明,即使一些编译器提供它们作为扩展(通常作为 柔性数组成员 的预 C99 实现)。

如果 VLA 的大小 表达式 具有副作用,则保证这些副作用会产生,除非它是 sizeof 表达式的一部分,而 sizeof 表达式的结果不依赖于它。

int n = 5, m = 5;
size_t sz = sizeof(int (*[n++])[m++]); // n is incremented, m may or may not be incremented

[编辑] 参考

  • C23 标准 (ISO/IEC 9899:2024)
  • 6.7.6.2 数组声明符 (p: 待定)
  • 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 数组声明符 (p: 116-118)
  • C89/C90 标准 (ISO/IEC 9899:1990)
  • 3.5.4.2 数组声明符

[编辑] 另请参阅

C++ 文档 针对 数组声明