• 【Linux03-基本工具之make和makefile】Linux下的项目构建工具+进度条小程序


    前言

    接上篇,接着学习基本工具。

    博主水平有限,不足之处望请斧正。

    三、make和makefile

    是什么

    makefile(Makefile):用来写入 依赖关系和依赖方法 的文件。

    make:用来执行 makefile 的命令。


    为什么

    允许我们自动化构建项目,方便。


    怎么用

    写出makefile ==> make执行。

    先见见猪跑:

    [bacon@VM-12-5-centos 3-make]$ touch makefile
    [bacon@VM-12-5-centos 3-make]$ vim makefile
    [bacon@VM-12-5-centos 3-make]$ ls
    makefile
    [bacon@VM-12-5-centos 3-make]$ touch test.c
    [bacon@VM-12-5-centos 3-make]$ vim test.c 
    [bacon@VM-12-5-centos 3-make]$ make
    gcc test.c -o test 
    [bacon@VM-12-5-centos 3-make]$ ls
    makefile  test  test.c
    [bacon@VM-12-5-centos 3-make]$ ./test 
    hello makefile
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    test.c:

    #include 
    int main()
    {
        printf("hello makefile\n");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    makefile:

    test:test.c
    	gcc test.c -o test 
    .PHONY:clean
    clean:
      rm -f test
    
    • 1
    • 2
    • 3
    • 4
    • 5

    “test”:目标文件(要得到的文件)。

    “test.c”:依赖文件(得到目标文件的基础)。

    .PHONY(adj. <口>假的;欺骗的):伪目标修饰(被修饰的目标成为伪目标,每次都生成

    “clean”:没有依赖文件的伪目标,每次都会执行。

    "gcc test.c -o test ":得到目标文件test的方法。

    "rm -f test ":得到目标文件clean的方法。

    以上就构成了makefile的全部要素:依赖关系和依赖方法。

    怎么理解呢?

    又月初了,大学生张三的生活费也差不多了,需要打个电话给老爸要钱。做成这件事必要的两个要素,

    张三打电话给他老爸:“老爸,我是你儿子。”——依赖关系:张三依赖他老爸。

    张三接着说:“该打点生活费了哈。”——依赖方法:这件事上,张三通过要钱来对他老爸产生依赖。

    为什么说是必要的呢?

    张三打电话给李四的老爸:“打点生活费哈。”——依赖关系不成立:张三不依赖李四老爸。

    张三打电话给他老爸:“老爸,我是你儿子。”(随即挂断电话)——没表明依赖方法:张三老爸一头雾水。

    到这里,我们再看makefile:

    test:test.c#依赖关系
    	gcc test.c -o test #依赖方法
    .PHONY:clean#指定clean为伪目标(总是生成)
    clean:#依赖关系
      rm -f test#依赖方法
    
    • 1
    • 2
    • 3
    • 4
    • 5

    “test” 通过 "gcc test.c -o test "的方法 依赖"test.c"而生。

    “.PHONY clean” 总是通过 "rm -f test " 的方法 不依赖任何文件而生。


    再看,

    当我们将已经编译且未修改过的文件再次编译:

    [bacon@VM-12-5-centos 3-make]$ make
    make: `test' is up to date.
    
    • 1
    • 2
    【make是怎么知道不需要再编译的?】
    [bacon@VM-12-5-centos 3-make]$ stat test.c
      File: ‘test.c’
      Size: 80        	Blocks: 8          IO Block: 4096   regular file
    Device: fd01h/64769d	Inode: 921768      Links: 1
    Access: (0664/-rw-rw-r--)  Uid: ( 1003/   bacon)   Gid: ( 1003/   bacon)
    Access: 2022-12-01 19:27:01.871065494 +0800
    Modify: 2022-12-01 19:27:01.515064249 +0800
    Change: 2022-12-01 19:27:01.515064249 +0800
     Birth: -
    [bacon@VM-12-5-centos 3-make]$ stat test
      File: ‘test’
      Size: 8360      	Blocks: 24         IO Block: 4096   regular file
    Device: fd01h/64769d	Inode: 921769      Links: 1
    Access: (0775/-rwxrwxr-x)  Uid: ( 1003/   bacon)   Gid: ( 1003/   bacon)
    Access: 2022-12-01 19:28:21.868345278 +0800
    Modify: 2022-12-01 19:28:21.485343938 +0800
    Change: 2022-12-01 19:28:21.485343938 +0800
     Birth: -
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • Access: 指最后一次读取的时间。

      (一定时间/次数后才更新,读取操作太频繁,总是更新要进行更多IO,慢,也因为这个时间没那么重要,没必要随时更新)

    • Modify: 指最后一次修改内容的时间。

    • Change: 指最后一次修改属性的时间。

    Access和Modify的验证:

    [bacon@VM-12-5-centos 3-make]$ stat test.c
      File: ‘test.c’
      Size: 144       	Blocks: 8          IO Block: 4096   regular file
    Device: fd01h/64769d	Inode: 921768      Links: 1
    Access: (0664/-rw-rw-r--)  Uid: ( 1003/   bacon)   Gid: ( 1003/   bacon)
    Access: 2022-12-01 19:33:55.887513525 +0800
    Modify: 2022-12-01 19:33:55.520512242 +0800
    Change: 2022-12-01 19:33:55.520512242 +0800
     Birth: -
    [bacon@VM-12-5-centos 3-make]$ vim test.c
    [bacon@VM-12-5-centos 3-make]$ stat test.c
      File: ‘test.c’
      Size: 240       	Blocks: 8          IO Block: 4096   regular file
    Device: fd01h/64769d	Inode: 921768      Links: 1
    Access: (0664/-rw-rw-r--)  Uid: ( 1003/   bacon)   Gid: ( 1003/   bacon)
    Access: 2022-12-01 19:34:17.867590404 +0800
    Modify: 2022-12-01 19:34:17.290588387 +0800
    Change: 2022-12-01 19:34:17.290588387 +0800
     Birth: -
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    我们vim进行读取了,access变,也改变了内容,modigy变,

    【但是为什么change也变??】

    :内容改变了,文件大小也改变,文件大小也是属性呀!

    Change的验证:

    [bacon@VM-12-5-centos 3-make]$ chmod a+r test.c
    [bacon@VM-12-5-centos 3-make]$ stat test.c
      File: ‘test.c’
      Size: 240       	Blocks: 8          IO Block: 4096   regular file
    Device: fd01h/64769d	Inode: 921768      Links: 1
    Access: (0664/-rw-rw-r--)  Uid: ( 1003/   bacon)   Gid: ( 1003/   bacon)
    Access: 2022-12-01 19:34:17.867590404 +0800
    Modify: 2022-12-01 19:34:17.290588387 +0800
    Change: 2022-12-01 19:38:12.245410193 +0800
     Birth: -
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    好像,通过源文件和可执行程序的Modify时间就可以确定:

    源文件更加地新:可执行程序已经落后了,需要重新编译。

    源文件更加地新:可执行程序还没修改,不需要再编译了。

    (想验证的话可以通过touch某个已存在文件来更新全部时间)

    所以.PHONY的本质就是不略过这个“对比时间”的规则。

    如果我们用.PHONY将test这个目标文件也变成伪目标,又能强制重复编译:

    .PHONY:test 
    test:test.c
    	gcc test.c -o test 
    .PHONY:clean
    clean:
    	rm -f test
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    [bacon@VM-12-5-centos 3-make]$ make
    gcc test.c -o test 
    [bacon@VM-12-5-centos 3-make]$ make
    gcc test.c -o test 
    [bacon@VM-12-5-centos 3-make]$ make
    gcc test.c -o test 
    [bacon@VM-12-5-centos 3-make]$ make
    gcc test.c -o test
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    makefile了解得差不多了,但make不只是单纯直接执行就万事大吉了。

    • make默认生成第一个目标文件——所以我们直接make就可以生成,要是想也可以指定生成第一个
    • make默认只生成一个目标文件
    [bacon@VM-12-5-centos 3-make]$ make test
    gcc test.c -o test 
    [bacon@VM-12-5-centos 3-make]$ make clean
    rm -f test
    [bacon@VM-12-5-centos 3-make]$ make test; make clean
    gcc test.c -o test 
    rm -f test
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    makefile的推导规则

    想了解它的推导规则,我们得把makefile复杂化一下:

    test:test.o
    	gcc test.o -o test 
    test.o:test.s
    	gcc -c test.s -o test.o
    test.s:test.i
    	gcc -S test.i -o test.s
    test.i:test.c
    	gcc -E test.c -o test.i
    
    
    .PHONY:clean
    clean:
    	rm -f test.i test.s test.o test
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这样其实才是真正过程:

    .c ==预处理==> .i ==编译==> .s ==汇编==> .o ==链接.o==> 可执行

    那我得先生成下面的.i才能一步步往后走啊?

    对的,当make在当前目录找不到test依赖的test.o,就会往下走;又看到当前目录下没有test.o依赖的test.s,就又往下走;直到走到要生成test.i的时候,当前目录下有test.c,就一步步往回执行。

    makefile的推导规则就像栈一样。

    *并不建议这样写,直接用.c一步到位就很好,上面只是为了帮助理解。


    第一个小程序:进度条

    预备知识

    1. 缓冲区问题

    我们先来通过一个简单程序看一个现象:

    #include 
    #include //休眠函数头文件
    
    
    int main()
    {
        printf("i'm here!\n");//带\n
        
        sleep(3);//休眠函数
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    当我们去掉/n:

    #include 
    #include 
    
    
    int main()
    {
        printf("i'm here!");
    
        sleep(3);
    
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    【明明先执行printf,才执行sleep,为什么先sleep才能看到数据被打印?】

    我们的代码是顺序结构,先prinf后sleep,为什么数据休眠完才打印?

    这涉及到缓冲区的概念:执行sleep前,printf一定执行完了。不过printf打印的数据而没有立即刷新到显示器,而是打印到缓冲区。而sleep执行完,程序退出,才再刷新缓冲区。就看到了“先sleep,后printf”的现象,其实只是看不见printf先执行,而不是它后执行。

    【为什么不立即刷新缓冲区或是直接打印到显示器?】

    因为显示器是外设,速度太慢,频繁的访问会导致效率降低——所以我们按需刷新(显示器默认一行刷新一次)。

    *今天不过多解释缓冲区的概念,以免提高学习成本。

    我们也可以手动刷新缓冲区:

    #include 
    #include 
    
    
    int main()
    {
        printf("i'm here!");
    
        fflush(stdout);//刷新缓冲区的函数
    
        sleep(3);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

    不通过显示器默认的行刷新来刷新,而是手动刷新,也能达到效果。

    2. 回车换行的概念

    回车和换行其实不是一个概念:

    • 回车:光标移动到行首
    • 换行:光标移动到下一个行同一位置

    只不过我们想要的效果是 回车 + 换行,所以键盘上的ENTER键就直接是 回车+换行 的效果
    在这里插入图片描述

    (较早期的键盘,键帽形状都很形象)

    一般 \r 是回车,\n是换行,不过语言给我们做了处理,\n直接就是回车换行。

    进度条

    我们要实现的效果大概是:

    先来个倒数感受一下:
    makefile:

    process:main.c process.c
    	gcc main.c process.c -o process 
    .PHONY:clean
    clean:
    	rm -f process
    
    • 1
    • 2
    • 3
    • 4
    • 5
    //process.h
    #pragma once 
    
    #include 
    
    void ProcessOn();
    
    //process.c
    #include "process.h"
    
    void ProcessOn()
    {
        printf("test\n");
    }
    
    //main.c
    
    include "process.h"
    
    int main()
    {
        ProcessOn();
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    [bacon@VM-12-5-centos process]$ ./process 
    test
    
    • 1
    • 2

    makefile中不需要写.h相关的操作,因为.h就在当前目录下能找到。

    再来实现进度的倒数(百分比):

    倒计时当然不能用\n换行,不是我们要的效果。所以换行,打印完一个数字后换行到行首再次覆盖式打印,就能原地倒数。

    void ProcessOn()
    {
        int cnt = 9;
    
        while(cnt)
        {
            printf("On: %d\r", cnt--);
          
          	sleep(1);//休眠函数,让程序休眠1秒
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    [bacon@VM-12-5-centos process]$ ./process 
    [bacon@VM-12-5-centos process]$ 
    
    • 1
    • 2

    程序没问题啊,为什么不打印信息?

    还是缓冲区的问题,显示器默认行缓冲,咱这里换行无法刷新,所以手动刷新。

    void ProcessOn()
    {
        int cnt = 9;
    
        while(cnt)
        {
            printf("On: %d\r", cnt--);
    
            fflush(stdout);
    
            sleep(1);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    再试试10秒:

    int cnt = 10;
    
    • 1

    在这里插入图片描述

    这可咋办?格式控制

    printf("On: %2d\r", cnt--);
    
    • 1

    在这里插入图片描述

    倒数的道理其实和进度条差不多,主要就是缓冲区问题和回车的概念,再来看看进度条吧!


    //process.h
    #pragma once 
    
    #include 
    #include 
    
    
    #define SYMBOL '#'
    #define BARSIZE 101 //100个填充符号 + \0
    
    void ProcessOn();
    
    //process.c
    #include "process.h"
    
    void ProcessOn()
    {
        char bar[BARSIZE] = "";
        int cnt = 0;
    
        while(cnt <= 100) // [0, 100]
        {
          	//打印 1/2/.../100个SYMBOL
          	//-100控制右对齐
          	//%将%转义,让它不是格式控制前缀,只是正常的字符
            printf("[%-100s] [%d%%]\r", bar, cnt); 
    
            bar[cnt++] = SYMBOL;
    
            fflush(stdout);
            //sleep(1); //每1%停1秒太慢,咱们这里让他5秒跑完:可以用usleep(),单位微秒(1微秒 = 1秒/1百万
            usleep(50000); //1秒打印20次,1次需要 1/20 秒, 再乘1百万,1百万/20 = 5万
    
        }
    
        printf("\n完成!\n");
    }
    
    
    //main.c
    #include "process.h"
    
    int main()
    {
        ProcessOn();
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    在这里插入图片描述

    但,当进度不推进(卡住)的时候,我们也没法看出来呀,搞个动态的标识就好了!最终代码和效果如下:

    //process.h
    #pragma once 
    
    #include 
    #include 
    
    
    #define SYMBOL '#'
    #define BARSIZE 101 //100个填充符号 + \0
    
    
    void ProcessOn();
    
    
    //process.c
    #include "process.h"
    
    void ProcessOn()
    {
        char bar[BARSIZE] = "";
        char rotate[] = {'|', '/', '-', '\\'};
        int cnt = 0;
    
        while(cnt <= 100) // [0, 100]
        {
            printf("[%-100s] [%-3d%%][%c]\r", bar, cnt, rotate[cnt % sizeof(rotate)]); //打印 1/2/.../100个SYMBOL
    
            bar[cnt++] = SYMBOL;
    
            fflush(stdout);
            //sleep(1); //每1%停1秒太慢,咱们这里让他5秒跑完:可以用usleep(),单位微秒(1微秒 = 1秒/1百万
            usleep(50000); //1秒打印20次,1次需要 1/20 秒, 再乘1百万,1百万/20 = 5万
    
        }
    
        printf("\n完成!\n");
    }
    
    
    //main.c
    #include "process.h"
    
    int main()
    {
        ProcessOn();
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    在这里插入图片描述


    今天的分享就到这里了,感谢观看!

    这里是培根的blog,期待与你共同进步,

    下期见~

  • 相关阅读:
    【基础】并发(ThreadLocal、synchronized、线程池、锁、volatile)面试题
    预处理器Less
    公共供水管网漏损治理智能化管理系统解决方案
    【自学前端】HTML篇已完结(附14节视频)
    @Linux系统安装部署Sql Server(MSSQL)
    实现响应式布局有几种方法
    ADBMS1818驱动程序解析
    粒子群算法(PSO)优化最小二乘支持向量机回归预测,PSO-LSSVM回归预测,多输入单输出模型。
    【走方格的方案数】
    Istio实践(1)- 环境搭建及应用部署
  • 原文地址:https://blog.csdn.net/BaconZzz/article/details/128159231