make基础(转)
1. 基本规则 请点评
除了Hello World这种极简单的程序之外,一般的程序都是由多个源文件编译链接而成的,这些源文件的处理步骤通常用Makefile来管理。Makefile起什么作用呢?我们先看一个例子,这个例子由例 12.3 “用深度优先搜索解迷宫问题”改写而成:
/* main.c */
#include <stdio.h>
#include "main.h"
#include "stack.h"
#include "maze.h" struct point predecessor[MAX_ROW][MAX_COL] = {
{{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}},
{{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}},
{{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}},
{{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}},
{{-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}, {-1,-1}},
}; void visit(int row, int col, struct point pre)
{
struct point visit_point = { row, col };
maze[row][col] = 2;
predecessor[row][col] = pre;
push(visit_point);
} int main(void)
{
struct point p = { 0, 0 }; maze[p.row][p.col] = 2;
push(p); while (!is_empty()) {
p = pop();
if (p.row == MAX_ROW - 1 /* goal */
&& p.col == MAX_COL - 1)
break;
if (p.col+1 < MAX_COL /* right */
&& maze[p.row][p.col+1] == 0)
visit(p.row, p.col+1, p);
if (p.row+1 < MAX_ROW /* down */
&& maze[p.row+1][p.col] == 0)
visit(p.row+1, p.col, p);
if (p.col-1 >= 0 /* left */
&& maze[p.row][p.col-1] == 0)
visit(p.row, p.col-1, p);
if (p.row-1 >= 0 /* up */
&& maze[p.row-1][p.col] == 0)
visit(p.row-1, p.col, p);
print_maze();
}
if (p.row == MAX_ROW - 1 && p.col == MAX_COL - 1) {
printf("(%d, %d)\n", p.row, p.col);
while (predecessor[p.row][p.col].row != -1) {
p = predecessor[p.row][p.col];
printf("(%d, %d)\n", p.row, p.col);
}
} else
printf("No path!\n"); return 0;
}
我们把堆栈和迷宫的代码分别转移到模块stack.c
和maze.c
中,main.c
包含它们提供的头文件stack.h
和maze.h
。
/* main.h */
#ifndef MAIN_H
#define MAIN_H typedef struct point { int row, col; } item_t; #define MAX_ROW 5
#define MAX_COL 5 #endif
在main.h
中定义了一个类型和两个常量,main.c
、stack.c
和maze.c
都要用到这些定义,都要包含这个头文件。
/* stack.c */
#include "stack.h" static item_t stack[512];
static int top = 0; void push(item_t p)
{
stack[top++] = p;
} item_t pop(void)
{
return stack[--top];
} int is_empty(void)
{
return top == 0;
}
/* stack.h */
#ifndef STACK_H
#define STACK_H #include "main.h" /* provides definition for item_t */ extern void push(item_t);
extern item_t pop(void);
extern int is_empty(void); #endif
例 12.3 “用深度优先搜索解迷宫问题”中的堆栈规定死了只能放char
型数据,现在我们做进一步抽象,堆栈中放item_t
类型的数据,item_t
可以定义为任意类型,只要它能够通过函数的参数和返回值传递并且支持赋值操作就行。这也是一种避免硬编码的策略,stack.c
中多次使用item_t
类型,要改变它的定义只需改变main.h
中的一行代码。
/* maze.c */
#include <stdio.h>
#include "maze.h" int maze[MAX_ROW][MAX_COL] = {
0, 1, 0, 0, 0,
0, 1, 0, 1, 0,
0, 0, 0, 0, 0,
0, 1, 1, 1, 0,
0, 0, 0, 1, 0,
}; void print_maze(void)
{
int i, j;
for (i = 0; i < MAX_ROW; i++) {
for (j = 0; j < MAX_COL; j++)
printf("%d ", maze[i][j]);
putchar('\n');
}
printf("*********\n");
}
/* maze.h */
#ifndef MAZE_H
#define MAZE_H #include "main.h" /* provides defintion for MAX_ROW and MAX_COL */ extern int maze[MAX_ROW][MAX_COL];
void print_maze(void); #endif
maze.c
中定义了一个maze
数组和一个print_maze
函数,需要在头文件maze.h
中声明,以便提供给main.c
使用,注意print_maze
的声明可以不加extern
,而maze
的声明必须加extern
。
这些源文件可以这样编译:
$ gcc main.c stack.c maze.c -o main
但这不是个好办法,如果编译之后又对maze.c
做了修改,又要把所有源文件编译一遍,即使main.c
、stack.c
和那些头文件都没有修改也要跟着重新编译。一个大型的软件项目往往由上千个源文件组成,全部编译一遍需要几个小时,只改一个源文件就要求全部重新编译肯定是不合理的。
这样编译也许更好一些:
$ gcc -c main.c
$ gcc -c stack.c
$ gcc -c maze.c
$ gcc main.o stack.o maze.o -o main
如果编译之后又对maze.c
做了修改,要重新编译只需要做两步:
$ gcc -c maze.c
$ gcc main.o stack.o maze.o -o main
这样又有一个问题,每次编译敲的命令都不一样,很容易出错,比如我修改了三个源文件,可能有一个忘了重新编译,结果编译完了修改没生效,运行时出了Bug还满世界找原因呢。更复杂的问题是,假如我改了main.h
怎么办?所有包含main.h
的源文件都需要重新编译,我得挨个找哪些源文件包含了main.h
,有的还很不明显,例如stack.c
包含了stack.h
,而后者包含了main.h
。可见手动处理这些问题非常容易出错,那有没有自动的解决办法呢?有,就是写一个Makefile
文件和源代码放在同一个目录下:
main: main.o stack.o maze.o
gcc main.o stack.o maze.o -o main main.o: main.c main.h stack.h maze.h
gcc -c main.c stack.o: stack.c stack.h main.h
gcc -c stack.c maze.o: maze.c maze.h main.h
gcc -c maze.c
然后在这个目录下运行make
编译:
$ make
gcc -c main.c
gcc -c stack.c
gcc -c maze.c
gcc main.o stack.o maze.o -o main
make
命令会自动读取当前目录下的Makefile
文件[33],完成相应的编译步骤。Makefile由一组规则(Rule)组成,每条规则的格式是:
target ... : prerequisites ...
command1
command2
...
例如:
main: main.o stack.o maze.o
gcc main.o stack.o maze.o -o main
main
是这条规则的目标(Target),main.o
、stack.o
和maze.o
是这条规则的条件(Prerequisite)。目标和条件之间的关系是:欲更新目标,必须首先更新它的所有条件;所有条件中只要有一个条件被更新了,目标也必须随之被更新。所谓“更新”就是执行一遍规则中的命令列表,命令列表中的每条命令必须以一个Tab开头,注意不能是空格,Makefile的格式不像C语言的缩进那么随意,对于Makefile中的每个以Tab开头的命令,make
会创建一个Shell进程去执行它。
对于上面这个例子,make
执行如下步骤:
尝试更新Makefile中第一条规则的目标
main
,第一条规则的目标称为缺省目标,只要缺省目标更新了就算完成任务了,其它工作都是为这个目的而做的。由于我们是第一次编译,main
文件还没生成,显然需要更新,但规则说必须先更新了main.o
、stack.o
和maze.o
这三个条件,然后才能更新main
。所以
make
会进一步查找以这三个条件为目标的规则,这些目标文件也没有生成,也需要更新,所以执行相应的命令(gcc -c main.c
、gcc -c stack.c
和gcc -c maze.c
)更新它们。最后执行
gcc main.o stack.o maze.o -o main
更新main
。
如果没有做任何改动,再次运行make
:
$ make
make: `main' is up to date.
make
会提示缺省目标已经是最新的了,不需要执行任何命令更新它。再做个实验,如果修改了maze.h
(比如加个无关痛痒的空格)再运行make
:
$ make
gcc -c main.c
gcc -c maze.c
gcc main.o stack.o maze.o -o main
make
会自动选择那些受影响的源文件重新编译,不受影响的源文件则不重新编译,这是怎么做到的呢?
make
仍然尝试更新缺省目标,首先检查目标main
是否需要更新,这就要检查三个条件main.o
、stack.o
和maze.o
是否需要更新。make
会进一步查找以这三个条件为目标的规则,然后发现main.o
和maze.o
需要更新,因为它们都有一个条件是maze.h
,而这个文件的修改时间比main.o
和maze.o
晚,所以执行相应的命令更新main.o
和maze.o
。既然
main
的三个条件中有两个被更新过了,那么main
也需要更新,所以执行命令gcc main.o stack.o maze.o -o main
更新main
。
现在总结一下Makefile的规则,请读者结合上面的例子理解。如果一条规则的目标属于以下情况之一,就称为需要更新:
目标没有生成。
某个条件需要更新。
某个条件的修改时间比目标晚。
在一条规则被执行之前,规则的条件可能处于以下三种状态之一:
需要更新。能够找到以该条件为目标的规则,并且该规则中目标需要更新。
不需要更新。能够找到以该条件为目标的规则,但是该规则中目标不需要更新;或者不能找到以该条件为目标的规则,并且该条件已经生成。
错误。不能找到以该条件为目标的规则,并且该条件没有生成。
执行一条规则A的步骤如下:
检查它的每个条件P:
如果P需要更新,就执行以P为目标的规则B。之后,无论是否生成文件P,都认为P已被更新。
如果找不到规则B,并且文件P已存在,表示P不需要更新。
如果找不到规则B,并且文件P不存在,则报错退出。
在检查完规则A的所有条件后,检查它的目标T,如果属于以下情况之一,就执行它的命令列表:
文件T不存在。
文件T存在,但是某个条件的修改时间比它晚。
某个条件P已被更新(并不一定生成文件P)。
通常Makefile都会有一个clean
规则,用于清除编译过程中产生的二进制文件,保留源文件:
clean:
@echo "cleanning project"
-rm main *.o
@echo "clean completed"
把这条规则添加到我们的Makefile末尾,然后执行这条规则:
$ make clean
cleanning project
rm main *.o
clean completed
如果在make
的命令行中指定一个目标(例如clean
),则更新这个目标,如果不指定目标则更新Makefile中第一条规则的目标(缺省目标)。
和前面介绍的规则不同,clean
目标不依赖于任何条件,并且执行它的命令列表不会生成clean
这个文件,刚才说过,只要执行了命令列表就算更新了目标,即使目标并没有生成也算。在这个例子还演示了命令前面加@
和-
字符的效果:如果make
执行的命令前面加了@
字符,则不显示命令本身而只显示它的结果;通常make
执行的命令如果出错(该命令的退出状态非0)就立刻终止,不再执行后续命令,但如果命令前面加了-
号,即使这条命令出错,make
也会继续执行后续命令。通常rm
命令和mkdir
命令前面要加-
号,因为rm
要删除的文件可能不存在,mkdir
要创建的目录可能已存在,这两个命令都有可能出错,但这种错误是应该忽略的。例如上面已经执行过一遍make clean
,再执行一遍就没有文件可删了,这时rm
会报错,但make
忽略这一错误,继续执行后面的echo
命令:
$ make clean
cleanning project
rm main *.o
rm: cannot remove `main': No such file or directory
rm: cannot remove `*.o': No such file or directory
make: [clean] Error 1 (ignored)
clean completed
读者可以把命令前面的@
和-
去掉再试试,对比一下结果有何不同。这里还有一个问题,如果当前目录下存在一个文件叫clean
会怎么样呢?
$ touch clean
$ make clean
make: `clean' is up to date.
如果存在clean
这个文件,clean
目标又不依赖于任何条件,make
就认为它不需要更新了。而我们希望把clean
当作一个特殊的名字使用,不管它存在不存在都要更新,可以添一条特殊规则,把clean
声明为一个伪目标:
.PHONY: clean
这条规则没有命令列表。类似.PHONY
这种make
内建的特殊目标还有很多,各有不同的用途,详见[GNUmake]。在C语言中要求变量和函数先声明后使用,而Makefile不太一样,这条规则写在clean:
规则的后面也行,也能起到声明clean
是伪目标的作用:
clean:
@echo "cleanning project"
-rm main *.o
@echo "clean completed" .PHONY: clean
当然写在前面也行。gcc
处理一个C程序分为预处理和编译两个阶段,类似地,make
处理Makefile的过程也分为两个阶段:
首先从前到后读取所有规则,建立起一个完整的依赖关系图,例如:
图 22.1. Makefile的依赖关系图
然后从缺省目标或者命令行指定的目标开始,根据依赖关系图选择适当的规则执行,执行Makefile中的规则和执行C代码不一样,并不是从前到后按顺序执行,也不是所有规则都要执行一遍,例如
make
缺省目标时不会更新clean
目标,因为从上图可以看出,它跟缺省目标没有任何依赖关系。
clean
目标是一个约定俗成的名字,在所有软件项目的Makefile中都表示清除编译生成的文件,类似这样的约定俗成的目标名字有:
all
,执行主要的编译工作,通常用作缺省目标。install
,执行编译后的安装工作,把可执行文件、配置文件、文档等分别拷到不同的安装目录。clean
,删除编译生成的二进制文件。distclean
,不仅删除编译生成的二进制文件,也删除其它生成的文件,例如配置文件和格式转换后的文档,执行make distclean
之后应该清除所有这些文件,只留下源文件。
make基础(转)的更多相关文章
- java基础集合经典训练题
第一题:要求产生10个随机的字符串,每一个字符串互相不重复,每一个字符串中组成的字符(a-zA-Z0-9)也不相同,每个字符串长度为10; 分析:*1.看到这个题目,或许你脑海中会想到很多方法,比如判 ...
- node-webkit 环境搭建与基础demo
首先去github上面下载(地址),具体更具自己的系统,我的是windows,这里只给出windows的做法 下载windows x64版本 下载之后解压,得到以下东西 为了方便,我们直接在这个目录中 ...
- js学习笔记:webpack基础入门(一)
之前听说过webpack,今天想正式的接触一下,先跟着webpack的官方用户指南走: 在这里有: 如何安装webpack 如何使用webpack 如何使用loader 如何使用webpack的开发者 ...
- Golang, 以17个简短代码片段,切底弄懂 channel 基础
(原创出处为本博客:http://www.cnblogs.com/linguanh/) 前序: 因为打算自己搞个基于Golang的IM服务器,所以复习了下之前一直没怎么使用的协程.管道等高并发编程知识 ...
- [C#] C# 基础回顾 - 匿名方法
C# 基础回顾 - 匿名方法 目录 简介 匿名方法的参数使用范围 委托示例 简介 在 C# 2.0 之前的版本中,我们创建委托的唯一形式 -- 命名方法. 而 C# 2.0 -- 引进了匿名方法,在 ...
- HTTPS 互联网世界的安全基础
近一年公司在努力推进全站的 HTTPS 化,作为负责应用系统的我们,在配合这个趋势的过程中,顺便也就想去搞清楚 HTTP 后面的这个 S 到底是个什么含义?有什么作用?带来了哪些影响?毕竟以前也就只是 ...
- Swift与C#的基础语法比较
背景: 这两天不小心看了一下Swift的基础语法,感觉既然看了,还是写一下笔记,留个痕迹~ 总体而言,感觉Swift是一种前后端多种语言混合的产物~~~ 做为一名.NET阵营人士,少少多多总喜欢通过对 ...
- .NetCore MVC中的路由(1)路由配置基础
.NetCore MVC中的路由(1)路由配置基础 0x00 路由在MVC中起到的作用 前段时间一直忙于别的事情,终于搞定了继续学习.NetCore.这次学习的主题是MVC中的路由.路由是所有MVC框 ...
- .NET基础拾遗(5)多线程开发基础
Index : (1)类型语法.内存管理和垃圾回收基础 (2)面向对象的实现和异常的处理基础 (3)字符串.集合与流 (4)委托.事件.反射与特性 (5)多线程开发基础 (6)ADO.NET与数据库开 ...
- .NET 基础 一步步 一幕幕[面向对象之方法、方法的重载、方法的重写、方法的递归]
方法.方法的重载.方法的重写.方法的递归 方法: 将一堆代码进行重用的一种机制. 语法: [访问修饰符] 返回类型 <方法名>(参数列表){ 方法主体: } 返回值类型:如果不需要写返回值 ...
随机推荐
- Java基础---AWT
流式布局FlowLayout package net.zyz; import java.awt.Button; import java.awt.FlowLayout; import java.awt. ...
- Command(命令)-对象行为型模式
1.意图 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化:对请求排队或记录请求日志,以及支持可撤销的操作. 2.别名 动作(Action),事务(Transaction) 3.动机 ...
- 1 Two Sum
// Java public int[] twoSum(int[] nums, int target) { int[] answer = new int[2]; for (int i = 0; i & ...
- [html]LESS-1.3.3
网站:http://www.bootcss.com/p/lesscss/ 下载链接:http://files.cnblogs.com/files/z5337/less-1.3.3.min.js
- NGUI 屏幕自适应(初始设定宽高800x480只支持比其大的屏幕)
自适应讲解部分可以参考以下网址:http://www.xuanyusong.com/archives/2536,下面代码中提到的AdaptiveManualHeight()函数就是参考该文章的. 下面 ...
- table 排序 添加 删除 等操作
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...
- $http服务和$location
1.angular的$http服务跟jquery中的ajax很类似,用法如下: $http({ method : 'GET', url : 'data.php' }).success(function ...
- 开始研究tigase和android客户端的实现
2015.7.5. 昨晚总算把tigaase7.0的开发环境搭起来了.稍微看了下代码结构. 主要是auth,db,compnent,cluster,server,xmpp,这几块,准备先发点时间看看开 ...
- 在ASP.NET MVC中使用Juqery实现页面局部刷新
自己做的一个实验,留作备忘,此实例包括扩一下几个文件: 1.MyMovieController.cs 2.Index.aspx 3.ViewUserControl1.ascx 4.movie类 其中M ...
- HttpWebRequest类
HttpWebRequest类与HttpRequest类的区别. HttpRequest类的对象用于服务器端,获取客户端传来的请求的信息,包括HTTP报文传送过来的所有信息.而HttpWebReque ...