[CMake翻译]C/C++编译过程

1,835 阅读6分钟

原文地址:thatonegamedev.com/cpp/c-cpp-c…

原文作者:thatonegamedev.com/

发布时间:2021-01-02

C和C++项目和应用的编译过程与其他大多数语言有些不同。首先我需要为那些不知道的人提一下--有两种类型的编程语言--解释型和编译型。

解释型语言(也就是脚本语言)是在运行时进行评估的。这比较慢,因为计算机不理解文本--它理解某些命令和计算操作。这意味着,我们写的文本语言必须首先被读取并翻译成计算机命令和计算,然后交给CPU执行。

编译后的语言会产生某种中间字节码(类似汇编的指令)。汇编是处理器语言--它由一些命令和内存管理组成。直接用汇编语言编写是不方便的,所以更高层次的抽象是通过将人类可以理解的文本编译成计算机的命令来实现的。

C/C++编译器是如何工作的

在这篇文章中,我不会直接谈论每个编译器如何处理这个过程,所以它将是纯理论性的。其他语言的编译器更容易工作,因为它们会更容易地完成你所期望的工作。而C语言的编译过程则分为两部分--编译和链接。编译过程会把一个文件,并产生该文件的CPU指令。

不过当你做一个应用程序的时候,你很少有一个单一的文件,所以你还要把所有编译后的文件组合起来(链接)成为一个可执行文件。这就是为什么第二部分被称为链接器的原因。

如果没有理解错...

这个过程中,如果你不完全理解,就很容易犯错。在每一行C/C++代码中,如果你执行了什么东西,将被编译的单文件需要知道它的存在。例如,如果你读过我的《C++入门》一文,你就会知道,函数可以有一个定义它的签名的声明和一个描述函数执行的实际计算的定义。函数的签名包括函数的名称是什么,它的返回内容和它接受的参数类型。其实不仅函数可以单独声明和使用,还有全局变量、结构、类等。所有这些组合起来也被称为符号。一个符号在使用前必须有一个声明。

如果我们看C++的介绍例子,你可以把函数定义放在一个单独的文件中。但是为了让使用该函数的文件知道它的存在,你还需要在使用该函数的文件中加入一个声明。这必须要做,因为文件是分开编译的。这就是任何符号类型的工作原理,也是最常见的错误之一。

你需要考虑的第二件事是链接过程。你可以让一个文件只用函数声明来编译。它会产生机器代码(汇编),它会引用这个函数的执行,但它就像一个空的空间,引用一些它并不确切知道的东西。链接器的工作是在所有编译文件中找到符号的定义,并使用它。这里有两种常见的错误--符号未定义和符号多次定义。你必须注意避免这两种错误,因为你的程序将无法产生可执行文件或库。

头文件和使用外部库

当一个外部库被编译使用到自己的项目中时,你会链接这个库的编译和链接过程的输出文件。但是要想编译你的程序使用该库的函数,你就需要在自己的代码中添加这些库函数的声明。事实上,即使在你自己的代码中有多个文件,你也可以在你写的每个.c或.cpp文件中添加函数的声明。

这就是重复而艰难的方法。语言提供了一种方法,使用#include "<filepath>"语句为你自动复制和粘贴文件内容。你只要写出一个文件的路径,编译器就会读取这个文件,并复制它的内容,把include语句替换掉。这些文件最常见的名称是头文件。头文件是通常以.h或.hpp为扩展名的文件,用于简化符号的声明。头文件应该只包含声明,因为如果你在其中有定义,并且它们被包含在两个翻译单元中(两个.c或.cpp文件),你会最终出现符号定义多次的错误。

当你声明了一个符号并使用了它(如果你有声明而没有使用,就没有问题),但在库文件或编译的任何翻译单元中没有这个符号的实际实现或定义时,就会发生符号未定义类型的错误。

头部防护

另一个常见的错误是头可以被重新包含。您可以包含一个包含另一个头的头,并最终在您的文件中重复声明。这就是为什么头卫士存在的原因。头保护应该像这样包围你的头文件中的所有代码。

// ExampleHeader.h

#ifndef SOME_UNIQUE_NAME_HERE
#define SOME_UNIQUE_NAME_HERE
 
// your declarations (and certain types of definitions) here
 
#endif

这至少是最安全的编译头文件的方式。一些但不是所有的编译器也支持另一种叫做#pragma once的头文件保护。它被放在头文件的顶部。

// ExampleHeader.h
#pramga once

// your declarations (and certain types of definitions) here

C++中的互连类

在C++中,你也可以有两个类耦合在一起。例如在游戏中,你可以有一个实体,它有多个组件--它需要更新它们,查询它们,添加它们,删除它们等等。但是组件也应该知道拥有它们的实体--因为它们可以调用方法来寻找实体拥有的其他组件。要做到这一点,两个类都必须知道对方,只有当你有多个文件时,你才能真正干净利落地实现这一点。

// Entity.h
#pragma once

# We need forward declaration
# this declaration only states the existance of the class
class Component;

class Entity {
public:
  std::vector<Component> Components;
}
// Component.h
#pragma once

// There is no problem to directly include this here 
// But you cannot have both file include each other as it will loop around itself
// as the compiler would try to endlessly include both files.
#include "Entity.h"

// You can expand the declaration of the component class
class Component {
  // ...some component stuff...
}

在实际编译的实现文件中,你可以同时包含这两个头文件,当定义函数时,他们将拥有两个类的全部知识。

结束语

这就是关于编译过程的内容。在其他编译语言中,如C#或Java,这被简化为不包含其他文件,而是直接包含其中的符号,这解决了最常见的错误。在C++中,它是有点困难,但也更强大。当使用外部库时,他们会提供自己的头文件,这些头文件会声明所有的函数,你只需要在你的文件中包含这些函数。如果你要写一个供其他人使用的库,记得把所有公众使用的函数都分开放在一个公共头文件中,这个头文件将与编译后的库一起发布。


www.deepl.com 翻译