• Makefile基础(一)


    在大型的C语言项目中,一般都是由多个源文件编译链接形成的可执行程序,而这些源文件的处理步骤,通常交给Makefile来管理,Makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 MMakefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。

    我们先来看几个源文件:

    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)
    ", p.row, p.col);
            while (predecessor[p.row][p.col].row != -1)
            {
                p = predecessor[p.row][p.col];
                printf("(%d, %d)
    ", p.row, p.col);
            }
        }
        else
            printf("No path!
    ");
        return 0;
    }
    

      

    main.c包含的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.c包含的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
    

      

    stack.h头文件的实现——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;
    }
    

      

    main.c包含的maze.h头文件:

    #ifndef MAIN_H
    #define MAIN_H
    typedef struct point { int row, col; } item_t;
    #define MAX_ROW 5
    #define MAX_COL 5
    #endif
    

      

    maze.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('
    ');
        }
        printf("*********
    ");
    }
    

      

    我们可以这样来编译C文件:

    [root@localhost linux_c]# gcc main.c stack.c maze.c -o main
    

      

    但这不是一个好办法,如果编译之后又对maze.c做了修改,又要把所有源文件都编译一遍,即时main.c、stack.c和那些头文件没有修改也要跟着重新编译

    又或者我们可以单个单个文件编译,最后再链接成一个可执行程序:

    [root@localhost linux_c]# gcc -c main.c 
    [root@localhost linux_c]# gcc -c stack.c
    [root@localhost linux_c]# gcc -c maze.c 
    [root@localhost linux_c]# gcc main.o stack.o maze.o -o main
    

      

    假如我们后面又对maze.c做了修改,只需要执行gcc -c maze.c重新编译一个maze.o文件,再把这些.o文件链接成一个可执行的程序。

    但假如修改了修改了两个文件,只编译了一个文件,还有一个忘记编译了怎么办?又或者一次性改动的文件太多,总不能挨个挨个执行一次gcc -c生成.o文件,于是,Makefile就来帮我们解决了难题,通常,我们把Makefile文件和源代码放在同一个目录下

    [root@localhost linux_c]# ls
    main.c  main.h  Makefile  maze.c  maze.h  stack.c  stack.h
    [root@localhost linux_c]# cat 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命令进行编译:

    [root@localhost linux_c]# make
    gcc -c main.c
    gcc -c stack.c
    gcc -c maze.c
    gcc main.o stack.o maze.o -o main
    [root@localhost linux_c]# ls
    main  main.c  main.h  main.o  Makefile  maze.c  maze.h  maze.o  stack.c  stack.h  stack.o
    

      

    make命令会自动读取当前目录下的Makefile文件,完成相应的编译步骤。Makefile由一组规则组成,每条规则的格式是:

    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中的每个以Tab开头的命令,make会创建一个shell进程去执行它

    对于上面的例子,make执行如下步骤:

    1. 尝试更新Makefile中第一条规则的目标main,第一条规则的目标称为缺省目标,只要缺省目标更新了就算完成任务了,其它工作都是为这个目的而做的。由于我们是第一次编译,main文件还没生成,显然需要更新,但规则说必须先更新了main.o、stack.o和maze.o这三个条件,然后才能更新main
    2. 所以make会进一步查找以这三个条件为目标的规则,这些目标文件也没有生成,也需要更新,所以执行相应的命令(gcc -c main.c、 gcc -c stack.c和gcc -c maze.c)更新它们
    3. 最后执行gcc main.o stack.o maze.o -o main更新main

    如果没有做任何改动再次执行make:

    [root@localhost linux_c]# make
    make: `main' is up to date.
    

      

    make会提示缺省目标已经是最新的了,不需要执行任何命令来更新它。假如,我们在maze.h文件中加个空格,再运行一下make:

    [root@localhost linux_c]# make
    gcc -c main.c
    gcc -c maze.c
    gcc main.o stack.o maze.o -o main
    

      

    make会自动选择那些改动过的源文件重新编译,没有改动过的源文件则不重新编译,这是怎么做得到呢?

    1.  make仍然尝试更新缺省目标,首先检查目标main是否需要更新,这就要检查三个条件:main.o、stack.o和maze.o是否需要更新
    2. make会进一步查找以这三个条件为目标的规则,然后发现main.o和maze.o需要更新,因为它们都有一个条件是maze.h,而这个文件的修改时间比main.o和maze.o晚,所以执行相应的命令更新main.o和maze.o
    3. 既然main的三个条件中有两个被更新过了,那么main也需要更新,所以执行命令gcc main.o stack.o maze.o -o main更新main

    现在总结一下Makefile的规则,如果一条规则的目标属于以下情况之一,则需要更新:

    •  目标没有生成
    • 某个条件需要更新
    • 某个条件的修改时间比目标晚

    在一条规则被执行前,规则的条件可能处于以下三种状态之一:

    • 需要更新。能够找到以该条件为目标的规则,并且该规则中目标需要更新
    • 不需要更新。能够找到以该条件为目标的规则,但是该规则中目标不需要更新;或者不能找到以该条件为目标的规则,并且该条件已经生成
    • 错误。不能找到以该条件为目标的规则,并且该条件没有生成

    执行一条规则A的步骤如下:

    1. 检查它的每个条件P:
      • 如果P需要更新,就执行以P为目标的规则B。之后,无论是否生成文件P,都认为P已被更新
      • 如果找不到规则B,并且文件P已存在,表示P不需要更新
      • 如果找不到规则B,并且文件P不存在,则报错退出  
    1. 在检查完规则A的所有条件之后,检查它的目标T,如果属于以下情况之一,就执行它的命令列表:
      • 文件T不存在
      • 文件T存在,但是某个条件修改时间比它晚
      • 某个条件P已更新(并不不一定生成文件P)

    通常,Makefile都会有一个clean规则,用于清除编译过程中产生的二进制文件,保留源文件:

    clean:
    	@echo "cleanning project"
    	-rm -f main.o
    	@echo "clean completed"
    

      

    这条规则添加到之前Makefile的末尾,然后执行这条规则:

    [root@localhost linux_c]# cat 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
    clean:
    	@echo "cleanning project"
    	-rm -f main *.o
    	@echo "clean completed"
    

        

    如果在make命令行中指定一个目标(例如:clean),则更新这个目标,如果不指定目标则更新Makefile中第一条规则的目标(缺省目标):

    [root@localhost linux_c]# 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目标不依赖于任何条件,并且执行它的命令列表不会生成clean文件,刚才说过,只要执行了命令列表就算更新了目标,即使目标并没有生成也算。在这个例子还演示了命令前面加上@和-字符的效果:如果make执行的命令前面加上@字符,则不显示命令本身而显示他的最终结果;通常make执行的命令如果出错(该命令的退出状态非0)就立刻终止,不再执行后续命令,但如果前面加上-号,即使这条命令出错,make也会继续执行后续的命令。通常rm命令和mkdir命令前面要加-号,因为rm删除的文件可能不存在,mkdir要创建的目录可能已存在,这两个命令都可能出错。如上面那个例子,执行make clean后发现当前目录并没有main和以.o结尾的文件,但程序还是打印clean completed

    这里还有一个问题,如果当前目录下存在一个clean文件,那么执行make clean会是什么效果?那我们尝试一下:

    [root@localhost linux_c]# touch clean
    [root@localhost linux_c]# make clean
    make: `clean' is up to date.
    

      

    可以看到,如果存在clean这个文件,clean目标又不依赖任何条件,make就认为它不需要更新了。而我们希望把clean当做一个特殊的名字使用,不管它是否存在都必须更新,可以添加一条特殊的规则,把clean声明为一个伪劣目标:

    .PHONY: clean
    

      

    我们在末尾添加这条伪目标,当前如果在前面添加也行

    [root@localhost linux_c]# cat 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
    clean:
    	@echo "cleanning project"
    	-rm  main *.o
    	@echo "clean completed"
    .PHONY: clean
    [root@localhost linux_c]# touch clean
    [root@localhost linux_c]# 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
    

      

    gcc处理一个C程序一般分为预处理和编译两个阶段,类似的,make处理Makefile文件的过程也分为两个阶段:

    1. 先从前到后读取所有的依赖规则,建立一个完整的依赖关系图,如图1-1
    2. 然后从缺省目标或命令行指定的目标开始,根据依赖关系图选择适当的规则执行,执行Makefile中的规则和执行C代码不一样,并不是从前到后顺序执行,也不是所有的规则都执行一遍,例如make缺省目标时不会更新clean目标,因为从图1-1中可以看出,clean目标跟缺省目标没有任何关系

    图1-1 Makefile的关系依赖图

    clean目标是一个约定俗称的名字,在所有软件项目的Makefile中都表示清除编译生成的文件,类似这样约定俗称的名字还有:

    • all:执行主要的编译工作,通常用作缺省目标
    • install:执行编译后的安装工作,把可执行文件、配置文件、文档等分别拷贝到不同的安装目录
    • clean:删除编译生成的二进制文件
    • distclean:不仅删除编译生成的二进制文件,也删除其他生成文件,例如配置文件和格式转换后的文档,执行make distclean之后应该清除所有这些文件,只留下源文件
  • 相关阅读:
    docker run 参数含义
    java——数组队列 ArrayQueue
    java——数组栈 ArrayStack
    java——时间复杂度、动态数组
    java——异常类、异常捕获、finally、异常抛出、自定义异常
    java——变量
    java——虚拟机、线程
    java——内部类
    java——object类
    java——抽象类、接口、二者区别
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/9265539.html
Copyright © 2020-2023  润新知