重新认识 C++ 程序

学习了函数之后,我们终于可以完整地介绍 C++ 程序的结构了。

编译(翻译)过程

首先来了解一下 C++ 程序如何进行编译的。

当我们把 C++ 代码写在一个文件内的时候,这个文件被称为 C++ 源文件(C++ source file)。一般情况下,这个文件经过编译后会生成另外一个文件,而这个文件存储了供计算机读取的二进制指令序列,称这个文件为 可执行文件(Executable file)或可执行程序(Executable program)。计算机可以直接运行这个可执行文件从而做我们想要做的事情。

C++ 语言本质上定义了一套如何从若干个 C++ 源文件转化为一个可执行程序的规则标准。在一般场合,我们称这个过程为编译,而本节为了防止歧义,称这个过程为翻译(Translation)。你会注意到这里的源文件可以不止一个,我们可以将一些代码分开写到多个文件中去。

整个翻译过程分为预处理、编译和连接三个大阶段。

预处理阶段

在执行将源代码转为二进制指令之前,首先会进行一些准备性质的操作。这个操作被称为预处理阶段。关于预处理都有哪些操作将在下文中介绍。

编译阶段

编译是从源代码转向二进制指令序列的过程。需要注意的是,一个源文件经过编译阶段会生成一个叫做“对象文件”的东西,这个对象文件存储了原来那个源文件中所包含的代码翻译的结果。

链接阶段

每一个对象文件都存储了各自源代码翻译的结果。因此如果我将所有代码分成三个源文件来写,那么这三个源文件各自编译后会生成三个“对象文件”。因此需要一个链接器(Linker)将这三个“对象文件”合并成一个可执行文件。在这个过程中,我们还需要做一些其它的事情,比如将程序执行的入口点放在 main 函数那里之类的。

代码结构

现在回头看看我们写的一个典型代码:

#include <iostream>
using namespace std;
void printSum(int x, int y) {
    cout << x + y << endl;
}
int answer{42};
int main() {
    int a, b;
    cin >> a >> b;
    printSum(a, b);
    cout << answer << endl;
}

首先我指出,C++代码实际上是由若干个声明构成的文本。我们一行行来看一下。

引入头文件

第一行 #include <iostream> 是一个预处理指令,也就是它指定了一个在编译前应该做的事情。它的含义是将头文件 <iostream> 中的内容直接引入。那么什么是头文件呢?

C++ 语言为了方便程序员写代码,已经提前为你写好了大量的代码。这些代码你可以直接使用,被称为标准库(Standarad library)。这些代码写在哪里了呢?就写在被称作标准库头文件(Header file)的这些文件里了。

其中一个头文件的文件名叫作 iostream。这个文件里存放的代码是有关输入输出的,它提供了 cin cout 之类的东西供你使用。当你把这个文件名放在 #include <> 的尖括号里面时,就会执行这样一个预处理过程:将文件 iostream 内的东西全部复制到 #include 这一行。

iostream 的含义是 Input/Output Stream,即输入输出流。

在一些高级的编辑器中,你可以按住 Ctrl 的同时点击 iostream,就可以跳转查看这个对应的头文件。GCC 编译器提供的 iostream 头文件第一行是这样的注释: // Standard iostream objects -*- C++ -*-

所以当你在文件最开头写上 #include <iostream> 的时候,编译器在预处理时就会帮你把 cincout 这些东西引入进来,你就可以使用了。这就是为什么后面我们使用 cin cout 时仍然遵循“先声明、后使用”的原则。

头文件除了 iostream 以外还有数十个,其中最常用的是 cmathcmath 中提供了诸多简单数学函数,如 sin\sin cos\cos 等,以及幂函数 pow(x,y)=xy\operatorname{pow}(x,y)=x^y 和开平方根函数 sqrt(x)=x\operatorname{sqrt}(x)=\sqrt x。在下一节我们可能会看到它们,届时将做更多介绍。

刚才提到过,这些由 C++ 标准规定可以直接使用的头文件是标准库的一部分。(Library)这个词的意思是指一系列已经写好(甚至编译好)的代码,可供广大程序员自由使用。库本身不能直接运行,而是作为程序的一部分发挥作用。比如标准库就提供了我们编写出的程序中输入输出那一部分的代码。除了标准库,还有许多著名的 C++ 库,比如 Boost、Qt 以及 EGE 等。

#include 是你最需要且目前阶段无法避免使用的一个预处理指令。所有的预处理指令都要求占一整行,且由 # 开头。你之后可能还会接触到如 #define #if 等预处理指令,这里暂且略过。

命名空间

