Skip to main content

编译、链接、静态库、动态库

1. 前言

在课堂中,我们运行一个 C++ 程序的步骤通常是这样的:在文件中写好程序,然后点击 “调试” 或者 “执行”,一个黑色的方框就会弹出来。

实际上,从 C++ 源代码文件到可执行文件的过程是十分复杂的,Visual Studio 等现代化的IDE(Integrated Development Environment,集成开发环境)掩盖了程序构建的复杂流程。本节我们就以 Linux 中的 C++ 程序为例,简略介绍 C++ 工程中的一些概念。

2. 从.cpp到.exe —— C/C++ 程序的构建过程

C/C++ 程序生成一个可执行文件的过程可以分为 4 个步骤: 预处理(Preprocessing)、编译(Compiling)、汇编(Assembly)和链接(Linking)

  • 预处理:在编译器处理程序之前完成头文件的包含,宏扩展,条件编译,行控制等操作。
  • 编译:通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将源文件代码翻译成等价的汇编代码。
  • 汇编:将汇编语言代码翻译成目标机器指令,生成目标文件。
  • 链接:将有关联的目标文件(以及库)相组合为一个可执行文件。

接下来,我们将介绍每一步发生的故事。

2-1. 编译工具

针对不同的应用场景和平台,各大厂家设计了不同的 C++ 编译工具。

  • MSVC(Microsoft Visual C++):MSVC 是微软开发的 C++ 开发工具,Visual Studio 就内置了 MSVC。
  • GCC(GNU Compiler Collection):GCC 是由 GNU 开发的一套编译工具,支持 C、C++、Fortran、Go 等一系列语言。本教程中我们使用的编译工具就是 GCC。

GCC 提供给用户的前端程序为 gcc (针对C)和 g++ (针对C++)。它们的区别详见 gcc vs g++

在 Linux(Ubuntu) 平台上,可以使用以下指令安装上述工具:

sudo apt install gcc g++

此外还有 Clang、NVCC 等编译工具。不同的编译工具对 C++ 的支持不尽然相同,此处不再赘述。

2-2. 预处理

C++ 程序在预处理阶段会执行以下操作:宏的替换、头文件的插入、删除条件编译中不满足条件的部分。

g++ –E test.cpp –o test.i

2-3. 编译

C++ 程序在编译阶段会将 C++ 文件转换为汇编文件。

# from .i file
g++ –S test.i –o test.s
# from .cpp file
g++ –S test.cpp –o test.s

2-4. 汇编

汇编语言文件经过汇编,生成目标文件 .o 文件(二进制文件,机器码),每一个源文件都对应一个目标文件。

# from .s file
g++ –c test.s –o test.o
# from .cpp file
g++ –c test.cpp –o test.o
g++ -c main.cpp -o main.o

生成的 test.o 和 main.o 文件不能直接打开,你可以使用 readelf -a file 阅读其信息。

2-5. 链接

将每个源文件对应的目标.o文件链接起来,就生成一个可执行程序文件。

g++ test.o main.o -o main

当然,如果想要使用 .cpp 文件一步到位生成可执行文件,可以使用以下指令:

g++ test.cpp main.cpp -o main

在 Linux 系统,可执行文件一般是没有后缀名的。

2-6. 语法总结

g++ 和 gcc 工具中使用的一些命令行参数:

  • -E 只进行预处理
  • -S 只进行编译
  • -c 只生成目标文件
  • -o file 指定输出文件的名称。我们约定:.i 为预处理后的文件,.s 为汇编文件,.o 为目标文件。

3. 静态库和动态库

出于便于复用、封装细节或防止源码泄露等原因,在实际应用过程中,我们需要把 C++ 源码封装为库(library)。

根据其行为不同,可以将库分为 静态库 (static library)和 动态库 (shared library)。

3-1. 静态库

静态库的代码在编译的过程中,会被直接载入到可执行文件中。这样做的好处是:可执行文件在执行时,不再需要静态库本身。但缺点也显而易见:生成的可执行文件的体积会比较大。

Linux 平台下静态库的后缀通常为 .a ,命名方式通常为 libxxx.a ; Windows 平台下静态库的后缀通常为 .lib

在 Linux 平台上生成静态库,并使用静态库链接形成可执行文件的方法为(了解即可):

# generate static lib
ar crv libtest.a test.o
# link to generate the executable file
g++ -static main.cpp -L . -ltest -o main_shared

3-2. 动态库

动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。这就带来了一个明显的好处:不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,减小了各个模块之间的耦合程度,也减小了可执行文件的体积。然而,这也要求用户的电脑上需要同时拥有可执行文件和动态库,也有可能因为版本不匹配等问题发生 DLL Hell 等问题。

Linux平台下动态库的后缀通常为 .so ,命名方式通常为 libxxx.so ; Windows平台下动态库的后缀通常为 .dll

在Linux平台上生成动态库,并使用动态库链接形成可执行文件的方法为(了解即可):

# generate shared lib
g++ test.cpp -I ./ -fPIC -shared -o libtest.so
# move the shared library to system
sudo mv libtest.so /usr/local/lib
# refresh
sudo ldconfig
# link to generate the executable file
g++ main.cpp -L . -ltest -o main_shared

4. 最后

在本节中,我们只介绍了如何在 Linux 平台上生成和使用静态库、动态库。实际上,利用 Visual Studio 也可以便捷地在 Windows 平台上生成静态库、动态库。

感兴趣的同学可参考以下:

GCC官网

learncpp

演练:创建并使用静态库

演练:创建和使用自己的动态链接库