在讲清楚这件事情之前,先想一下这样一些问题:

  • 为什么要使用随机数函数rand需要在程序开头写了#include <math.h>?为什么要使用C的格式化输入函数printf又需要在程序开头写#include <stdio.h>?写不写有什么不同?而math.hstdio.h又有什么不同?
  • 为什么在一个代码文件里写两个main函数就会报错?
  • 为什么在一个项目的两个代码文件里,分别写两个main函数,也会报错,但是报的错和上一种情况还不一样?
  • 自己写的函数的时候需要提供函数的实现(执行代码)才能调用,那为什么用randprintf的时候不需要有实现就能直接用呢?

要回答这些问题,其实就是分别理解预处理、编译、链接各是什么。

对预处理、编译、链接这三个名词,大家不应该太陌生。各种教材上都会说,从代码到执行文件,会“经过预处理、编译、链接三个步骤”,分别会生成预处理文件啊模块文件啊中间文件啊之类的什么什么一大堆东西。可是大家在实践过程中,尤其是用Visual Studio的同学们,根本不知道这些中间过程体现在什么地方,只知道按一下运行按钮就万事大吉了。然而这种程度的理解,对进一步的程序设计学习,是很不利的。所以弄清楚实际发生了什么是很有必要的。

预处理、编译、链接分别是什么

预处理

在C和C++中,预处理过程是对代码的文本变换。而预处理指令是指#开头的行,这些行告诉预处理器怎么进行预处理。
这个笼统的讲法非常抽象,所以还是结合具体预处理指令来分析一下。
最常见,并且是C/C++标准中定义的预处理指令,主要是如下三组(但是标准中规定的预处理指令不止这三组):

#include
#define/#undef
#if(#ifdef)/#elif/#else/#endif

它们的功能分别是:

  • #include:把#include这一行用#include的文件内容替换掉。
  • #define/#undef:定义或取消宏,关于什么是宏,后面会进一步解释。
  • #if(#ifdef)/#elif/#else/#endif:条件编译,如果条件成立,则条件编译管理到的后续文本是有效的代码,否则是无效的文本,不参与代码生成。

预处理完之后,生成的是没有预处理指令的等效的.c或.cpp文件。

编译

对预处理好的代码进行“编译”
这一步骤里,从每个预处理好的代码文件里,生成函数。这一步骤中,不考虑不同源文件之间的重名问题,也不把函数调用对应到具体地址上,而是留个“占位”用符号。

链接

对编译生成的中间文件进行“连接”
这一步骤里,把前一步骤里的各个函数安排到程序(内存)的不同位置,然后把调用的“占位符号”换成对具体地址的调用。所以如果不同的代码文件里有同名的函数的实现(在C++里,是“同名且同参数表”),会无法安排地址,产生错误。
至于“把调用的‘占位符号’换成对具体地址的调用”的方法,普通函数直接用一条“执行”指令去调用,而inline(内联)函数会由编译器选择,是直接把编译生成的部分直接代入(这是内联的方式),或者按普通函数的方式。

例子

a.h

原始文件

#ifndef A_H
#define A_H
class A {
public:
  A();
  int x();
private:
  int y;
};
#endif //A_H

预处理结果:不直接参与
编译结果:不直接参与

a.cpp

原始文件

#include "a.h"
// 注释也会删掉的
A::A() : y(3) { }
int A::x()
{ return y; }

预处理结果:

class A {
public:
  A();
  int x();
private:
  int y;
};

A::A() : y(3) { }
int A::x()
{ return y; }

编译结果:

存在如下符号
  A::A()
  A::x()
存在如下符号的定义
  A::A()
  A::x()

main.cpp

原始文件

#include <iostream>
#include "a.h"
using namespace std;
int main() {
  A a;
  cout << a.x() << endl;
  return 0;
}

预处理结果:

iostream内容略过
class A {
public:
  A();
  int x();
private:
  int y;
};
using namespace std;
int main() {
  A a;
  cout << a.x() << endl;
  return 0;
}

编译结果:

存在如下符号
  A::A()
  A::x()
  main()
存在如下符号的定义
  main()
    调用A::A()
    调用A::x()

连接结果

0x1000  A::A()
0x1100  A::x()
0x1200  main()
  调用0x1000
  调用0x1100

模块化

头文件

头文件肯定是不能用.cpp或.c做扩展名的!
更加不能在被#include的文件里写函数实现(除非标明了inline,下一章会讲)!否则这个文件被#include两次之后,一个函数就有两个实现,应该用哪个?——即使两个实现是一样的也不行。
还有,.h文件里写using namespace std;是个不好的做法,这个应该放在.cpp里。否则会出现引用了.h文件的其他人无辜地被其不想要的std里的东西和自己的东西重名而冲突的可能性。

extern

extern用于标记一个符号是由其它地方提供实现的,而本代码行只用于声明一下这个符号是个什么东西。
函数声明的时候可以加extern,函数实现的时候不需要extern