| 目录 |

33 数组(二)

本章我们还要再看几个与数组有关的杂项内容。

它们并不十分常见,但既然属于较新的规范,我们还是来瞥一眼。

33.1 形参列表中数组的类型限定符

如果还记得前面的内容,那么在函数形参列表中,下面两种写法是等价的:

int func(int *p) {...}
int func(int p[]) {...}

你可能也还记得,可以像这样给指针变量加上类型限定符:

int *const p;
int *volatile p;
int *const volatile p;
// 等等

但如果在形参列表里用的是数组写法,又该怎么做呢?

答案是写在方括号里,后面还可以跟上可选的元素个数。下面两行是等价的:

int func(int *const volatile p) {...}
int func(int p[const volatile]) {...}
int func(int p[const volatile 10]) {...}

如果是多维数组,类型限定符要放在第一对方括号中。

33.2 形参列表中数组的 static

类似地,也可以在形参列表的数组中使用 static 关键字。

这东西我在实际代码里从没见过。它总是后面跟着一个维度:

int func(int p[static 4]) {...}

在上面的例子中,这意味着编译器会假定你传给该函数的数组至少有 4 个元素。

其他情况都属于未定义行为。

int func(int p[static 4]) {...}

int main(void)
{
    int a[] = {11, 22, 33, 44};
    int b[] = {11, 22, 33, 44, 55};
    int c[] = {11, 22};

    func(a);  // 没问题!a 有 4 个元素,正好是最小值
    func(b);  // 没问题!b 至少有 4 个元素
    func(c);  // 未定义行为!c 不足 4 个元素!
}

这基本上设定了数组允许的最小尺寸。

重要提示:编译器并不会禁止你传入更小的数组。它多半不会警告你,运行时也检测不出来。

把 static 写在那里,你等于在说:“我以双重机密郑重保证,绝不会传入比这更小的数组。”而编译器则说:“行吧。”然后信任你不会那么做。

这样编译器就能放心地做某些代码优化,因为它知道你这个程序员总会做出正确的选择。

33.3 等价的初始化器

在数组初始化器方面,C 可以说有点灵活。

我们已经见过一些了:缺失的值都会被替换为零。

例如,下面这样可以把一个 5 元素数组初始化为 1,2,0,0,0:

int a[5] = {1, 2};

或者用下面的写法把整个数组设为零:

int a[5] = {0};

但初始化多维数组时就有意思了。

我们来创建一个 3 行 2 列的数组:

int a[3][2];

写点代码初始化它,并打印结果:

#include <stdio.h>

int main(void)
{
    int a[3][2] = {
        {1, 2},
        {3, 4},
        {5, 6}
    };

    for (int row = 0; row < 3; row++) {
        for (int col = 0; col < 2; col++)
            printf("%d ", a[row][col]);
        printf("\n");
    }
}

运行后得到符合预期的结果:

1 2
3 4
5 6

我们省掉初始化器里的几个元素,看看它们会不会被设为零:

    int a[3][2] = {
        {1, 2},
        {3},    // 把 4 省掉了!
        {5, 6}
    };

输出为:

1 2
3 0
5 6

现在我们干脆把中间那一整项也去掉:

    int a[3][2] = {
        {1, 2},
        // {3, 4},   // 把这一整项直接删掉
        {5, 6}
    };

于是得到下面这样,可能和你想的不一样:

1 2
5 6
0 0

但停下来想想就明白了:我们只提供了够两行用的初始化器,所以它们被用在了前两行上,其余元素则被初始化为零。

到目前为止一切正常。一般来说,如果初始化器少写了一部分,编译器会把对应的元素设为 0。

但咱们来点疯狂的。

    int a[3][2] = { 1, 2, 3, 4, 5, 6 };

什么——?这是个二维数组,可初始化器只有一维!

结果这居然合法(不过打开相应警告后,GCC 会对此发出警告)。

它的基本行为是从左到右依次填充第 0 行、第 1 行、第 2 行的元素。

所以打印时,输出顺序是:

1 2
3 4
5 6

如果省掉几个:

    int a[3][2] = { 1, 2, 3 };

它们会被 0 填满:

1 2
3 0
0 0

所以如果想把整个数组填满 0,尽管这样写:

    int a[3][2] = {0};

但我的建议是:既然是二维数组,就用二维初始化器,这样代码更易读。(把整个数组初始化为 0 的情况除外,此时无论数组是几维,惯用写法都是 {0}。)


| 目录 |