数组

为了解释清楚一些现象背后的原因,本文涉及一些超纲的计算机底层知识。这部分内容都在绿色的框内,如果你无法看懂可以直接跳过。等到大家学完了计算机系统基础 (ICS) 后自然就能理解这些话的含义。

有了循环之后,你很快就会发现简单地定义一个一个的变量有点“不够用”了,比如考虑如下问题:

输入整数 $N$,然后输入 $N$ 个数的一个数列,将这个数列倒序输出。

我们想要倒序输出,就说明我们在读取完数列的最后一个整数时,还要“记住”前面的 $N-1$ 个整数,所以我们每读取到一个整数都得将其保存在变量里,但我们在预先不知道数列长度的情况下怎么知道该定义多少个变量呢?这似乎陷入了死局。

(在这里我们不考虑递归等技巧) 我们希望有一种语法,可以批量开一堆变量,而且最好能用下标去索引它们。在 C/C++ 中我们可以通过定义数组来实现这一点。我们先给出上面问题的一段示例代码:

#include <bits/stdc++.h>

int main ()
{
    int n;
    std::cin >> n;
    int a[n];
    for (int i = 0; i < n; i++)
        std::cin >> a[i];
    for (int i = n - 1; i >= 0; i--)
        std::cout << a[i] << '\n';
    return 0;
}

这里出现的新语法是 int a[n]。如果说 int 类型的变量是一个可以存放一个整数的小盒子,那么 int a[n] 就定义了 $n$ 个小盒子,每个小盒子都可以存放一个整数。值得注意的是,这里的“小盒子”是按照 $0,1,\cdots, n-1$ 编号的。

Variable-Length Array (VLA)

在上面的例子中,我们定义的数组的长度依赖于我们输入的变量 $n$ 的值,也就是说,在程序开始运行之前,我们无法知道数组的具体长度。这种以变量作为长度的数组称为 variable-length array (VLA)。

VLA 的微妙之处在于,编译器在不知道数组具体长度的情况下可能会在内存分配上犯难。C99 标准首次允许 VLA 的使用,但对其作出了诸多限制,比如不能使用 $\mathbf{extern}$, $\mathbf{static}$ 等关键字修饰。不同的编译器支持的标准也略有差异,例如 Visual Studio 使用的 msvc 编译器很可能会对上面的示例代码报错。

如果你对这些内容感兴趣,可以上网查询更多的资料。

如果上面关于 VLA 的内容你没有看懂,没有关系,一句话概括就是使用变量作为数组的长度“不太好”。我们的 OJ 题会对输入数据的范围作出严格的限制,你可以根据数据范围将数组开到足够大的一个固定长度。假设题目规定了 $N\leq 1000$,那么一个不使用 VLA 的程序应该这样写:

#include <bits/stdc++.h>

int a[1000];

int main ()
{
    int n;
    std::cin >> n;
    for (int i = 0; i < n; i++)
        std::cin >> a[i];
    for (int i = n - 1; i >= 0; i--)
        std::cout << a[i] << '\n';
    return 0;
}

这里我们希望不加解释地做出一个规定:如果你要定义一个定长的数组,请将它定义在 main() 函数的外面。

不行,我就是想知道为什么要这么规定

C/C++ 的编译器需要负责将高级语言程序映射到具体的硬件上。对于数组这样的存储设施,编译器会将其映射到内存中的某块区域。如果你将数组定义在函数内部,编译器会将其安排在栈上;如果你将数组定义在全局 (函数外部),编译器会将其安排在静态数据区。除非特别配置,一个程序的栈空间通常不是很大,如果在函数内部定义了过长的数组可能会导致栈溢出,栈溢出会导致不可预知的严重后果。

如果你想亲手体验一下“栈溢出”,你可以尝试运行以下代码 (不要加任何编译优化):

#include <bits/stdc++.h>

int main () { char s[1 << 25]; }

直接运行这段代码可能会获得段错误 (Segmentation Fault)。

如果你没有看懂这段话,那就老老实实地遵守我们的规定吧。

总有一些“完美主义者”觉得这样写代码十分令人不爽——如果 $N$ 远小于 1000,我们的代码岂不是无谓的多使用了很多资源?这里我们再介绍一种定义数组的方式,它在功能上和前面的几种是完全相同的:

#include <bits/stdc++.h>

int main ()
{
    int n;
    std::cin >> n;
    int *a = new int [n];
    for (int i = 0; i < n; i++)
        std::cin >> a[i];
    for (int i = n - 1; i >= 0; i--)
        std::cout << a[i] << '\n';
    delete [] a;
    return 0;
}

int *a = new int [n] 的功能是定义一个长度为 $n$ 的数组,每个数组元素都是 int 类型,这个数组的名字叫做 a。我们在这里希望强调一点:如果你使用了new语法定义数组,请一定在你确定不会再使用该数组的时刻 (例如 return 0 之前) 使用delete释放它

我不写delete这一行好像也没报错啊?

如果你是一个曾经学过算法竞赛的同学,你很可能已经养成了“随手new,从不delete”的习惯。我们在这里必须严肃地警告:这是一个非常危险的习惯!你之前写过的忘记 delete 的 OJ 程序之所以可以正常退出,是因为当 OJ 程序所在的进程被销毁时,操作系统会将进程申请的资源自动释放——换言之,操作系统帮你默默地做了 delete。没有及时释放申请的内存会导致内存泄漏 (memory leak),如果将来你维护一个大型的项目,内存泄漏的累积很可能导致程序崩溃。

如果你是一个计算机小白,恭喜你拥有了一个小小的优势:你没有经历过算法竞赛中各种糟糕的代码书写习惯的熏陶。从初学阶段开始严格遵守各种规范,你将自然而然地将书写安全、高质量、可读性强的代码作为一种本能。

Previous
Next