C++ 的编译、运行和链接

隐身守侯 提交于 2020-02-08 14:37:30

涉及的内容

此系列会在开头说明本篇博客涉及的内容,以方式各位使用,如下:

  1. 简要介绍编译型语言和解释型语言
  2. C/C++ 的编译器 gcc/g++ 的用法
  3. 使用 gcc/g++ 制作静态库和动态库
  4. 使用 gdb 进行调试

C++ 作为编译型语言

计算机上可以运行的只有机器指令(由 01 组成),其他任何语言编写的程序(包括汇编)都要翻译成对应的机器指令才能运行,C++ 属于编译型语言。

编译语言有很多,常见的高级语言都是编译语言,如 Java、C\C++、C# 等,特点是运行前要经过一系列的处理,通常将这一过程称为 “编译”,编译成功后会生成对应的二进制文件,也就是可执行文件(您的程序)。

与之对应的解释型语言有: Python、JavaScript、HTML 等,特点是运行时在进行 “解释” ,即每执行到一条语言就进行解释成对应的机器指令。

大家知道的编译语言速度快,快的地方其实在于“一次编译,多次运行”,而解释型语言则是每次运行时都要进行 “解释” 所以才慢了些。 但是实际开发过程解释型语言较编译型语言快得多喔, 原因也是因为编译型语言运行前要进行编译,当工程较大时,编译可以耗费大量时间,不适合调试,另外学习编译语言的成本和难度较解释型要大。

编译语言的运行,通常可以分成几个步骤,如下:

  1. 预处理(负责展开宏,头文件、条件编译)
  2. 编译(检查语法规范、将源文件翻译成汇编指令)
  3. 汇编(将汇编指令翻译成机器指令)
  4. 链接(数据段合并,地址回填)

C/C++ 的编译器

前面说到编译语言的运行可以分成 4 个步骤, 而对应的 4 个步骤都是需要我们手动去做的(本篇博客不涉及任何 IDE, IDE 固然好,但是隐藏了很多需要我们了解和知道的知识)。为了完成上面的任务,我们使用 GNU gcc/g++ 这一编译器(Linux环境中), gcc 和 g++ 都是编译器, 建议编译纯 C 程序时,使用 gcc , 含有 c++ 代码的程序使用 g++ 。下面都是直接使用 g++ 并假设 source.cpp 为我的源文件。

source.cpp 的内容。

#include <iostream>

int main(int argc, char *argv[])
{
    std::cout << "Hello world" << std::endl;
    return 0;
}

g++ 的用法

g++ [option] source.cpp

option 是可选项,完成指定的功能。

  1. 预处理
g++ -E source.cpp

运行上面的命令后,终端会输出大量的信息, 主要就是将头文件 iostream 进行展开,加载外部符号。 这里的 option 为 -E 就是预处理的意思, 我们也可以继续添加其他选择, 如 -o filename , -o 表示输出的文件名, 后面紧接着一个 文件名 (在没有使用 -o 选项时, 有些版本或许会直接生成对应的 .i 文件)。

g++ -o source.i -E source.cpp //生成一个 source.i 文件, 内容为 g++ -E source.cpp 所输出的内容
  1. 编译(检查语法规范、将源文件翻译成汇编指令)
g++ -S source.cpp // 自动生成 source.s 文件,打开看是汇编指令

另外还有汇编是 -c 生成对应机器指令(对应为 .o 文件),这里不再赘述, Linux 下使用 g++ --help 可以查看使用方法。
还有一点需要说明的是 g++ -S souce.cpp 时并没有跳过预处理的过程,也就是说 g++ -S source.cpp 等价于下面的指令:

g++ -o source.i -E source.cpp
g++ -S source.i

其他命令的行为也类似。通常使用如下方式直接生成目标程序:

g++ source.cpp // 生成一个 a.out 的可执行程序
g++ -o run source.cpp // 指定生成一个名为 run 的可执行程序

使用头文件
现在我们添加一个 hello.cpp 和 hello.h 文件,内容如下:

//hello.cpp
#include <iostream>

void hello()
{
    std::cout << "hello" << std::endl;
}

hello.h
#pragma once
void hello();

然后在source.cpp 中加入头文件 hello.h 和 调用 hello(), 再进行编译,发现如下报错:

/tmp/ccSzU52G.o: In function `main':
source.cpp:(.text+0x10): undefined reference to `hello()'
collect2: error: ld returned 1 exit status

这是正常的现象,解决此问题的方法就是需要将 source.cpp 和 hello.cpp 一起编译

g++ source.cpp hello.cpp //正常生成对于 a.out 文件

制作和调用库

很多时候我们需要使用第三方库, 在 Linux 下可以到 /usr/lib 可以看到许多 libxxx.so 的文件,这些就是动态库, 而静态库的命名格式是 libxxx.a, 请注意 xxx 才是正常的库名,链接时会忽略掉 lib 前缀和 .so 后缀。
例如当使用 pthread.h 这个多线程库时, 需要在 g++ 加上 -pthread 链接加载动态库, 或者使用 -lpthread , -l 表示这是一个动态库。我们只是表示要链接一个动态库, 但是并没有告诉 g++ 去哪里找这个动态库(-lasdasd 随便指定一个动态库试试),这是因为 g++ 会去默认的几个地方去寻找对应的库 。

echo $LD_LIBRARY_PATH //输出动态库搜索路径

// 若要添加动态库搜索路径, 可以使用如下方法:
修改 ~/.bashrc (实质为一个 shell 脚本)
添加一条 export LD_LIBRARY_PATH="you_path_to_lib/:$LD_LIBRARY_PATH"  , 如
export LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu/:$LD_LIBRARY_PATH"
修改后, 可以 source ~/.bashrc 表示执行 .bashrc 文件

当然也可以使用 -L (Library的意思) 指定一个库搜索路径

同理,还有其他搜索路径

-I (Include 的意思) 指定头文件搜索路径, 也可以像上面修改如下环境变量:
C_INCLUDE_PATH,CPLUS_INCLUDE_PATH,OBJC_INCLUDE_PATH

cpp -v 可以查看默认搜索路径,例如:

cpp -v  // 可输出如下信息
#include <...> search starts here:
 /usr/lib/gcc/x86_64-linux-gnu/7/include
 /usr/local/include
 /usr/lib/gcc/x86_64-linux-gnu/7/include-fixed
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.
  1. 制作静态库
    假设你用 C++ 写了一个有用小工具,然后想要贡献出去给别人使用,但是不想公布源码,这就要制作库了。首先介绍静态库的制作和使用,以将上面的 hello.cpp 制作成库为例。
g++ -c hello.cpp  // 生成 .o 文件
ar rcs  libhello.a hello.o //生成静态库
g++ source.cpp libhello.a // 将静态库和 source.cpp 链接起来并生成 a.out 文件

有了 libxxx.a 这静态库,就可以将源文件隐藏起来了,但是头文件还是不可少的。

  1. 制作动态库
g++ -shared -fPIC -o libhello.so hello.cpp //生成动态库
g++ source.cpp -L . -lhello //指定动态库搜索路径为当前目录和动态库名

动态库的制作还没有结束,现在做这些事情,将制作好的动态库放到合适的地方:

mkdir lib
mv libhello.so /lib
./a.out   //第一次报错

// 尝试重新链接
g++ source.cpp -L ./lib -lhello // 成功编译
./a.out  // 第二次报错

??? 第二次不是指定了库路径了吗??? 还是报错??? 这是因为编译和运行是分开的。 编译时指定一次动态库的路径, 运行时再到默认的动态库去查找编译阶段的动态库, 也就是上面提到的 LD_LIBRARY_PATH,试下下面的测试。

LD_LIBRARY_PATH="$LD_LIBRARY_PATH:path_to_your_lib" // 将 ./lib加入搜索路径,注意要绝对路径
./a.out  //成功运行

测试完成后,也可以在 .bashrc 中修改 LD_LIBRARY_PATH, 然后将自己的库放到对应的位置。

这样似乎很麻烦? 想想我们把 libhello.so 放在与 a.out 同一个目录下时,也没有指定搜索目录不也是成功运行了吗? 这里介绍另一个参数:

g++ source.cpp -lhello -L ./lib -Wl,-rpath=./lib //编译时指定运行时的搜索路径

利用 GDB 进行调式

目前利用 g++ 摆脱了以前 IDE 的编译和运行, 那么调试呢??? 别担心,调试非常简单, 只需要在编译时添加一个选项 -g 便可。 来瞧瞧下面的命令及运行结果:

g++ source.cpp -lhello -L ./lib -Wl,-rpath=./lib
ll -h a.out 
// 输出
8.5K Feb  8 12:32 a.out*

g++ -g source.cpp -lhello -L ./lib -Wl,-rpath=./lib
ll -h a.out
// 输出
29K Feb  8 12:34 a.out*

多了个 -g ,瞬间大了20K呢,表示已经添加了调试信息,接下来就开始调试啦。

gdb a.out // 进入调试

然后就有一大堆的命令要学了, 目前来说, 我也不是非常习惯利用 gdb 调试, 但是利用 gdb 调试的信息会比 IDE 的多一些(也可能是我太久没用IDE的调试而记错了)。 下来给出一些基本命令和用法:

命令 作用
l 列出 10 行源码, 可以加个数字, l 10 表示列出 10-19 行
b num num表示行号, b num 表示在第 num 行设置断点
info b 列出所有断点信息
set args 设置 main 函数的参数
run 运行到断点
finsh 结束函数调用
until 运行到某行
continue 运行到下个断点
n 单步执行(不进入函数调用)
s 单步进入(进入函数调用)
p 查看某变量的值
display 显示某变量的值(每条语句执行后都会显示)
undisplay 取消显示
quit 退出

更多的用法自己了解吧, 目前看起来挺弱鸡, 但是强的地方正是 gdb 更多的用法那里。

总结

虽然本篇博客为 C++ 系列的第一篇, 但却是非常重要的一篇,里面有许多内容,也要求有一丢丢 Linux 基础和其他一些计算机基础,这里的内容可能要经常查阅直到熟悉。掌握了本篇内容,自己使用网络上的第三方库时,也不会摸不着头脑啦 : )

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!