当前位置: C语言 -- 基础 -- 声明

声明(十五)

2、数组声明符

数组声明符(array declarator)具有以下语法格式:

pointeropt direct-declarator [ content ] attribute-specifier-sequenceopt

其中content具有以下语法格式之一:

type-qualifier-listopt assignment-expressionopt
static type-qualifier-listopt assignment-expression
type-qualifier-list static assignment-expression
type-qualifier-listopt *

ISO/IEC 9899:2024标准对数组声明符的使用作了一些限制:

-- 除了可选的类型限定符和关键词static外,[ ]内还可以包含表达式或者*。如果是表达式(表达式指定数组大小。),表达式必须是整数类型表达式。如果表达式是常量表达式,其值应大于0。

int a[3];
int *b[5];

-- 数组元素类型不能是不完整类型或者函数类型。

struct s;
struct s a[3];  //非法,这里struct s类型是不完整类型。

struct s{
  int i;
  double d;
};
struct s b[3];  //合法,这里struct s类型是完整类型。

typedef int Func(void);
Func c[3];  //非法,元素类型是函数类型。

-- 可选类型限定符和关键字static只能出现在具有数组类型的函数形参声明中,并且只能出现在最外层的数组类型派生中。

int f1(int arr[static 3]);	//合法。
int f2(int arr[3][const 3]);	//非法。
int f3(int arr[const 3][3]);	//合法。

这种情况是由函数形参中数组类型会转换成指针类型的转换机制决定的,因此限定符仅能作用于最外层指针。


-- 如果标识符声明具有可变修改类型(variably modified type),该标识符应是普通标识符,并且无链接,具有块作用域或者函数原型作用域。如果标识符声明为具有静态存储期限或者线程存储期限的对象,该标识符不应具有变长数组类型(variable length array type)。

int n = 3;
int (*p1)[n]; //假设p1具有文件作用域,非法。
...
int (*p2)[n]; //假设p2具有块作用域,合法。

数组声明符中可选的属性说明符序列(attribute-specifier-sequenceopt)适用于数组。

int a1[3] [[gnu::aligned(16)]]; //属性[[gnu::aligned(16)]]适用于数组。
[[gnu::aligned(16)]] int a2[5]; //属性[[gnu::aligned(16)]]适用于数组元素。

如果未指定数组大小,数组类型是不完整类型。

extern int arr[];	//这里数组arr具有不完整类型。

如果数组大小使用*表示,而不是使用表达式表示,则数组类型是未指定大小的变长数组类型,这类数组只能作为函数形参声明的声明符或者抽象声明符嵌套序列的一部分使用,而不包括表示数组大小表达式的任何内容;尽管如此此类数组仍是完整类型。这类数组只能在非函数定义的函数声明中使用,而不能在函数定义中使用。

int f1(int arr[*]);	//合法。
int f2(int (*ptr)[*]);	//合法。
int f3(int [sizeof(int (*)[*])]);   //非法。

在函数f3中,[*]是在表示数组大小的表达式中使用的;但不是直接作为形参声明抽象说明符嵌套序列的一部分使用的。


如果数组类型[ ]中存在类型限定符,这样的数组类型声明为函数形参类型时应转换成限定指针类型。

int func(int [const *]);    //等价于int func(int * const);。

如果数组类型[ ]中存在static存储类说明符,这样的数组类型声明为函数形参类型时应转换成指针类型;每次函数调用,该指针指向数组第一个元素,该数组的元素数应至少与数组大小表达式指定的元素数相同。

int func(int n, const int arr[static n])
{
    int sum = 0;
    for(int i=0; i<n; i++)
        sum += arr[i];

    return sum;
}
...
const int a1[3] = {1, 3, 5};
const int a2[5] = {2, 4, 6, 8, 10};

func(5, a1);    //非法,数组a1元素数小于5。
func(5, a2);    //合法,数组a2元素数等于5。

如果数组大小使用整数常量表达式表示,并且元素类型具有常量大小,则数组类型不是变长数组类型;否则数组类型是变长数组类型。变长数组是个可选特征;是否支持变长数组将由实现决定,例如:Visual Studio不支持变长数组;GCC支持变长数组。

int n = 3;

int a1[n];  //a1是变长数组。
int a2[3];  //a2不是变长数组。

如果数组大小使用非整数常量表达式表示:如果在函数原型作用域的声明中,视为使用*替换;否则每次评估时数组大小值应大于0

int func(int n, int arr[n]);  //与int func(int n, int arr[*]);是等价的。

变长数组在其生命周期内大小不能改变。

int n = 3;
int arr[n]; //数组arr含有3个数组元素,占12个字节。

n = 5;
printf("%zu\n", sizeof(arr)); //输出12,数组arr大小没有发生改变。

如果表示数组大小的表达式是typeof类运算符或者sizeof运算符操作数的一部分,并且改变表达式的值不会影响运算符的运算结果,是否对表达式进行评估ISO/IEC 9899:2024标准未作明确要求。

int n = 3;
size_t st = sizeof(int (*)[n++]);  //n++可能不会评估。

由于实现中编译器行为可能存在差异,因此编程时应避免这样的副作用。


如果表示数组大小的表达式是alignof运算符操作数的一部分,表达式不会被评估。

ISO/IEC 9899:2024标准中alignofC语言关键词,可以直接使用;ISO/IEC 9899:2018标准中alignof<stdalign.h>头文件中定义的宏,使用时需包含<stdalign.h>头文件。)

int n = 3;
alignas(alignof(n++)) short sh = 0;  //n++不会评估。

int arr[n];

如果两个数组类型兼容,元素类型应兼容;如果都存在表示数组大小的整数常量表达式,表达式的值应相等。如果两个数组类型在要求它们必须兼容的上下文中使用,如果数组大小不相等,其行为是未定义的。

int n = 3;
int a1[n];
int *p1 = nullptr;

p1 = a1;  //合法。

int a2[2][n];
int (*p2)[4] = nullptr;
int (*p3)[n] = nullptr;
int (*p4)[3] = nullptr;

p2 = a2;  //非法,n不等于4。
p3 = a2;  //合法。
p4 = a2;  //合法。

可变修改类型(variably modified types)的所有有效声明都应具有块作用域或者函数原型作用域。使用thread_localstatic或者extern存储类说明符声明的数组对象不能具有变长数组类型(variable length array type);但使用static存储类说明符声明的对象可以具有可变修改类型(即指向变长数组类型的指针。)。只有普通标识符可以声明为可变修改类型,因此具有可变修改类型的标识符不能是结构或者联合成员。

int n = 3;

{
  struct s{
    int (*p1)[n]; //非法,p1具有可变修改类型,但不是普通标识符。
    int a1[n];    //非法,a1具有变长数组类型,但不是普通标识符。
  };
  
  int a2[n];	//合法。
  static int a3[n]; //非法,a3具有变长数组类型。
  extern int a4[n]; //非法,a4具有变长数组类型。
  
  int (*p2)[n]; //合法。
  static int (*p3)[n];  //合法。
  extern int (*p4)[n];  //非法,p4具有可变修改类型。
}