第二行 using namespace std; 是一个被称为 using 声明的东西。为了解释 using 声明,我首先解释一下命名空间的概念。命名空间(Namespace)是一种用于组织管理各式各样的名字的形式。举一个例子,我在编写一个程序的时候,需要用到一个名字叫 A 的库。为了用 A 库里面的东西,我需要把它的头文件 #include 进来:

#include <a.h> // 假设 a.h 就是 A 库的一个头文件

其中假设 A 库里声明了一个叫 PI 的常量:

// a.h
constexpr float PI{3.14159f};

现在看起来没有任何问题。我写着写着发现有一个 B 库很有用,我也把它引入进来:

#include <a.h>
#include <b.h>

但是问题出现了,b.h 里面也有一个叫 PI 的常量,但是它的定义不同……

// b.h
constexpr double PI{3.14159265359};

现在问题是,我在自己的文件中写下 PI 这个名字的时候,就发生了重复定义的现象,即发生了命名冲突。

#include <iostream>

#include <a.h>
#include <b.h>
int main() {
    std::cout << PI; // Ambigious name 'PI', declarations in 'a.h' and 'b.h'
}

怎么办?难道我还需要手动去修改现成的库文件不成?这样做会很繁琐,而且会造成许多不良影响。命名空间就是为了解决这个问题而产生的。库编写者可以将自己声明引入的名字用命名空间包起来……

// a.h
namespace libA {
    constexpr float PI{3.14159f};
}

// b.h
namespace libB {
    constexpr double PI{3.14159265359};
}

先不用管这是什么语法,你只需要这样一通操作下来,你在写代码的时候就可以有效避免命名冲突了:

#include <iostream>

#include <a.h>
#include <b.h>
int main() {
    std::cout << libA::PI; // 这个是 a.h 中声明的那个 PI
    std::cout << libB::PI; // 这个是 b.h 中声明的那个 PI
    PI; // 编译错误,未指明命名空间
}

你可以用

命名空间名::名字

的方式来指定 名字 来自于哪一个命名空间。有时这个 :: 被称为作用域解析运算符,它是优先级最高的运算符。

运算符名称作用
a::b作用域解析运算符指明名字 b 来自于命名空间 a

当你明白命名空间的作用之后,就能解释 using namespace std; 的作用了。在第一章中,如果我们不写这句话,你就需要通过 std::cinstd::cout 这种方式来输入输出。这是因为,cincout 这两个东西声明在了 std 命名空间中。事实上,标准库中所有引入的名字都声明在 std 命名空间中。如果我们经常用标准库中的东西的话,这些 std:: 的前缀就会显得很啰嗦:

// 下面这一行是一个声明语句,引入了名字 a,就是它的类型标识有点长
std::priority_queue<std::string, std::vector<std::string>, std::greater<std::string>> a;

这个时候,就可以用 using 声明来简化。using 声明的语法有两种:

using namespace 命名空间名;
using 命名空间名::名字;

当写下第一种形式的 using 声明时,在这个声明所在作用域内,命名空间名 中所有名字的“命名空间前缀”都可以省略掉。这就是 using namespace std; 的由来了:有了它,就可以省去全部的 std::(因为这个声明写在全局作用域,因此任何声明点之后地方都可以省去)。第二种可以引入 命名空间名 中的部分名字,比如:

#include <iostream>
using std::cin;
using std::cout;
int main() {
    int a;
    cin >> a;
    cout << a << std::endl; // endl 未引入
}

我们说过 using namespace std; 不是一个好习惯,因为这容易造成命名冲突。因此这个时候,采用第二种引入部分常用名字的写法是比较可取的。

using namespace ... 的标准术语叫“using 指令”而非“using 声明”,但它仍然是一个声明,本文为了便于理解而采取了错误的术语。

所有命名空间都是可以嵌套的,如 A::B::C::d。所有命名空间都包含于全局命名空间中。当使用全局命名空间时,只需用两个冒号即可,如 ::answer ::printSum。若不引起歧义,则双冒号是可省略的。

其它

从第三行开始,就是函数声明和变量声明了(函数定义也是声明的一种)。这些没有什么可多说的,唯需记住如果要写一个可执行文件的话,你必须定义一个叫做 main 的函数,且返回值类型必须为 int

int main() {
    // [...]
}

main 函数非常特殊。比如标准不允许在任何地方调用它:

int main() {
    main(); // 非法
}

除此之外,main 函数还有一种特别的写法:

int main(int argc, char** argv) {
    // [...]
}

问题是目前的知识储备还不足以理解它,这里只是提及一下,之后会做更详细的说明。

总结

因此总地看下来,除去第一行的预编译指令外,其余的都是声明(using 声明、函数声明、变量声明等等)。因此我们说: C++ 代码实际上是由一系列声明构成的。

最近更新:
代码未运行