目录
一、头文件
1、gcc查找头文件
2、gcc与g++的区别
二、库
1、简介
2、实验
2.1、静态库的编写
2.2、动态库的编写
2.3、库的使用
2.4、链接库
一、头文件
- 头文件存在的目的是为了把接口和实现分离,便于多文件编程的组织,例如:
- 在多文件的项目中,把函数声明都集中到若干头文件中,在源文件中引用它们,便于跨文件的函数调用。
- 在使用库的时候,首先需要在源代码中引用头文件,然后在链接步骤中链接需要的库文件。
- 在提供库的同时,也需要提供库的使用接口(头文件),通过头文件中的类和函数声明,用户可以知道如何使用该库。
1、gcc查找头文件
- gcc在编译过程中的预处理环节需要处理 include的头文件,那它是如何找到头文件的呢?
- gcc有专门的查找头文件的选项:-I path,path是一个路径。如果使用多个路径需要加多个 -I,例如:
-
gcc hello.c -I mudir1 -I mydir2
-
- 如果直接写完整路径加文件名,就不存在查找文件的问题。但如果使用的是不完整的路径加文件名,则存在查找顺序的问题。
- 在 include语句中,双引号和尖括号引用头文件的查找顺序有一点区别:
- ""双引号 include的查找顺序:
- 使用 #include的源文件所在的路径。
- -I指定的路径。
- 环境变量 CPATH、C_INCLUDE_PATH或 CPLUS_INCLUDE_PATH包含的路径。
- 内定路径。
- <>尖括号 include的查找顺序:
- -I指定的路径。
- 环境变量CPATH、C_INCLUDE_PATH或CPLUS_INCLUDE_PATH包含的路径。
- 内定路径。
- 【注】查找顺序是重要的,如果两个路径下有同名的头文件,gcc会使用先找到的那个,这很可能导致 bug。
- 环境变量 CPATH被 c/c++共用,C_INCLUDE_PATH或 CPLUS_INCLUDE_PATH只在处理相应的文件时使用。
- 内定路径是由 gcc和平台决定的,我们无法改变。
- ""双引号 include的查找顺序:
- 可以使用下面的命令查看当前 gcc 使用的头文件搜索路径(环境变量和内定路径):
-
# c `gcc -print-prog-name=cc1` -v # c++ `gcc -print-prog-name=cc1plus` -v # -v选项,会打印编译过程的详细信息。
-
- 特别注意,include通常是递归的,即一个头文件可能还包括另一个头文件,例如:
-
A include B B include C C include D
- 为了将内容完整地拷贝到 A中,需要首先完成 B的头文件查找,查找顺序是:
- A所在的位置
- 其它路径
- 然后完成 C文件的查找,查找顺序是:
- B所在的位置
- A所在的位置
- 其它路径
- 然后完成 D文件的查找,查找顺序是:
- C所在的位置
- B所在的位置
- A所在的位置
- 其它路径
-
2、gcc与g++的区别
- 在处理c/c++文件时,我们可以分别采用gcc或g++命令,两者效果类似,可以理解为g++命令在gcc命令上又针对c++进行了一层封装。因此它们的区别主要是对c/c++文件的处理细节,例如:
- g++对于.c/.cpp结尾的文件全都默认当作c++文件处理。
- gcc对于.c视作c文件,对于.cpp 视作c++文件处理。
- 对于STL标准库,如果使用 g++会自动链接进来,如果使用gcc则需要加参数-lstdc++显式地完成链接,并且可能有细节差异。
- 对于预定义宏,两者支持的宏不完全一样。
- 下述案例展示了gcc和g++分别对c++文件处理时的差异。
- 创建c++文件:
-
mkdir project02 # 创建存放c++文件的文件夹 cd project02 # 进入文件夹 vim hello.cpp # 创建并编写c++文件
-
- 编写hello.cpp:
- 按 i,进入插入模式,编辑文件。
- 编辑完毕后按回退键,输入 :wq,保存文件。
- 使用gcc编译
-
gcc hello.cpp # 会报错iostream cout各种未定义gcc hello.cpp -lstdc++ -o hello # 链接STL后可以顺利进行./hello # 执行目标文件
-
- 使用g++编译
-
g++ hello.cpp -o hello # 编译正常./hello # 执行目标文件
-
- 创建c++文件:
二、库
1、简介
- 库的存在目的:
- 为了把一些基础功能封装,在链接阶段或者运行阶段直接使用,方便功能复用。
- 为了隐藏代码,使用二进制的库文件不需要直接暴露源代码,方便发布产品的同时隐藏功能实现的细节。
- 根据库是否直接包含到可执行文件中,在运行期间是否需要,可以分为静态库和动态库。
- 根据平台和编译器的不同,c/c++的静态库和动态库处理在windows+msvc,MinGW+gcc和Linux+gcc三种情景下各不相同,windows对动态库非常不友好。本文将基于Linux+gcc,实现基本的静态库和动态库的编写。
- 【注】库文件的习惯命名。
- 在 Linux 中,静态库通常名称为 libxxx.a,动态库通常名称为 libxxx.so.x.y.z。
- 在 windows 中,静态库通常名称为 xxx.lib,动态库通常名称为 xxx.dll(DLL辅助部分 xxx.lib)。
- 在mingw中,静态库通常名称为 libxxx.a,动态库通常名称为libxxx.dll(DLL辅助部分libxxx.dll.a)。
2、实验
- 首先创建好目录结构。
-
|-bin |-includefoo.h |-lib|-all|-shared|-static |-srcfoo1.cppfoo2.cppmain.cpp
-
- 创建一个名为foo的库,头文件为foo.h。
- 【注】命令:cd.. 返回上一级目录。
- 创建两个源文件,分别实现静态库和动态库。
- foo1.cpp
- foo2.cpp
- 这里foo1.cpp和foo2.cpp都分别对头文件foo.h中的两个函数getnum()、sayhello()进行了实现。
- foo1.cpp
- 创建main.cpp引用头文件foo.h,用多种方式调用foo库。
- 实验将在 lib/static中存在静态库,在 lib/shared中存放动态库,在 lib/all存放动态库和静态库。
2.1、静态库的编写
- 在 Linux中使用 gcc编写一个静态库,需要编译和打包两步。
-
# 编译获得目标文件 g++ -c src/foo1.cpp -I include -o lib/static/foo1.o# 打包得到.a静态库,注意前面是输出文件,后面是输入文件(可以有多个输入) ar -crv lib/static/libfoo.a lib/static/foo1.o# 从lib/static复制一份到lib/all cp lib/static/libfoo.a lib/all/libfoo.a
-
2.2、动态库的编写
- 在 Linux中使用 gcc编写一个动态库,也需要两步。
-
# 注意在生成目标文件的时候需要加-fPIC选项,生成位置无关的目标文件 g++ -I include -fPIC -c src/foo2.cpp -o lib/shared/foo2.o # 注意得到动态库时,需要加-shared选项 g++ -shared lib/shared/foo2.o -o lib/shared/libfoo.so# 两步可以合并为一步,效果一样 g++ -fPIC -shared src/foo2.cpp -I include -o lib/shared/libfoo.so# 从lib/shared复制一份到lib/all cp lib/shared/libfoo.so lib/all/libfoo.so
-
- 【注】从目标文件到动态库文件,必须使用 -shared选项。
- 从源文件到目标文件,建议使用 -fPIC选项,可以生成位置无关的代码,此时动态库在内存中只需要加载一次,多个程序可以共同并且同时使用;否则只能相当于代码拷贝的方式,多个程序需要多次加载同一个动态库到内存中使用。
- 相比于静态库,动态库生成之后的存放位置是很重要的,因为可执行程序在运行时,还需要找到动态库并一起加载到内存中。
- 由于动态库通常有 -fPIC,而静态库通常没有,因此动态库中如果想要链接一个静态库会报错,解决办法可以是把静态库也添加选项,改为生成位置无关的代码。
2.3、库的使用
- 【注】库的名称需要满足一定的命名规范,对于静态库通常命名为 libxxx.a,对于动态库的名称通常为 libxxx.so.x.y.z ,还需要附带动态库的版本号。
- 在遵循这种命名规范的前提下,gcc 可以使用 -lxxx选项链接相应的库,如果命名不规范则需要给出库文件的完整名称。
- gcc 相应的选项
- -l:链接指定的库,例如 -lxxx为链接名为 xxx的库,可能是静态也可能是动态链接。
- -L:指出在编译时,库文件的优先查找目录,例如 -Llib会在 ./lib目录下查找。缺省目录时 -L代表当前目录,-L只能后接一个目录,多个目录使用多个-L选项。
- -Wl, rpath=your_dir:指出在运行时,动态库文件的优先查找目录,可以接多个目录,用
:
分隔不同目录,类似于环境变量中的路径分隔。(在 gcc 中,-Wl之类的选项会传递给链接程序,-Wa之类的选项会传递给汇编程序)
- 【注】选项 -l不能过早出现,因为 gcc从左到右检索,记住在源文件中没找到的符号,在对应的库当中查找并提取,对于没用到的部分,静态库可能丢弃剔除。因此库的链接选项最好在用到的源代码之后,在库的查找路径之后。
- 在这里要区分两个概念:编译时和运行时。
- 编译时,gcc 需要找到库文件,然后完成二进制文件的生成,这对于静态链接和动态链接都是必须的。
- 运行时,可执行文件需要找到它需要的动态库文件,然后一起加载执行。由于静态链接的库文件在运行时是不需要的,因此我们只有使用动态链接时,才有在运行时找到动态库文件的需求。
- -L选项告诉 gcc编译器,编译时在哪找到库文件,但这个信息通常不会留在可执行文件上。如果使用 -Wl, rpath=选项,则这个信息会留存在可执行文件上,明白在运行时首先去哪找到动态库文件。这两个路径的区分是必要的,因为编译时的开发环境和运行时的生产环境,动态库存放的位置很可能是不一样的。
- 注意到 -lxxx选项并没有告诉 gcc用静态链接还是动态链接,使用哪种链接的选择逻辑如下:
- 如果 gcc只找到了静态库版本,则使用静态链接。
- 如果 gcc只找到了动态库版本,则使用动态链接。
- 如果 gcc在同一目录下同时找到了静态库版本和动态库版本,则默认使用动态链接。默认行为可以被下列选项所更改:
- -static这个选项强制让所有的链接都使用静态链接,这很可能会在链接中报错,因为 glibc,libstdc++等最基础的库,通常在系统中只有动态库版本,没有静态库版本,并且这基础库并不适合静态链接。
- 更精细的强制链接选项:-Wl, -Bstatic指示跟在后面的 -l选项都使用静态链接,-Wl, -Bdynamic指示跟在后面的 -l选项都使用动态链接。注意这种用法需要保证在最后生效的是 -Wl, -Bdynamic,这是为了最后动态链接 glibc等基础库而准备的。
- 【注】对于静态库,其实并不是必须要以 -lxxx格式调用它,也可以简单粗暴地直接把 .o或者 .a文件和源文件放在一起编译。
2.4、链接库
- 先将 main.cpp转换为目标文件。
-
g++ -c src/main.cpp -Iinclude -o src/main.o
-
- 设置 MY_LD_LIBRARY_PATH环境变量,它要要写入 rpath中,这是最优先的查找顺序。(不设置的话,执行动态链接文件会找不到库文件的位置)
-
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/用户名/c++/project02/libtest/lib/all
-
- 在 lib/static只找到静态库时,使用静态链接:
-
g++ -Wl,-rpath=$MY_LD_LIBRARY_PATH src/main.o -Llib/static -lfoo -o test_static_1g++ -Wl,-rpath=$MY_LD_LIBRARY_PATH src/main.o lib/static/libfoo.a -o test_static_2
-
- 在 lib/shared只找到动态库时,使用动态链接:
-
g++ -Wl,-rpath=$MY_LD_LIBRARY_PATH src/main.o -Llib/shared -lfoo -o test_shared_1
-
- 在 lib/all同时找到两种库的时候,默认动态链接,可以使用选项改成静态链接:
-
g++ -Wl,-rpath=$MY_LD_LIBRARY_PATH src/main.o -Llib/all -lfoo -o test_shared_2g++ -Wl,-rpath=$MY_LD_LIBRARY_PATH src/main.o -Llib/all -Wl,-Bstatic -lfoo -Wl,-Bdynamic -o test_static_3
-
- 分别执行静态链接文件和动态链接文件。
- 【注】对于一个可执行文件,可以使用 ldd命令查看运行它所需要的动态库,以及它们现在是否能被系统找到,如果显示有的动态库没有找到,直接运行就会报错,这个选项也可以查看找到的是否是正确的动态库。