要学好 C 语言 / C++ ,Makefile 可少不了
一、Makefile 简介
1. Makefile 是什么?
Makefile 通常指的是一个含有一系列命令(directive)的,通过 Make 自动化编译工具,帮助 C/C++ 程序实现自动编译目标文件的文件。这个文件的默认命名是 "Makefile"。
2. 为什么要使用 Makefile?
Makefile 文件描述了整个工程的编译、链接的规则。
为工程编写 Makefile 的好处是能够使用一行命令来完成“自动化编译”。只需提供一个(通常对于一个工程来说会是多个)正确的 Makefile,接下来每次的编译都只需要在终端输入“make”命令,整个工程便会完全自动编译,极大提高了效率。尤其是在编译一个仅有一小部分文件被改动过的大项目的情况下。
绝大多数的 IDE 开发环境都会为用户自动编写 Makefile。
3. Make 是怎么工作的?
Make 工作的原则就是:
一个目标文件当且仅当在其依赖文件(dependencies)的更改时间戳比该目标文件的创建时间戳新时,这个目标文件才需要被重新编译。
Make 工具会遍历所有的依赖文件,并且把它们对应的目标文件进行更新。编译的命令和这些目标文件及它们对应的依赖文件的关系则全部储存在 Makefile 中。
Makefile 中也指定了应该如何创建,创建出怎么样的目标文件和可执行文件等信息。
除此之外,你甚至还可以在 Makefile 中储存一些你想调用的系统终端的命令,像一个 Shell 脚本一样使用它。
二、简单了解编译连接与执行
1. 实验介绍
按照 GNU make 官方手册中采用的教学模式,在正式的学习 Makefile 知识之前,本次实验先介绍一些简单的前导知识。实验详细介绍了 GNU GCC 编译和链接的基本方法,通过编译、链接、静态链接、动态链接的实验内容让用户学习和理解 GCC 的基本使用方法。同时,用户也将在实验过程中体会到手动编译链接的低效,从而体会到自动编译的在项目工程管理中的重要性。
知识点
- GCC 编译的使用方式
- GCC 链接的使用方式
- GCC 静态链接的使用方式
- GCC 动态链接的使用方式
- GCC 静态链接 + 动态链接混用的方式
代码获取
通过在 Terminal 中输入以下命令可以将本课程所涉及到的所有源代码下载到在线环境中,作为参照对比进行学习。
wget http://labfile.oss.aliyuncs.com/courses/849/make_example-master.zip && unzip make_example-master.zip && rm make_example-master.zip
命令执行后 WebIDE 的工作区中将会出现一个名为 make_example-master 的文件夹,文件夹中包含了课程所涉及到的源代码,目录结构如图所示:
2. 实验步骤
本章节的源代码位于 /home/project/make_example-master/chapter0
目录中,请在 Terminal 中通过 cd 命令切换至该目录后再进行实验学习。
项目涉及到的代码文件:
(1)main.c
: 主要文件
(2)add_minus.c add_minus.h
: 加减法 API 及实现
(3)multi_div.c multi_div.h
: 乘除法 API 及实现
项目涉及到的 gcc 参数:
参数 | 描述 |
---|---|
-c | 编译、汇编指定的源文件(也就是编译源文件),但是不进行链接 |
-o | 用来指定输出文件 |
-L | 为 gcc 增加一个搜索链接库的目录 |
-l | 用来指定程序要链接的库 |
这一章节我们将正式开始进行简易四则预算程序的编译实验,分步骤进行。
主程序的编译、链接与执行
打开 chapter0 文件夹查看 main.c
文件,内容如下:
#include <stdio.h>
int main(void)
{
printf("Hello Cacu!\n");
return 0;
}
点击 chapter0 文件夹并右键选择 Open in Terminal 在终端中打开 main.c
所在的文件夹。
在 Terminal 中执行以下命令,对 main.c
文件只编译而不链接。
gcc -c main.c
可以发现在当前目录中生成了一个新的文件 main.o
。
通过 file
命令查看 main.o
的文件格式:
file main.o
输出结果如图所示:
这说明 main.o
实际上是一个 relocatable object 文件。
通过以下命令为 main.o
文件赋予可执行的权限:
chmod 777 main.o
chmod
命令用于改变文件的读写以及运行许可设置,详细解绍参考 Permissions
输入以下命令尝试执行 main.o
文件:
./main.o
Terminal 输出可执行文件格式错误,如图所示:
说明 relocatable object 文件是不可执行的。
接下来通过 GCC 对 main.o
文件进行链接操作,从而生成一个可执行的程序 main
。
在 Terminal 中输入以下命令将 main.o
链接为 main
文件:
gcc -o main main.o
可以发现当前目录新增了一个名为 main
的文件。
通过 file
命令查看 main
的文件格式:
file main
输出结果如图所示:
说明 main
文件是一个可执行的文件,于是通过以下命令来执行 main
文件:
./main
输出结果如图所示:
说明程序得到了正确的执行。
静态链接
编写 add_minus.h
文件,在文件中对函数 add()
和 minus()
进行声明,不过在 chapter0 文件夹中已经提供编写好的 add_minus.h
文件,我们可以拿来直接使用。
文件内容如下:
#ifndef __ADD_MINUS_H__
#define __ADD_MINUS_H__
int add(int a, int b);
int minus(int a, int b);
#endif /*__ADD_MINUS_H__*
编写 add_minus.c
文件,实现函数 add()
和 minus()
,同样的在 chapter0 文件夹中已经有编写好的 add_minus.c
文件,我们可以拿来直接使用。
文件内容如下:
#include "add_minus.h"
int add(int a, int b)
{
return a+b;
}
int minus(int a, int b)
{
return a-b;
对 add_minus.c
文件进行编译,生成 add_minus.o
文件。
gcc -c add_minus.c
修改 main.c
文件,为其增加加减法运算并编译这个文件。
执行以下命令给 main.c
打上 v1.0.patch
补丁:
patch -p2 < v1.0.patch
patch
命令可以处理 diff 程序生成的补丁文件,补丁格式可以是四种比较格式中任意一种, 然后把这些差异融入到原始文件中,生成一个打过补丁的版本。-p
选项表示剥离层级,通过在 Terminal 中输入man patch
命令可获取详细说明。
此时 main.c
文件内容如下:
#include <stdio.h>
#include "add_minus.h"
int main(void)
{
int rst;
printf("Hello Cacu!\n");
rst = add(3,2);
printf("3 + 2 = %d\n",rst);
rst = minus(3,2);
printf("3 - 2 = %d\n",rst);
return
通过以下命令对 main.c
文件进行编译和链接:
gcc -c main.c
gcc -o main main.o
链接生成的 main.o
文件时,发现有错误出现,错误内容如图所示:
原因在于链接过程中找不到 add
和 minus
这两个 symbol。
现将 main.o
和 add_minus.o
链接成可执行文件并执行测试。
gcc -o main main.o add_minus.o
执行新生成的可执行文件 main
。
./main
输出结果如下:
说明程序得到了正常执行。
重新编译 add_minus.c
生成 add_minus.o
文件。
gcc -c add_minus.c
通过 ar
命令将 add_minus.o
打包到静态库中。
ar rc libadd_minus.a add_minus.o
可以发现在当前目录下,生成了一个名为 libadd_minus.a
的静态库文件。
用 file
命令查看 libadd_minus.a
的文件格式。
file libadd_minus.a
Terminal 输出结果如图所示:
实际上 libxxx.a
格式的文件可以简单的看成指定的以 .o
结尾的文件集合。
链接 main.o
和静态库文件。
gcc -o main2 main.o -L./ -ladd_minus
-L./
:表明库文件位置在当前文件夹。
-ladd_minus
:表示链接libadd_minus.a
文件,使用-l
参数时,前缀lib
和后缀.a
是需要省略的。
执行 main2
:
./main2
Terminal 输出结果如图所示:
说明程序的到了正确的执行。
动态链接
编写 multi_div.h
文件,并在其中对函数 multi()
和 div()
进行声明。由于提供的源代码中已经包含了编写好的 multi_div.h
文件,因此我们可以拿来直接使用。
multi_div.h
文件的内容如下:
#ifndef __MULTI_DIV_H__
#define __MULTI_DIV_H__
int multi(int a, int b);
int div(int a, int b);
#endif /*__MULTI_DIV_H__*
编写 multi_div.c
文件,实现函数 multi()
和 div()
,同样的由于提供的源代码中已包含了编写好的 multi_div.c
文件,我们可以直接拿来使用。
multi_div.c
文件内容如下:
#include "multi_div.h"
int multi(int a, int b)
{
return a*b;
}
int div(int a, int b)
{
return a/b;
通过以下命令将 multi_div.c
文件编译成动态链接库。
gcc multi_div.c -fPIC -shared -o libmulti_div.so
-fPIC
选项作用于编译阶段,在生成目标文件时就得使用该选项,以生成位置无关的代码。
命令执行结束后,在当前目录下会生成一个名为 libmulti_div.so
的文件。
通过 file
命令来查看 libmulti_div.so
的文件格式。
file libmulti_div.so
Terminal 输出结果如图所示:
由此可知 libmulti_div.so
是一个 shared object 文件。
删除之前的 main.c
文件,并编写新的 main.c
文件,内容如下:
#include <stdio.h>
int main(void)
{
printf("Hello Cacu!\n");
return 0;
}
通过以下命令为 main.c
打上 v2.0.patch
补丁:
patch -p2 < v2.0.patch
此时 main.c
文件的内容如下:
#include <stdio.h>
/*
#include "add_minus.h"
*/
#include "multi_div.h"
int main(void)
{
int rst;
printf("Hello Cacu!\n");
/*
rst = add(3,2);
printf("3 + 2 = %d\n",rst);
rst = minus(3,2);
printf("3 - 2 = %d\n",rst);
*/
rst = multi(3,2);
printf("3 * 2 = %d\n",rst);
rst = div(6,2);
printf("6 / 2 = %d\n",rst);
return
编译 main.c
生成 main.o
:
gcc -c main.c
链接 main.o
与动态链接库文件。
gcc -o main3 main.o -L./ -lmulti_div
执行生成的 main3
文件。
./main3
输出结果出现错误,如图所示:
出现错误的原因是我们生成的动态库 libmulti_div.so
并不在库文件搜索路径中。
解决办法:
- 将
libmulti_div.so
拷贝到/lib/
或/usr/lib/
文件夹下。
sudo cp libmulti_div.so /usr/lib
- 在
LD_LIBRARY_PATH
变量中指定库文件路径,而动态链接库文件存放在/home/project/make_example-master/chapter0/
路径下。
所以需要在 Terminal 中执行下面的命令:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/project/make_example-master/chapter0/
现在在 Terminal 中执行下面的命令:
./main3
输出结果如图所示:
说明程序得到了正确的执行。
混合使用静态链接与动态链接
删除旧的 main.c
文件,并编写新的 main.c
文件,内容如下:
#include <stdio.h>
int main(void)
{
printf("Hello Cacu!\n");
return 0;
}
为新的 main.c
文件打上 v3.0.patch
补丁。
patch -p2 < v3.0.patch
编译 main.c
生成 main.o
。
gcc -c main.c
测试执行混用静态链接和动态链接的方式。
gcc -o main4 main.o -L./ -ladd_minus -lmulti_div
由于我们之前已经修改过 LD_LIBRARY_PATH
变量,所以此次无需再次修改。
执行下面的命令:
./main4
输出结果如图所示:
说明程序得到正确的执行。
尽管我们知道无论是静态链接还是动态链接都能达到链接对象文件生成可执行文件的目的,但是我们还是得 z 注意静态链接库与动态链接库之间的区别,详细内容参考 Static, Shared Dynamic and Loadable Linux Libraries
三、总结
上述内容来自课程《Makefile 基础入门实战》,主要介绍了 GCC 编译,链接的方法和静态链接库与动态链接库的创建和使用方法。
后续课程内容将学习以下内容:
点击《Makefile 基础入门实战》,即可可学习完整课程!