当您使用Visual Studio 2019进行C++开发时,您将进入C++生态环境。以下是一些重要的概念和步骤:
C++程序的结构: 一个典型的C++程序包括源文件(.cpp)、头文件(.h)、编译后的目标文件(.obj)和最终的可执行文件(.exe)或库文件(.lib、.dll)。
第三方库的工作方式: 如果您需要使用第三方库,通常会将其源代码或者预编译的库文件(.lib、.dll)包含到您的项目中。第三方库通常会包含头文件和库文件,您需要将这些文件包含到您的项目中,并在编译和链接过程中告诉编译器和链接器去哪里寻找这些文件。
相互引用和VS配置: 在Visual Studio中,您可以通过项目属性来配置第三方库的引用。具体步骤包括:
C++生态环境的其他方面:
总的来说,C++生态环境包括了C++编译器、标准库、第三方库、开发工具等,通过合理配置和使用这些工具,您可以进行高效的C++开发。
????????当进行C++开发时,源码经历了多个阶段才能最终转化为可执行文件或库文件。以下是这个过程的详细步骤:
预处理阶段: 在预处理阶段,C++编译器会处理源文件(.cpp)中的预处理指令,比如#include和#define。预处理器会将头文件内容替换到源文件中,并进行宏替换等操作,生成一个经过预处理的源文件(通常是一个临时文件)。
编译阶段: 经过预处理的源文件会被编译器翻译成汇编代码(.asm文件),这个过程包括词法分析、语法分析、语义分析和优化等步骤。编译器将源码翻译成目标代码,生成一个或多个目标文件(.obj文件),每个目标文件对应源文件中的一个编译单元。
链接阶段: 在链接阶段,链接器会将多个目标文件以及所需的库文件(静态库或动态库)链接在一起,生成最终的可执行文件(.exe)或者库文件(.lib、.dll)。链接器会解析符号引用,处理重定位等工作,确保最终的可执行文件或库文件可以正确运行。
????????目标码(Object Code)是编译器生成的中间文件,包含了机器代码、符号表、重定位信息等。目标码是编译后的源码的中间表示,还不能被直接执行。目标码的作用是为了后续的链接阶段使用,链接器会将多个目标文件合并成最终的可执行文件或库文件。
????????总的来说,C++程序从源码到目标码再到可执行文件或库文件经历了预处理、编译和链接三个阶段。目标码是编译器生成的中间文件,用于链接器生成最终的可执行文件或库文件。
????????编写完成一个C/C++程序后,想要运行起来,必须要经过四个步骤:预处理、编译、汇编和链接。每个步骤都会生成对应的文件,如下图所示(注意后缀名):
?
为了后面过程的介绍更方便,这里对C++编译过程中涉及的一些常用名词进行解释。
? ? GNU:一个操作系统,具体内容不重要,感兴趣可以参考:
简单来说,gcc调用了GCC中的C Compiler,而g++调用了GCC中的C++ Compiler。 - 对于?*.c
?和?*.cpp
?文件,gcc分别当作 c 和 cpp文件编译,而g++则统一当作cpp文件编译。
gcc/g++常用指令选项:
GDB(gdb)全称“GNU symbolic debugger”,是 Linux 下常用的程序调试器。 为了能够使用 gdb 调试,需要在代码编译的时候加上-g
,如
g++ -g -o test test.cpp
本文中只演示从源代码生成可执行二进制文件的过程,暂不涉及调试过程。调试的配置会在另一篇文章中专门介绍。
????????初次使用Visual C++的时候一定有经历过项目的配置,肯定会对预编译头,链接器之类的东西心生疑惑,敲代码就敲代码,怎么有这么多要设置的东西。还有看着Windows版本的程序和Linux版本的编译完成后的文件夹也会疑惑,编译完除了程序怎么还有一堆.obj和.o文件。
????????首先我们要明白一个流程,程序要运行起来,必须要经过四个步骤:预处理、编译、汇编和链接。每个步骤都会生成对应的文件。像下图一样,接下来我会分成四个步骤描述一下每个步骤都会做什么事
????????预编译如其名,就是编译前的一些准备工作。代码文件放到编译器面前,他肯定也不知道怎么去读,看到头文件他也不知道头文件里是什么,所以预编译就要把一些#define的东西完成文本替换,然后#include的文件里的内容复制到.cpp文件里,如果.h文件里还有.h文件,那递归展开,注释这些编辑器也不管的,完全是给程序员看的。简而言之,就是剔除掉编译器看不懂的东西,然后把所有头文件展开的一个过程。
????????编译只是把我们写的代码转为汇编代码,它的工作是检查词法和语法规则,所以,如果程序没有词法或则语法错误,那么不管逻辑是怎样错误的,都不会报错。
????????汇编代码转换机器码,这个阶段,非底层的程序员不需要考虑, 编译器不会搞错的。也与c/c++开发者无关,但是我们可以利用反汇编来调试代码,学习汇编语言依然是必备的。
????????配置链接库的重要性就体验在这里,配置的静态链接库还是动态链接库都会在这部分和最后的程序链接到一起。
????????在这之前引用百度过来的一段话介绍下静态链接和动态链接的区别
? ?(1)静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器或汇编器生成)链接到一块生成可执行程序。静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。
(2)动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。
????????C语言代码经过编译以后,并没有生成最终的可执行文件(.exe 文件),而是生成了一种叫做目标文件(Object File)的中间文件(或者说临时文件)。目标文件也是二进制形式的,它和可执行文件的格式是一样的。对于 Visual C++,目标文件的后缀是.obj
;对于 GCC,目标文件的后缀是.o
。这就是一开始所说的编译完一堆.obj和.o文件的来源。
????????目标文件经过链接(Link)以后才能变成可执行文件。既然目标文件和可执行文件的格式是一样的,为什么还要再链接一次呢,因为编译只是将我们自己写的代码变成了二进制形式,它还需要和系统组件(比如标准库、动态链接库等)结合起来,这些组件都是程序运行所必须的。链接(Link)其实就是一个“打包”的过程,它将所有二进制形式的目标文件和系统组件组合成一个可执行文件。完成链接的过程也需要一个特殊的软件,叫做链接器(Linker)。
????????这里还要讲一个东西,C++程序编译的时候其实只识别.cpp文件。每个cpp文件都会分别编译一次,生成一个.o或者.obj文件。这个时候,链接器除了将目标文件和系统组件组合起来,还需要将编译器生成的多个.o或者.obj文件组合起来。
本节内容用下面的简单C++工程做演示。示例的文件结构如下:
|—— include
|—— func.h
|—— src
|—— func.cpp
|—— main.cpp
其中,main.cpp
是主要代码,include/func.h
是自定义函数的头文件,src/func.cpp
是函数的具体实现
各个文件的内容如下:
// main.cpp
#include <iostream>
#include "func.h"
using namespace std;
int main(){
int a = 1;
int b = 2;
cout << "a + b = " << sum(a, b) << endl;;
return 0;
}
// func.h
#ifndef FUNC_H
#define FUNC_H
int sum(int a, int b);
#endif
// func.cpp
#include "func.h"
int sum(int a, int b) {
return a + b;
}
????????预处理,顾名思义就是编译前的一些准备工作。
????????预编译把一些#define
的宏定义完成文本替换,然后将#include
的文件里的内容复制到.cpp
文件里,如果.h
文件里还有.h
文件,就递归展开。在预处理这一步,代码注释直接被忽略,不会进入到后续的处理中,所以注释在程序中不会执行。
????????gcc/g++的预处理实质上是通过预处理器cpp
(应该是c preprocess的缩写?)来完成的,所以我们既可以通过g++ -E
,也可以通过cpp
命令对main.cpp
进行预处理:
g++ -E -I include/ main.cpp -o main.i
# 或者直接调用 cpp 命令
cpp -I include/ main.cpp -o main.i
????????上述命令中: -?g++ -E
?是让编译器在预处理之后就退出,不进行后续编译过程,等价于cpp
指令 -?-I include/
用于指定头文件目录 -?main.cpp
是要预处理的源文件 -?-o main.i
用于指定生成的文件名
????????预处理之后的程序格式为?*.i
,仍是文本文件,可以用任意文本编辑器打开。
执行完预处理后的文件结构如下:
|—— include
|—— func.h
|—— src
|—— func.cpp
|—— main.cpp
|—— main.i
编译 只是把我们写的代码转为汇编代码, 它的工作是检查词法和语法规则,所以,如果程序没有词法或则语法错误,那么不管逻辑是怎样错误的,都不会报错。
编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程。
编译的指令如下:
g++ -S -I include/ main.cpp -o main.s
与预处理类似,上述命令中: -?g++ -S
是让编译器在编译之后停止,不进行后续过程 -?-I include/
用于指定头文件目录 -?main.cpp
是要编译的源文件 -?-o main.s
用于指定生成的文件名
编译完成后,会生成程序的汇编代码main.s
,这也是文本文件,可以直接用任意文本编辑器查看。
执行完编译后的文件结构如下:
|—— include
|—— func.h
|—— src
|—— func.cpp
|—— main.cpp
|—— main.i
|—— main.s
汇编过程将上一步的 汇编代码(main.s
) 转换成机器码(machine code), 这一步产生的文件叫做目标文件(main.o
), 是二进制格式。
gcc/g++的汇编过程通过?as
?命令完成,所以我们可以通过g++ -c
或as
命令完成汇编:
g++ -c -I include/ main.cpp -o main.o
# 或者直接调用 as 命令
as main.s -o main.o
上述指令中: -?g++ -c
让编译器在汇编之后退出,等价于?as
?-?-I include/
仍是用于指定头文件目录 -?main.cpp
是要汇编的源文件 -?-o main.o
用于指定生成的文件名
汇编这一步需要为每一个源文件(本文示例代码中为main.cpp
、func.cpp
)产生一个目标文件。因此func.cpp
也需要执行一次这个汇编过程产生一个func.o
文件:
# 可以用 g++ -c 命令一步生成 func.o
g++ -c -I include/ src/func.cpp -o src/func.o
# 当然也可以按照上面的预处理、编译、汇编三个步骤生成func.o
到了这一步,代码的文件结构如下:
|—— include
|—— func.h
|—— src
|—— func.cpp
|—— func.o
|—— main.cpp
|—— main.i
|—— main.s
|—— main.o
C/C++代码经过汇编之后生成的目标文件(*.o
)并不是最终的可执行二进制文件,而仍是一种中间文件(或称临时文件),目标文件仍然需要经过链接(Link)才能变成可执行文件。
既然目标文件和可执行文件的格式是一样的(都是二进制格式),为什么还要再链接一次呢?
因为 编译只是将我们自己写的代码变成了二进制形式,它还需要和系统组件(比如标准库、动态链接库等)结合起来,这些组件都是程序运行所必须的。
链接(Link)其实就是一个“打包”的过程,它将所有二进制形式的目标文件(.o)和系统组件组合成一个可执行文件。完成链接的过程也需要一个特殊的软件,叫做 链接器(Linker)。
此外需要注意的是: C++程序编译的时候其实只识别.cpp
文件,每个cpp文件都会分别编译一次,生成一个.o
文件。这个时候,链接器除了将目标文件和系统组件组合起来,还需要将编译器生成的多个.o
或者.obj
文件组合起来, 生成最终的可执行文件(Executable file)。
以本文中的代码为例,将func.o
和main.o
链接成可执行文件main.out
,指令如下:
g++ src/func.o main.o -o main.out
-o main.out
用于指定生成的可执行二进制文件名g++
自动链接了系统组件,所以我们只需要把自定义函数的目标文件与main.o
链接即可。运行main.out
,结果如下:
./main.out
a + b = 3
从上面的介绍可以看出,从C++源代码到最终的可执行文件的中间过程并不简单。了解预处理、编译、汇编、链接各个步骤的作用有助于我们处理更加复杂的项目工程。
不过也不必被这么麻烦的编译过程劝退,当我们编译简单.cpp
代码时,
// hello.cpp
#include <iostream>
using namespace std;
int main(){
cout << "Hello, world!" << endl;
return 0;
}
仍然可以直接使用g++
命令生成可执行文件,而不必考虑中间过程:
g++ hello.cpp -o hello
./hello
Hello, world!