为了方便演示和讲解,在这里提前准备好几个简单的文件:
test.cpp
test.h
main.cpp
文件内容如下:
main.cpp
#include "test.h"
int main (int argc, char **argv)
{
Test t;
t.hello();
return 0;
}
test.h
//test.h
#ifndef _TEST_H_
#define _TEST_H_
class Test
{
public:
Test();
void hello();
~Test();
};
#endif //TEST
test.cpp
//test.cpp
#include "test.h"
#include
using namespace std;
Test::Test()
{
}
void Test::hello()
{
cout << "hello" << endl;
}
Test::~Test()
{
}
一个完整的C++编译过程(例如g++ a.cpp生成可执行文件),总共包含以下四个过程:
g++ -E
执行g++ -S
执行as
或者g++ -c
执行g++ xxx.o xxx.so xxx.a
执行可以通过添加
g++ --save-temps
参数,保存编译过程中生成的所有中间文件
下面对这四个步骤进行逐一讲解
可以使用g++ -E
让g++ 在预处理之后停止编译过程,生成 *.ii
(.c
文件生成的是*.i
) 文件。
因为上面写的main.cpp
中没有任何预编译指令,所以预编译生成与源文件几乎没有差别。这里预编译一下test.cpp
文件
g++ -E test.cpp test.h -o test.ii
可以打开test.ii查看,刚刚的main.cpp文件预编译完成后的内容:
预编译完成后,#include
引入的内容 被全部复制进预编译文件中,除此之外,如果文件中有使用宏定义也会被替换处理。
- 预编译过程最主要的工作,就是宏命令的替换
#include
命令的工作就是单纯的导入,这里其实并不限制导入的类型,甚至可以导入.cpp
、.txt
等等。- 感兴趣的同学可以预编译一个包含Qt中信号的文件,会看到预编译之后:
- emit直接成了空。发射信号实质就是一次函数调用;
- 头文件中的
signals:
也被替换成了protected:
(Qt5被替换为public:
)- 以及Qt中其他的宏定义都在预编译时被处理如:
Q_OBJECT
Q_INVOKEABLE
等
可以使用-S 选项进行查看,该选项只进行编译而不进行汇编,生成汇编代码。
g++ -S main.ii -o main.s
汇编代码中生成的是和CPU架构相关的汇编指令,不同CPU架构采用的汇编指令集不同,生成的汇编代码也不一样:
有两种方式:
g++ -c *.s -o *.o
as *.s -o *.o
到编译阶段,代码还都是人类可以读懂的。汇编这一阶段,正式将汇编代码生成机器可以执行的目标代码,也就是二进制码。
# 编译
g++ -c main.s -o main.o
# 汇编器编译
as main.s -o main.o
也可以直接使用as *.s
, 将执行汇编、链接过程生成可执行文件a.out, 可以像上面使用-o 选项指定输出文件的格式。
修改main.cpp
的内容,引用Test
类
#include "test.h"
int main (int argc, char **argv)
{
Test t;
t.hello();
return 0;
}
生成目标文件:
g++ test.cpp -c -o test.o
g++ main.cpp -c -o main.o
链接生成可执行文件:
g++ main.o test.o -o a.out
链接的过程,其核心工作是解决模块间各种符号(变量,函数)相互引用的问题,更多的时候我们除了使用.o意外,还将静态库和动态库链接一同链接生成可执行文件。
对符号的引用本质是对其在内存中具体地址的引用,因此确定符号地址是编译,链接,加载过程中一项不可缺少的工作,这就是所谓的符号重定位。本质上来说,符号重定位要解决的是当前编译单元如何访问「外部」符号这个问题。
接下来我们先讲解如何将源文件编译成动态库和静态库,然后再讲述如何在链接时链接我们编译好的库。
大型项目中不可能使用一个单独的可执行程序提供服务,必须将程序的某些模块编译成动态或静态库:
使用ar
命令进行“归档”(.a的实质是将文件进行打包)
ar crsv libtest.a test.o
r
替换归档文件中已有的文件或加入新文件 (必要)c
不在必须创建库的时候给出警告s
创建归档索引v
输出详细信息使用g++ -shared
命令指定编译生成的是一个动态库
g++ test.cpp -fPIC -shared -Wl,-soname,libtest.so -o libtest.so.0.1
shared
:告诉编译器生成一个动态链接库-Wl,-soname
:指示生成的动态链接库的别名(这里是libtest.so
)-o
:指示实际生成的动态链接库(这里是libtest.so.0.1
)-fPIC
Position Independent Code
, 用于生成位置无关代码(看不懂没关系,总之加上这个参数,别的代码在引用这个库的时候才更方便,反之,稍不注意就会有各种乱七八糟的报错)。在gcc中,如果指定
-shared
不指定-fPIC
会报错,无法生成非PIC的动态库,不过clang
可以。
库中函数和变量的地址是相对地址,不是绝对地址,真实地址在调用动态库的程序加载时形成。
动态库的名称有别名(soname),真名(realname)和链接名(linker name)。
libQt5Core.5.7.1
-lQt5Core
注意:
- 生成的库文件总是以
libXXX
开头,这是一个约定,因为在编译器通过-l
参数寻找库时,比如-lpthread会自动去寻找libpthread.so
和libpthread.a
。- 如果生成的库并没有以
lib
开头,编译的时候仍然可以连接到,不过只能以显示加在编译命令参数里的方式链接。例如g++ main.o test.so
编译C++的程序可以分为动态编译和静态编译两种
链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。这种称为静态编译,静态编译中使用的库就是静态库(*.a
或*.lib
)生成的可执行文件在运行时不需要依赖于链接库。
g++ main.o libtest.a
编译完成后可以运行a.out
查看效果,通过ldd
命令查看运行a.out
所需依赖,可以看到静态编译的程序并不依赖libtest库。
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。
动态编译中使用的库就是动态库(*.so
或*.dll
)
动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。
动态库在链接过程中涉及到加载时符号重定位
的问题,感兴趣的同学参看链接:动态编译原理分析
g++ main.o libtest.so
编译完成后可以运行a.out
查看效果,通过ldd
命令查看运行a.out
所需依赖
从上面的截图中,我们已经看到了刚才的程序运行报错,原因是找不到动态链接库libtest.so
。
这个报错的解决方案有很多例如:
LD_LIBRARY_PATH=. ./a.out
那么明明编译成功,运行时为什么会找不到库?为了弄清这个问题,我们需要对链接动态库的过程有一个更深入的理解。
我们在main.cpp
中明确引用到了Test
类,所以在编译进行到最后阶段,链接的时候。如果在所有参与编译的文件中没能检索到Test
这个符号,则会报错未定义的引用。
所以在编译过程中必须能够找到包含Test
符号的文件,可以是.o
、.a
、或者.so
。
如果是.o
或者.a
,也就是静态链接,那么它会将.o或者.a中的内容一起打包到生成的可执行文件中,生成的可执行文件可以独立运行不受任何限制。
而如果是.so
这种动态链接库,就比较麻烦了。链接器将不会把这个库打包到生成的可执行文件里,而仅仅只会在这里记录一个地址,告诉程序,如果遇到Test
符号,你就去文件libtest.so
的第三行第五列(打个比方,实际是一个相对的内存地址)找它的定义。
综上所述
main.cpp
的时候,必须能够找到libtest.so
的动态库,记录下Test
符号的偏移地址。libtest.so
,然后寻址找到Test
-L
与 -l
链接器参数,就是指定链接时去(哪里)找(什么)库。
-l
,代表链接哪个库,会自动检索lib
开头的对应库名。 例如-lpthread
,-lQt5Core
。会自动检索libpthread.so,libpthread.a,libQt5Core.so,libQt5Core.a
-L
,指定去哪里找库文件。例如指定:-L/home/threedog/test
,则在编译时会优先检索/home/threedog/test/libpthread.so
等文件。LIBRARY_PATH
/lib
/usr/lib
/usr/local/lib
这是当初编译 gcc时写在程序内的-lxxxx
所以以上的编译命令,可以通过多种方式通过编译:
g++ main.o libtest.so
,或g++ main.o ./libtest.so
g++ main.o /home/threedog/test/libtest.so
g++ main.o -ltest -L.
,或g++ main.o -ltest -L/home/threedog/test/
LIBRARY_PATH=. g++ main.o -ltest
,或LIBRARY_PATH=/home/threedog/test/ g++ main.o -ltest
libtest.so
拷贝到/usr/lib
目录下去。通过上面的方法编译出的a.out,运行会报错,通过ldd命令查看,发现编译时链接的libtest.so
成了not found
这就引出了第二个问题:如何让程序运行的时候能够找到对应的库。
-Wl,-rpath
就是做这个事情的:-Wl
代表后面的这个参数是一个链接器参数,-rpath
+库所在的目录,会给程序明确指定去哪里找对应的库。
手动将一个目录指定成了ld的搜索目录。
另外,也可以通过在环境变量LD_LIBRARY_PATH
里添加路径的方式成功运行
运行时库的查找顺序:
其实rpath和rpath-link都是链接器ld的参数,不是gcc的。
rpath-link
和rpath
只是看起来很像,可实际上关系并不大,rpath-link
和-L
一样也是在链接时指定目录的。
rpath-link
的作用,在我们的这个实例中体现不出来。
例如你上述的例子指定的需要libtest.so
,但是如果 libtest.so
本身是需要 xxx.so
的话,这个 xxx.so
我们你并没有指定,而是 libtest.so
引用到它,这个时候,会先从 -rpath-link
给的路径里找。
rpath-link
指定的目录与并运行时无关。
上面提到了编译时链接库的查找顺序和运行时动态库的检索顺序,顺便再提一下C++编译时头文件的检索顺序:
#include
只在默认的系统包含路径搜索头文件#include"file.h"
首先在当前目录以及-I
指定的目录搜索头文件, 若头文件不位于当前目录, 则到系统默认的包含路径搜索顺序:
CPLUS_INCLUDE_PATH
(C程序使用的是C_INCLUDE_PATH
)以上,就是对gcc参数的一些详细总结,下面根据上面的讲解解决几个常遇到的疑问:
参考链接: