C/C++ 开发环境(终端)

    Linux 下,一般使用 gcc 编译 C 语言代码。gcc 可以通过包管理工具进行安装,以 Ubuntu 为例:

    接下来,我们编译一个非常简单的 C 语言程序 。代码如下:

    hello_world.c

    1. int main() {
    2. printf("Hello, world!\n");
    3. return 0;
    4. }

    你可以使用任何编辑工具来编写代码,nanovi ,甚至记事本均可。

    代码编辑完毕后,运行 gcc 命令进行编译:

    1. $ ls
    2. hello_world.c
    3. $ gcc -o hello_world hello_world.c

    其中, -o 选项指定可执行程序名, hello_world.c 是源码文件。不出意外,当前目录下将出现一个可执行文件:

    1. $ ls
    2. hello_world hello_world.c

    最后,还是在命令行下,将程序运行起来。看,程序输出预期内容:

    1. $ ./hello_world
    2. Hello, world!

    C++ 语言

    C++ 语言编译与 C 语言类似,只不过编译工具不再是 gcc ,而是 g++ 。同样地, g++ 也可以通过包管理工具来安装:

    1. $ sudo apt install g++

    还是编译一个简单的程序 ,代码如下:

    hello_world.cpp

    1. #include <iostream>
    2. using std::cout;
    3. using std::endl;
    4. int main() {
    5. cout << "Hello, world!" << endl;
    6. return 0;
    7. }

    运行 g++ 命令进行编译,用法与 gcc 一样:

    1. $ ls
    2. hello_world.c
    3. $ g++ -o hello_world hello_world.cpp

    编译完毕后,执行程序:

    1. $ ./hello_world
    2. Hello, world!

    大型程序一般由多个文件组成,编译多文件程序,将所有源码文件传给编译器即可。

    C 语言为例,将 拆解成两个文件进行演示。首先是 say.c

    multi-file/say.c

    1. #include <stdio.h>
    2. void say_hello() {
    3. }

    say.c 定义了一个函数,名为 say_hello 用于输出 Hello, world!

    在 中,直接调用 say_hello 即可:

    multi-file/hello_world.c

    1. void say_hello();
    2. int main() {
    3. return 0;
    4. }

    编译这个由两个文件组成的程序,步骤也非常简单:

    请注意,需要在 hello_world.c 中申明函数 say_hello 的原型(第 1 行),否则编译将输出以下警告:

    1. hello_world.c:17:5: warning: implicit declaration of function 'say_hello' is invalid in C99 [-Wimplicit-function-declaration]
    2. say_hello();
    3. ^
    4. 1 warning generated.

    原型申明相当于告诉编译器,say_hello 函数在别处定义,调用时不需要参数,也没有返回值。编译器编译 hello_world.c 时需要这个信息,因为 say_hello 函数的定义不在该文件下。

    编译流程

    如下图,程序编译可以进一步分成 编译 ( Compile )和 链接 ( Link )两个阶段:

    接下来,我们分阶段编译 ,源文件如下:

    1. $ cd multi-file2
    2. $ ls
    3. hello_world.c say.c say.h

    编译say.c 文件,生成 say.o 对象文件:

    1. $ gcc -c say.c
    2. $ ls
    3. hello_world.c say.c say.h say.o

    编译 文件,生成 hello_world.o 对象文件:

    1. $ gcc -c hello_world.c
    2. $ ls
    3. hello_world.c hello_world.o say.c say.h say.o

    最后 链接 俩对象文件,生成可执行程序:

    1. $ gcc -o hello_world hello_world.o say.o
    2. $ $ ./hello_world
    3. Hello, world!

    注解

    为啥要分阶段编译?

    分阶段编译最大的好处是,可以进行部分编译—— 只编译有变更部分

    假设例子中, hello_world.c 有变更,而 say.c 没有变更。那么,我们只需要编译 hello_world.c 生成新的 hello_world.o 对象文件,最后再跟先前的 say.o 文件链接生成新的可执行文件即可。

    换句话将,我们省去了编译 say.c 的麻烦!这个小程序可能不明显,在大型程序中节省的编译时间非常可观。

    我们尝到部分编译的好处,但是区分哪些源码文件有变更是个麻烦事儿。而且,手工敲这么多编译命令也很无聊,我们急需一种能够自动构建的方法。

    这时,我们可以借助自动化构建工具 make) :

    1. $ sudo apt install make

    构建规则定义在 里:

    Makefile

    1. .DEFAULT_GOAL := run
    2. say.o: say.c
    3. gcc -o say.o -c say.c
    4. hello_world.o: hello_world.c
    5. gcc -o hello_world.o -c hello_world.c
    6. hello_world: say.o hello_world.o
    7. gcc -o hello_world say.o hello_world.o
    8. run: hello_world
    9. ./hello_world
    10. clean:
    11. rm -f *.o

    Makefile 规则可以很复杂,这里只介绍最基本的。

    Makefile 大致可以理解成 目标依赖 以及 构建指令

    以第 3-4 行为例, say.o 是构建 目标say.c依赖 ,其后以制表符( \t )缩进的行是 构建指令 。换句话将,要构建 say.o 需要依赖 say.c 源码文件,构建方法是执行 gcc 编译命令。

    依赖定义清楚之后, make 可以决定什么情况下需要重新构建,什么情况下则不用。如果 say.c 有变更( mtimesay.o 大),则需要重新运行构建指令生成新的 say.o 。反之则不用。

    此外,目标可以被其他目标所依赖,就像搭积木一样!执行程序( run ),依赖二进制程序( hello_world ),二进制程序则依赖两个目标文件( say.o, hello_world.o ),而目标文件又分别依赖各自的源码文件。通过递归, make 自动找到了构建并运行程序的方法!

    Makefile 定义好后,运行 make 命令加上目标名即可构建指定的目标。例如,编译 say.o

    1. $ make say.o

    或者编译并生产可执行程序( hello_world ):

    1. $ make hello_world

    make 命令将确保俩子目标( say.o 以及 hello_world.o )先被正确构建。

    最后,一起感受一下自动构建有多爽。进入 目录:

    1. $ ls
    2. hello_world.c Makefile say.c say.h

    执行 make 命令:

    一个 make 命令搞定从编译到执行的所有环节!

    注解

    我们在 Makefile 第一行显式定义了默认目标。

    由于没有变更,再次构建时自动省略编译环节:

    1. $ make
    2. ./hello_world
    3. Hello, world!

    顺便提一下,我们的 Makefile 还定义了一个用于清理编译结果的目标—— clean

    1. $ ls
    2. Makefile hello_world hello_world.c hello_world.o say.c say.h say.o
    3. $ make clean
    4. rm -f *.o
    5. rm -f hello_world
    6. $ ls
    7. Makefile hello_world.c say.c say.h

    清理编译结果在打包源码、进行全新编译等场景特别有用。

    程序库

    可复用的代码,一般编译成程序库来使用。程序库可以分成两种:

    • 静态链接库
    • 动态链接库

    静态库的全称是静态链接程序库,在程序编译阶段被链接进可执行程序。

    接下来,我们将 say.c 编译成一个静态库,以此演示制作并使用静态库的方法。

    进入 目录:

    1. $ cd static-library
    2. $ ls
    3. hello_world.c say.c say.h

    编译 say.c

    1. $ gcc -c say.c
    2. $ ls
    3. hello_world.c say.c say.h say.o

    将目标文件打包成静态库,库名为 libsay.a

    1. $ ar -crv libsay.a say.o
    2. a - say.o
    3. $ ls
    4. hello_world.c libsay.a say.c say.h say.o

    编译 时指定链接静态库:

    1. $ gcc -o hello_world hello_world.c libsay.a
    2. $ ./hello_world
    3. Hello, world!

    动态库

    对应地,动态库的全称是动态链接程序库。

    与静态库不同,动态库不链接进可执行程序。相反,程序只记录需要的动态库,直到运行时才搜索并加载。因此,采用动态库的程序,编译后生成的可执行文件更为短小。

    接下来,我们将 say.c 编译成一个动态库,以此演示制作并使用动态库的方法。

    进入 目录:

    1. $ cd dynamic-library
    2. $ ls
    3. hello_world.c say.c say.h

    编译 say.c

    1. $ gcc -fPIC -c say.c
    2. $ ls
    3. hello_world.c say.c say.h say.o

    注意到,通过 -fPIC 告诉 gcc 生成位置无关代码( Position-Independent Code )。这是制作动态库所必须的。

    使用 gcc 生成动态库( -shared ),库名为 libsay.so

    1. $ gcc -shared -o libsay.so say.o
    2. $ ls
    3. hello_world.c libsay.so say.c say.h say.o

    在编译 时,链接到生成的动态库:

    1. $ gcc -o hello_world hello_world.c -L. -lsay

    其中,选项 -L 指定动态库搜索路径; -l 指定需要链接的动态库名,可以多次指定。

    好了,启动程序。然而,程序异常退出了:

    原因是程序启动后找不到 libsay.so 动态库文件。

    系统默认在 /lib/usr/lib 等路径下搜索动态库。因此,可以将生成的动态库放置到上述目录再运行程序。

    另一种方法是,通过 LD_LIBRARY_PATH 环境变量指定动态库搜索路径:

    1. Hello, world!

    订阅更新,获取更多学习资料,请关注我们的 微信公众号

    小菜学编程