编译的时候发生了什么?
通常我们会用这样的命令来编译一个 cpp 文件:
1 |
g++ main.cpp -o a.out |
我们教材告诉我们编译的过程是“预处理-编译-链接”。但是这显然难以让我们清楚理解这个问题。
就以最简单的 hello world 文件为例:
1 2 3 4 5 6 |
#include <iostream> int main() { std::cout << "nihao\n"; return 0; } |
在这里 g++ 实际上是把 main.cpp 和 libstdc++ 做了链接。
使用 objdump -D a.out | less
查找 main 函数的位置:
1 2 3 4 5 6 7 8 9 10 11 12 |
0000000000001149 <main>: 1149: f3 0f 1e fa endbr64 114d: 55 push %rbp 114e: 48 89 e5 mov %rsp,%rbp 1151: 48 8d 05 ac 0e 00 00 lea 0xeac(%rip),%rax # 2004 <_IO_stdin_used+0x4> 1158: 48 89 c6 mov %rax,%rsi 115b: 48 8d 05 de 2e 00 00 lea 0x2ede(%rip),%rax # 4040 <_ZSt4cout@GLIBCXX_3.4> 1162: 48 89 c7 mov %rax,%rdi 1165: e8 e6 fe ff ff call 1050 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt> 116a: b8 00 00 00 00 mov $0x0,%eax 116f: 5d pop %rbp 1170: c3 ret |
我们发现主函数调用了 std::basic_ostream& operator<<(std::basic_ostream&, const char*);
这个函数,我们继续找这个函数的定义:
1 2 3 4 |
0000000000001050 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>: 1050: f3 0f 1e fa endbr64 1054: ff 25 76 2f 00 00 jmp *0x2f76(%rip) # 3fd0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@GLIBCXX_3.4> 105a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) |
我们会发现他只是一个跳转。进一步的,我们使用 ldd 查看程序依赖库:
1 2 3 4 5 6 7 |
fantasy@Kotomi:~$ ldd a.out linux-vdso.so.1 (0x00007fff23bf8000) libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f0132158000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0131f46000) libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f0131e5d000) /lib64/ld-linux-x86-64.so.2 (0x00007f01323e1000) libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f0131e30000) |
我们会发现 a.out 链接了 libstdc++.so。
单文件编译虽然方便,但是有缺点。当我们的项目变大的时候,会不断放大这个缺点。所以我需要多文件编译。我们可以通过 g++ -c hello.cpp -o hello.o
将 hello.cpp
编译成中间文件,最后只需要链接这个 .o 文件即可。
但是这同样存在问题,当我们的文件依赖关系变多的情况下,我们不可能每一次都重写编译指令。这就有了 makefile
,只需要写出文件的依赖关系即可。不过,makefile
不跨平台,而且编写的时候比较头疼,这就有了 cmake
。
什么是库(library)
考虑我们编写代码的过程,如果我们使用模块化编程的思想,那么我们会有一些头文件。最简单的方式就是 head-only
。不过这也会导致一些问题,也就是说当我们的头文件过于庞大的时候,每一次我们都需要重新编译一遍程序。我们会发现 boost
库的编译很慢,就是这个原因(当然这是由于模板是不能分离头文件和源文件导致的,如果我们需要模板编程,那么我们必须在头文件中就有实现)。
所以,我们可以分离声明和定义。也就是说头文件只有声明的作用,让编译器自动去连接函数的声明。这样我们把源文件编译成库,就可以让多个文件使用。
库分为动态库与静态库。其中静态库会直接把代码插入可执行文件,而动态库则是在可执行文件中加入一个“插桩”函数,当程序执行到这里的时候则会将 .dll 文件中的函数加载到内存中。
(等待施工)
发表回复