Bootstrap

从零Makefile落地算法大项目

目录

前言

转自:从零Makefile落地算法大项目
作者:手写AI
博主自己过了一遍,内容可能有所偏差,有点长需要各位耐心看完😄,强烈建议读原文!!!。目的——和大家分享这篇非常nice的文章同时自己重新学习下Makefile。PPT、代码均在here

1. g++指令介绍

1.1 g++/gcc是什么,有什么区别
  • g++和gcc都是gnu推出的cpp编译器,时代不同
  • g++和gcc都可以进行cpp编译
  • g++和gcc一样,都属于driver,即驱动编译,它会调用cclplus/ccl/ld/ad等指令实现编译链接等工作,它俩只是默认选项上的处理不同
  • 本文采用g++而不是gcc
  • g++等价于gcc -xc++ -lstdc++ -shared-libgcc
  • 参考自gcc和g++是什么关系?
1.2 g++的编译过程

完整的编译过程包含预处理、汇编、编译、链接4个步骤,注意相关指令的大小写

  • 预处理

    $ g++ -E main.cpp -o main.i
    
  • 汇编

    $ g++ -S main.i -o main.s
    
  • 编译

    $ g++ -c main.s -o main.o
    
  • 链接

    $ g++ main.o -o main.bin
    

g++可以允许跳过中间步骤,例如下面二者结果是等价的:

  • 跳过预处理和汇编过程

    $ g++ -S main.cpp -o main.s
    $ g++ main.s -o main.bin
    
  • 跳过预处理、汇编和编译过程

    $ g++ main.cpp -o main.bin
    

对于开发人员来说,需要关注的是编译和链接两个过程

  • 编译—代码编译到二进制

    $ g++ -c main.cpp -o main.o
    
  • 链接—多个二进制链接成可执行程序

    $ g++ main.o -o main.bin
    

下面为大家分析这四个过程,主要用到两个文件:test.hpp用于函数声明,main.cpp用于函数调用,二者具体内容如下:

  • test.hpp

    int add(int a, int b);
    int mul(int a, int b);
    
  • main.cpp

    #include "test.hpp"
    
    #define Title        "This is "
    #define Add(a, b)     a + b
    
    int main(){
    
    	const char * text = Title "Computer Vision";
    	int sum = Add(5, 9);
    	
    	return 0;
    }
    

预处理指令效果如下:g++ -E main.cpp -o main.i

预处理后,宏定义和包含的头文件都会被展开

在这里插入图片描述

汇编指令效果如下:g++ -S main.i -o main.s

汇编指令后,转为汇编代码

在这里插入图片描述

编译指令效果如下:g++ -c main.s -o main.o

编译指令后,转为二进制代码,ELF文件格式

在这里插入图片描述

链接指令效果如下:g++ main.o -o out.bin

链接指令后,将二进制代码链接为可执行程序,可使用readelf查看链接的库文件,具体指令如下

$ readelf -d 可执行文件名

在这里插入图片描述

2. C++编译链接

2.1 C++编译链接流程图

在这里插入图片描述

2.2 C++的声明和实现

C++的声明代表一个函数的符号或者ID,实现则代表函数的具体执行的代码,如下所示

// 声明
int add(int, int);
int add(int a, int b);

// 实现
int add(int a, int b){
    return a + b;
}

声明不关心参数名称是什么,也不关心返回值是什么,也就是说int add(int, int)int add(int a, int b)是等价的。

2.3 C++的编译过程-案例
2.3.1 代码结构 main.cpp和test.cpp

main.cpptest.cpp具体内容如下

在这里插入图片描述

2.3.2 main.cpp的汇编代码

指令g++ -S main.cpp -o main.s生成main.smain.cpp的汇编代码如下

在这里插入图片描述

2.3.3 test.cpp的汇编代码

指令g++ -S test.cpp -o test.s生成test.stest.cpp的汇编代码如下

在这里插入图片描述

2.3.4 两者汇编代码对比

main.stest.s二者相比如下所示

  • main.s里面没有add函数的具体实现,只有call add操作
  • add的具体实现在test.s里面

在这里插入图片描述

2.3.5 带有命名空间的汇编

带有命名空间的汇编代码如下

在这里插入图片描述

2.4 C++编译过程

关于编码的结论是

  • C++中的函数/符号/变量会被编码
  • 函数的编码关心的是:函数名称、所在命名空间、参数类型
  • 函数的编码不关心的是:返回值类型

关于编译的结论是

  • 调用一个函数,会生成call 函数名称的代码,函数的具体实现不会搬过来

在这里插入图片描述

2.5 C++链接过程

C++的链接过程如下,实际链接指令为g++ main.o test.o -l3rd -lpkg -o out.bin,需要注意的是任何链接符号(如add)会在所有链接对象中查找实现,例如这里的test.olib3rd.solibpkg.a中找。如果没找到,则报错undefined reference _Z3addii,如果找到多个,则报错multiple definition of add,任何符号的实现全局只能有一个。关于链接时的动态库和静态库区别如下:

  • 如果add函数在动态库lib3rd.so中,out.bin会引用这个符号的so文件,在运行时动态加载lib3rd.so后,再调用add函数
  • 如果add函数在静态库libpkg.a中,out.bin将add的实现代码完整复制到out.bin中,运行out.bin时不再需要libpkg.a

在这里插入图片描述

2.6 编译链接完整过程

从编译到链接到运行的完整过程如下,指令如下

$ g++ -c main.cpp -o main.o
$ g++ -c test.cpp -o test.o
$ g++ main.o test.o -o out.bin
$ ./out.bin

在这里插入图片描述

2.6.1 链接时,查找so文件、a文件的方式

当程序链接时即g++ -lpkg -l3rd main.o -o out.bin,如何决定链接的是哪个so、a文件,是按照以下依据来的

  • g++ -lname,表示链接一个动态或静态库(即.so/.a),库文件名字是libname.so/libname.a
  • g++ -Lfolder,表示配置一个动态或静态库的查找目录
  • g++查找so/a文件的地方有3个,按照以下优先顺序查找
    • 第一顺序:-L配置的目录
    • 第二顺序:g++内置的系统目录,如/usr/lib...
    • 第三顺序:系统环境变量(如LIBRARY_PATH)指定的目录
2.6.2 运行时,查找so文件的方式

当程序运行时即./out.bin,此时so文件的查找按照如下进行

  • 第一顺序:应用程序的当前目录
  • 第二顺序:out.bin中存储的rpath(run path)目录。readelf -d out.bin指令可以查看文件的runpath信息。如果该选项指定了依旧失效,说明依赖的so文件还存在更多依赖在其他目录没有明确
  • 第三顺序:环境变量指定的目录(如LD_LIBRARY_PATH)
2.6.3 编译时,查找头文件的方式

当程序编译时,头文件的查找方式按照如下进行

  • g++ -Ifolder表示配置一个头文件查找目录
  • #include "path/name.hpp"使用双引号时,编译器会在当前文件的目录下查找path/name.hpp
  • #include <path/name.hpp>使用尖括号时
    • 第一顺序:以g++ -I配置的路径查找,例如g++ -I/data/folder,确认路径时/data/folder/path/name.hpp,对所有路径进行测试,找到为止
    • 第二顺序:g++内置的系统路径,一般是usr/include等等,指令g++ -print-search-dirs可以打印出来
    • 第三顺序:系统环境变量配置的路径,如C_INCLUDE_PATH, CPP_INCLUDE_PATH

3. Makefile

3.1 Makefile基础-解决的问题是什么
  1. 使用GCC的命令行编译代码在单个文件下较方便,当工程文件逐渐增多时,使用GCC命令编译就会变得力不从心。此时我们需要借助项目构造工具make帮助我们完成编译工作
  2. GNU make是一个命令工具,是一个解释makefile中指令的命令工具,一边来说大多数的IDE都有这个命令
  3. make工具在构建项目时需要加载一个叫做makefile的文件,makefile关系到了整个工程的编译规则。一个工程中的源文件不计其数,其类型、功能、模板分别放在若干个目录中,Makefile定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。
  4. Makefile为我们带来了极大地好处——“自动化编译”,一旦写好,只需要一个make命令,整个工程就完全自动编译,极大的提升了软件开发的效率。
  5. make指令执行时,默认会查找当前目录下的makefile或Makefile作为代码文件,当然也可以make -f abc的方式指定make运行的Makefile代码,因此在一个项目中可以有多个makefile文件,分别位于不同的目录中。
  6. Makefile主要解决的问题是描述生成依赖关系,根据生成和依赖文件修改时间新旧决定是否执行command。虽然可手动调用g++编译,但如果每次编译时都是全体代码参与,对于没有修改部分的代码进行编译是浪费时间。而项目文件越多,这个问题越严重。Makefile可以帮我们解决这个问题
  7. Makefile的重点有:描述依赖关系command(生成文件的指令)
  8. 学习Makefile的基础操作足以应付项目需求即可,并不需要学习全部语法
  9. 参考官方文档,可查看更多定义GNU make
  10. 参考自爱编程的大丙的Makefile
3.2 Makefile基础-代码域

Makefile包含变量定义域、依赖定义域以及command域,如下所示。当执行make main.o时,会检查main.omain.cpp的修改时间,决定是否执行生成指令g++ -c main.cpp -o main.o

在这里插入图片描述

3.3 Makefile基础
3.3.1 语法
  • 数据类型包含字符串和字符串数组
  • 变量定义如var := folder,定义变量var为string类型,值是folder
  • 数组定义如var := hello world folder,定义变量var为数组类型,值是[“hello”, “world”, “folder”]
  • 变量定义的方式有多种
    • =赋值 var = folder 基本赋值,Makefile全部执行后决定取值(不常用)
    • :=赋值 var := folder 基本赋值,当前所在位置决定取值(常用)
    • ?=赋值 var ?= folder 如果没有赋值,则赋值为folder
    • +=赋值 var += folder 添加值,在var后面添加值。可以认为数组后面增加元素append,var := hello, var += world,结果是hello world,中间有空格
  • $(var),解释为var的值,如var2 := $(var)
  • $(func param),调用Make提供的内置函数
    • var := $(shell echo hello),定义var的值为执行shell echo hello后的输出
    • $(info $(var)),直接打印var变量内容
    • var := $(subst a, x, aabbcc),替换aabbcc中a为x后赋值给var,结果为xxbbcc
  • 逻辑语法如ifeq、ifneq、ifdef、ifndef
    ifeq($(var), depends)
    	name := hello
    endif
    

示例如下

# 定义变量a,赋值为folder
a := folder

# 为变量a,增加内容directory,结果是:folder directory
a += directory

# 定义变量b,为字符串拼接,结果是:beginfolder directoryend
b := begin$(a)end

# 定义变量c,为执行ls指令后输出的字符串
c := $(shell ls)

# 定义变量d,将xiao中的x,替换为a,结果是:aiao
# 或者 d := $(xiao:x=a) 或者 d := ${xiao:x=a}
d := $(subst x, a, xiao)

# 定义变量e,为在a的每个元素前面增加-L符号,结果是:-Lfolder -Ldirectory
e := $(patsubst %, -L%, $(a))

# 打印变量e的内容
$(info e = ${e})

:生成项可以没有依赖项,那么如果该生成项文件不存在,command将永远执行

图示如下

在这里插入图片描述

3.3.2 依赖关系定义

依赖关系定义如下图所示

  • 第一次执行make a.o时,由于a.o不存在,执行了command
  • 第二次执行make a.o时,由于a.cpp时间没有比a.o新,打印a.o is up to date,不需要编译
  • 生成项和依赖项,从来都是当成文件来看待的

在这里插入图片描述

3.3.3 编译和链接结合起来

编译和链接结合,整体流程如图所示

  • 定义好依赖后make out.bin,会自动查找依赖关系,并自动按照顺序执行command
  • 这是makefile解决的核心问题即描述生成关系,剩下就是如何更方便。比如自动检索a.cppb.cpp,自动定义a.o依赖a.cpp

在这里插入图片描述

3.3.4 总结
  1. 变量赋值有4种方式var = 123,var := 123,var ?= 123,var += 123。其中var := 123var += 123常用

  2. 取变量值有2种方式 $(var),${var}。小括号大括号都可以

  3. 数据类型只有字符串和字符串数组,空格隔开表示多个元素

  4. $(function arguments)是调用make内置函数的方法,具体可以参考官方文档的函数大全。但常用的其实只有少数两个即可

  5. 依赖关系定义种,如果代码修改时间比生成的更新或生成不存在时,command会执行

  6. 依赖关系可以链式的定义,即b依赖a,c依赖b,而make会自动链式的查找并根据时间执行command

  7. command是shell指令,可以使用$(var)来将变量用到其中。前面加@表示执行时不打印原指令内容。否则默认打印指令后再执行指令

  8. make不写具体生成名称,则会选择依赖关系中的第一项生成

4. 基于Makefile的标准工程结构

4.1 Makefile工程结构

一个标准工程做如下定义:

  • 具有src目录,存放代码,可能有多级,例如main.cppfoo/foo.cpp
  • 具有objs目录,存放由cpp编译后得到的o文件等中间文件
  • 具有workspace目录,存放编译后的可执行程序、资源、数据
  • .vscode目录,存放vscode的cpp配置,用于语法解析。
  • Makefile文件,当前工程的Makefile代码

在这里插入图片描述

4.2 代码

代码如图所示

在这里插入图片描述

4.3 编写Makefile文件
4.3.1 解决多级目录cpp检索问题

解决cpp文件多级目录的查找问题,可以使用shell指令的find,如下所示

在这里插入图片描述

4.3.2 替换src/为objs,o文件放到objs中

在这里插入图片描述

4.3.3 定义依赖关系,通配

objs/%.o和src/%.cpp代表了通配依赖关系,模式匹配,其中%为通配符相当于变量部分,如下所示

在这里插入图片描述

4.3.4 为o文件创建目录

编译失败,目录不存在,如下所示

在这里插入图片描述

  • 编译失败原因在于objs/foo目录不存在造成的。对于高版本的g++(例如9.0)不会报错并为你创建objs/foo目录
  • 因此需要我们创建objs/foo目录,需要执行mkdir -p dir($@),通过dir($@)获取目录后创建,如下所示

在这里插入图片描述

4.3.5 链接所有o文件生成可执行程序

定义workspace/pro的生成,依赖自所有的o文件,如下所示

在这里插入图片描述

4.3.6 完善Makefile

完善makefile如下所示,添加如下内容

  • 添加make pro,简介的编译程序
  • 添加make run,编译后执行
  • 添加make clean,清理编译后生成的文件
  • 添加.PHONY,让我们作为指令存在的东西,视作指令。即make时永远执行command

在这里插入图片描述

4.3.7 完整版Makefile

完整版的makefile如下所示


# 检索src目录查找cpp为后缀的文件,用shell指令的find
srcs := $(shell find src -name "*.cpp")

# 将src的后缀为.cpp替换为.o
objs := $(srcs:.cpp=.o)

# 将src/前缀替换为objs/前缀,将o文件放到objs目录下 %代表通配符
objs := $(objs:src/%=objs/%)

# 定义objs下的o文件,依赖src下对应的cpp文件
# $@ = 左边的生成项
# $< = 右边的依赖项第一个
objs/%.o : src/%.cpp
	@echo 编译$<, 生成 $@, 目录是:$(dir $@)
	@mkdir -p $(dir $@)
	g++ -c $< -o $@

# $^ = 右边依赖项全部
# 将pro放到workspace下面
workspace/pro : $(objs)
	@echo 这里的依赖项所有是[$^]
	@echo 链接$@
	g++ $^ -o $@

# 定义简洁指令,make pro即可生成程序
pro : workspace/pro
	@echo 编译完成

# 定义make run,编译好pro后执行
run : pro
	@cd workspace && ./pro

# 定义make clean,请理编译产生的文件
clean :
	@rm -rf workspace/pro objs

# 定义伪符号,这些符号不作为文件,视作指令
# 也可以说,视作永远都不存在的文件
.PHONY : pro run debug clean

debug :
	@echo srcs is[$(srcs)]
	@echo objs is[$(objs)]	
	

在这里插入图片描述

4.4 测试

测试结果如下所示

在这里插入图片描述

5. 基于Makefile实现的完整功能项目

代码下载地址

5.1 Makefile工程-一个复杂的例子,实现http请求
  • 实现的目的——锻炼一个完整的相对完善的工程案例,锻炼代码调试
  • 实现的效果——实现一个程序,可以从任何网址上下载东西
  • 实现的依赖——openssl、libcurl
    • openssl-这是用于实现加密通信的加密算法库。用于访问https开头的链接
    • libcurl-这是用于实现http/https的访问操作,如果要访问https,则依赖openssl
    • 下载地址Baidu Drive[password:make]
5.2 下载和编译libcurl/openssl
5.2.1 准备工作
  • 创建build目录,用于存储下载后的文件,准备用来编译
  • 创建lean目录,用于存放编译后的结果,并作为依赖项目录
  • 将下载后的压缩包放到build目录下,并解压

在这里插入图片描述

5.2.2 编译openssl

编译指令如下

$ cd openssl-1.1.1j
$ pwd
$ ./config --prefix=/home/huanyu/makefile/make2/lean/openssl-1.1.1j
$ make all -j6 && make install -j6
  • ./config是配置生成Makefile,指定install到/home/huanyu/makefile/make2/lean/openssl-1.1.1j目录。make all -j6 && make install -j6这里-j16是同时16个线程执行操作,编译后执行安装
  • lean目录修改为你当前想放的位置

图示如下

在这里插入图片描述

5.2.3 编译libcurl

编译指令如下

$ cd curl-7.78.0/
$ pwd
$ ./configure --prefix=/home/huanyu/makefile/make2/lean/curl-7.78.0 --with-openssl=/home/huanyu/makefile/make2/lean/openssl-1.1.1j
$ make all -j6 && make install -j6
  • --prefix设置安装目录,指定编译好的curl存放位置
  • --with-openssl指令刚才编译安装openssl的目录
  • ./configure配置curl生成Makefile文件
  • 执行make all -j6实现编译
  • 执行make install -j6实现安装

图示如下

在这里插入图片描述

5.2.4 编译结果

编译完成后的结果如下所示,在lean目录下包含openssl和curl的头文件和库文件

在这里插入图片描述

5.3 配置IntellSense和browse路径
  • 变量${workspaceFolder}代表当前目录,即/home/huanyu/makefile/make2

在这里插入图片描述

5.4 配置Makefile

整个makefile的配置如下


srcs := $(shell find src -name "*.cpp")
objs := $(srcs:.cpp=.o)
objs := $(objs:src/%=objs/%)

include_paths := /home/huanyu/makefile/make2/lean/openssl-1.1.1j/include \
				 /home/huanyu/makefile/make2/lean/curl-7.78.0/include

library_paths := /home/huanyu/makefile/make2/lean/openssl-1.1.1j/lib \
				 /home/huanyu/makefile/make2/lean/curl-7.78.0/lib

ld_librarys	  := curl ssl crypto

# 把每一个头文件路径前面增加-I,库文件路径前面增加-L,链接选项前面加-l
run_paths	  := $(library_paths:%=-Wl,-rpath=%)
include_paths := $(include_paths:%=-I%)
library_paths := $(library_paths:%=-L%)
ld_librarys	  := $(ld_librarys:%=-l%)

$(info $(run_paths))

compile_flags := -std=c++11 -w -g -O0 $(include_paths)
link_flags 	  := $(library_paths) $(ld_librarys) $(run_paths)

objs/%.o : src/%.cpp
	@echo 编译$<, 生成$@, 目录是: $(dir $@)
	@mkdir -p $(dir $@)
	@g++ -c $< -o $@ $(compile_flags)

workspace/pro : $(objs)
	@echo 链接$@
	@g++ $^ -o $@ $(link_flags)

pro : workspace/pro
	@echo 编译完成

run : pro
	@cd workspace && ./pro

clean :
	@ rm -rf workspace/pro objs

.PHONY : pro run debug clean

图示如下

在这里插入图片描述

5.5 代码和效果
5.5.1 代码
  • download.hpp

    
    #ifndef DOWNLOAD_HPP
    #define DOWNLOAD_HPP
    
    #include <string>
    
    std::string download(const std::string& url);
    
    # endif // DOWNLOAD_HPP
    
  • download.cpp

    
    #include "download.hpp"
    #include <curl/curl.h>
    
    using namespace std;
    
    // 这个回调函数,是为了让curl获取到数据后,写入的管道。我们通过string的append函数
    // 写入到string对象中。string可以储存二进制也可以储存字符串
    size_t write_data(const void* buffer, size_t count, size_t size, void* user_data){
        string* stream = static_cast<string*>(user_data);
        const char* pbytes = static_cast<const char*>(buffer);
        stream->append(pbytes, count * size);
        return count * size;
    }
    
    string download(const string& url){
    
        CURL *curl = curl_easy_init();
        string response;
    
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &write_data);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 1);
    
        CURLcode code = curl_easy_perform(curl);
        if(code != CURLE_OK){
            printf("Has error: code = %d, meassage: %s\n", code, curl_easy_strerror(code));
        }
        curl_easy_cleanup(curl);
        return response;
    
    }
    
  • main.cpp

    #include <stdio.h>
    #include <fstream>
    #include "download.hpp"
    
    using namespace std;
    
    int main(){
        
        // 下载一个图片文件
        string sxai = download(
            "http://www.zifuture.com:1556/fs/sxai/2021/07/pro-18432c111ca44aa9bba49eab650f466c.jpg"
        );
    
        // 打印大小
        printf("sxai.size = %d byte\n", sxai.size());
        
        // 储存图片数据到sxai.jpg文件
        ofstream of("sxai.jpg", ios::binary | ios::out);
        of.write(sxai.data(), sxai.size());
        of.close();
        return 0;
    }
    
5.5.2 演示效果

演示效果如下图所示

在这里插入图片描述

5.6 分析程序依赖项
5.6.1 使用readlf -d workspace/pro分析

指令如下

$ readelf -d workspace/pro

效果如下

在这里插入图片描述

5.6.2 使用ldd workspace/pro分析

指令如下

$ ldd workspace/pro

效果如下

在这里插入图片描述

6. 配置C++的调试功能

一、相关疑问

  1. .vscode文件夹是什么?

    .vscode是一个隐藏文件夹,其主要作用是以.json的方式保存相关配置信息,如编译任务的配置、调试任务的配置、工作环境的配置等等。

  2. c_cpp_properties.json是什么?

    c_cpp_properties.json是C/C++插件UI界面的json形式,其主要作用是配置C/C++插件代码的智能提示,方便代码的书写

  3. task.json是什么?

    task.json是用来配置编译文件的信息,用于生成可执行文件

  4. launch.json是什么?

    launch.json是用来配置调试文件的信息,比如指定调试语言环境,指定调试类型等等。

二、生成上述文件操作

  1. c_cpp_properties.json

    ctrl+shift+p显示命令行面板,键入C/C++:Edit Configurations(JSON),选择即可

  2. tasks.json

    终端->配置任务->使用模板创建tasks.json文件->Other

  3. launch.json

    .vscode文件夹下新建launch.json,然后点击运行->添加配置->C/C++: (gdb) 启动

6.1 配置task.json

task.json配置如下

{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build",
            "type": "shell",
            "command": "make pro -j6"
        }
    ]
}

图示如下

在这里插入图片描述

6.2 配置launch.json

launch.json配置如下,关于launch.json文件更多属性的含义可参考配置VScode的launch.json文件vscode做C++开发

{
    "version": "0.2.0",
    "configurations": [
    {
        "name": "(gdb) 启动",
        "type": "cppdbg",
        "request": "launch",
        "program": "${workspaceFolder}/workspace/pro",
        "args": [],
        "stopAtEntry": false,
        "cwd": "${workspaceFolder}/workspace",
        "environment": [],
        "externalConsole": false,
        "MIMode": "gdb",
        "setupCommands": [
            {
                "description": "为 gdb 启用整齐打印",
                "text": "-enable-pretty-printing",
                "ignoreFailures": true
            },
            {
                "description":  "将反汇编风格设置为 Intel",
                "text": "-gdb-set disassembly-flavor intel",
                "ignoreFailures": true
            }
        ],
        "preLaunchTask": "build"
    }
    ]
}

图示如下

在这里插入图片描述

6.3 调试

main.cpp中设置断点,按下F5进入调试,如下所示

在这里插入图片描述

6.4 界面介绍

调试界面介绍如下所示

在这里插入图片描述

7. 拓展-头文件修改自动编译

问题:如果头文件修改了那么依赖头文件的cpp会发生编译吗?如果不编译会带来什么后果?

回答:如果头文件修改,依赖头文件的cpp没有发生编译,可能会有莫名的错误(不常发生)。如果发生了make clean后重新编译即可。但这并不是最好的解决方案。

7.1 问题

当前有a.hppa.cpp两个文件,头文件定义了Number,修改Number后看编译结果是否符合预期

  • a.hpp

    #define Number 520
    
  • a.cpp

    
    #include <stdio.h>
    #include "a.hpp"
    
    int main(){
    
        printf("Number = %d\n", Number);
        return 0;
    }
    
  • Makefile

    
    a.o : a.cpp
    	@echo 编译 $<
    	@g++ -c $< -o $@
    
    out.bin : a.o
    	@echo 链接 $@
    	@g++ $^ -o $@
    
    run : out.bin
    	@./out.bin
    
    clean :
    	@rm a.o out.bin
    

图示如下

在这里插入图片描述

编译运行如下所示,可以看到修改头文件a.hpp中的Number后,并没有编译a.cpp

在这里插入图片描述

7.2 原因分析
  • 原因:缺少a.o对hpp依赖关系的定义。makefile中没有定义a.o : a.hpp,没有要求编译a.cpp需要检查a.hpp的时间
  • 解决方案:直接增加a.o : a.cpp a.hpp,强制要求a.o生成时检查a.hpp

图示如下

在这里插入图片描述

7.3 解决方案

问题:强制加入a.hpp检查虽然能解决头文件修改编译问题,但是a.hpp来自于a.cpp的#include语法,我们可能随时修改a.cpp依赖不同的hpp文件,如何保证自动完成?

7.3.1 使用g++ -MM a.cpp -MF a.mk -MT prefix/a.o生成makefile文件a.mk

g++ -MM a.cpp -MF a.mk -MT prefix/a.o

  • 该指令产生a.mk文件,里面写了a.cpp依赖的头文件。a.mk本身就是makefile语法,它解析了a.cpp的全部头文件后分析依赖的头文件,并写成makefile语法
  • -MM 分析引号类型的包含如#include "a.cpp"
  • -M 分析全部包含(尖括号和引号)
  • -MF 指定存储路径
  • -MT 指定依赖项名称

图示如下

在这里插入图片描述

7.3.2 通过include a.mk包含生成的文件,使其生效
  • 使用g++ -MM a.cpp -MF a.mk -MT a.o生成a.mk
  • 为了使编译后的a.mk生效,通过include a.mk包含进来

图示如下

在这里插入图片描述

7.3.3 整合

整合后的makefile文件如下


# 如果make的指令不是clean,就包含
# 因为clean不需要这个依赖关系
ifneq ($(MAKECMDGOALS), clean)
include a.mk
endif

a.o : a.cpp
	@echo 编译 $<
	@g++ -c $< -o $@

out.bin : a.o
	@echo 链接 $@
	@g++ $^ -o $@

a.mk : a.cpp
	@echo 生成依赖a.mk
	@g++ -MM a.cpp -MF a.mk -MT a.o

run : out.bin
	@./out.bin

clean :
	@rm -rf a.o out.bin a.mk

图示如下

在这里插入图片描述

7.3.4 集成到项目中去

修改之前项目中的makefile如下所示


srcs := $(shell find src -name "*.cpp")
objs := $(srcs:.cpp=.o)
objs := $(objs:src/%=objs/%)
mks  := $(objs:.o=.mk)

include_paths := /home/huanyu/makefile/make2/lean/openssl-1.1.1j/include \
				 /home/huanyu/makefile/make2/lean/curl-7.78.0/include

library_paths := /home/huanyu/makefile/make2/lean/openssl-1.1.1j/lib \
				 /home/huanyu/makefile/make2/lean/curl-7.78.0/lib

ld_librarys	  := curl ssl crypto

# 把每一个头文件路径前面增加-I,库文件路径前面增加-L,链接选项前面加-l
run_paths	  := $(library_paths:%=-Wl,-rpath=%)
include_paths := $(include_paths:%=-I%)
library_paths := $(library_paths:%=-L%)
ld_librarys	  := $(ld_librarys:%=-l%)

compile_flags := -std=c++11 -w -g -O0 $(include_paths)
link_flags 	  := $(library_paths) $(ld_librarys) $(run_paths)

# 所有的头文件依赖产生的makefile文件,进行include
ifneq ($(MAKECMDGOALS), clean)
include $(mks)
endif

objs/%.o : src/%.cpp
	@echo 编译$<, 生成$@, 目录是: $(dir $@)
	@mkdir -p $(dir $@)
	@g++ -c $< -o $@ $(compile_flags)

workspace/pro : $(objs)
	@echo 链接$@
	@g++ $^ -o $@ $(link_flags)

objs/%.mk : src/%.cpp
	@echo 生成依赖$@
	@mkdir -p $(dir $@)
	@g++ -MM $< -MF $@ -MT $(@:.mk=.o)

pro : workspace/pro
	@echo 编译完成

run : pro
	@cd workspace && ./pro

clean :
	@ rm -rf workspace/pro objs

.PHONY : pro run debug clean

图示如下

在这里插入图片描述

效果如下

在这里插入图片描述

至此,完整的Makefile工程搞定

8. Makefile工程模板(option)

C++工程模板,参考自heremakefile文件如下,方便自己以后查看

cc        := g++
nvcc      := nvcc
name      := pro
workdir   := workspace
srcdir    := src
objdir    := objs
defined   := PROD=\"sxai\"
stdcpp    := c++11
pwd       := $(abspath .)
cuda_arch := -gencode=arch=compute_75,code=sm_75
run_args  := 

# 导出你的环境变量值,可以在程序中使用,该功能还可以写成例如:
# export LD_LIBRARY_PATH=xxxxx,作用与你在终端中设置是一样的
export workdir srcdir objdir pwd

# 定义cpp的路径查找和依赖项mk文件
cpp_srcs := $(shell find $(srcdir) -name "*.cpp")
cpp_objs := $(cpp_srcs:.cpp=.cpp.o)
cpp_objs := $(cpp_objs:$(srcdir)/%=$(objdir)/%)
cpp_mk   := $(cpp_objs:.cpp.o=.cpp.mk)

# 定义cu文件的路径查找和依赖项mk文件
cu_srcs := $(shell find $(srcdir) -name "*.cu")
cu_objs := $(cu_srcs:.cu=.cu.o)
cu_objs := $(cu_objs:$(srcdir)/%=$(objdir)/%)
cu_mk   := $(cu_objs:.cu.o=.cu.mk)

# 定义opencv和cuda需要用到的库文件
link_opencv    := opencv_core opencv_imgproc opencv_videoio opencv_imgcodecs
link_cuda      := cudart
link_sys       := stdc++ dl
link_librarys  := $(link_opencv) $(link_cuda) $(link_sys)

# 定义cuda和opencv的库路径
lean_cuda      := /data/sxai/lean/cuda-10.2
lean_opencv    := /data/sxai/lean/opencv4.2.0

# 定义头文件路径,请注意斜杠后边不能有空格
# 只需要写路径,不需要写-I
include_paths := \
    src                             \
    $(lean_opencv)/include/opencv4  \
    $(lean_cuda)/include

# 定义库文件路径,只需要写路径,不需要写-L
library_paths := \
    $(lean_opencv)/lib  \
    $(lean_cuda)/lib64

# 把library path给拼接为一个字符串,例如a b c => a:b:c
# 然后使得LD_LIBRARY_PATH=a:b:c
empty := 
library_path_export := $(subst $(empty) $(empty),:,$(library_paths))

# 把库路径和头文件路径拼接起来成一个,批量自动加-I、-L、-l
run_paths     := $(foreach item,$(library_paths),-Wl,-rpath=$(item))
include_paths := $(foreach item,$(include_paths),-I$(item))
library_paths := $(foreach item,$(library_paths),-L$(item))
link_librarys := $(foreach item,$(link_librarys),-l$(item))
defined       := $(foreach item,$(defined),-D$(item))

# 如果是其他显卡,请修改-gencode=arch=compute_75,code=sm_75为对应显卡的能力
# 显卡对应的号码参考这里:https://developer.nvidia.com/zh-cn/cuda-gpus#compute
# 如果是 jetson nano,提示找不到-m64指令,请删掉 -m64选项。不影响结果
cpp_compile_flags := -std=$(stdcpp) -w -g -O0 -m64 -fPIC -fopenmp -pthread $(defined)
cu_compile_flags  := -std=$(stdcpp) -w -g -O0 -m64 $(cuda_arch) -Xcompiler "$(cpp_compile_flags)" $(defined)
link_flags        := -pthread -fopenmp -Wl,-rpath='$$ORIGIN'

cpp_compile_flags += $(include_paths)
cu_compile_flags  += $(include_paths)
link_flags        += $(library_paths) $(link_librarys) $(run_paths)

# 如果头文件修改了,这里的指令可以让他自动编译依赖的cpp或者cu文件
ifneq ($(MAKECMDGOALS), clean)
-include $(cpp_mk) $(cu_mk)
endif

$(name)   : $(workdir)/$(name)

all       : $(name)
run       : $(name)
	@cd $(workdir) && ./$(name) $(run_args)

$(workdir)/$(name) : $(cpp_objs) $(cu_objs)
	@echo Link $@
	@mkdir -p $(dir $@)
	@$(cc) $^ -o $@ $(link_flags)

$(objdir)/%.cpp.o : $(srcdir)/%.cpp
	@echo Compile CXX $<
	@mkdir -p $(dir $@)
	@$(cc) -c $< -o $@ $(cpp_compile_flags)

$(objdir)/%.cu.o : $(srcdir)/%.cu
	@echo Compile CUDA $<
	@mkdir -p $(dir $@)
	@$(nvcc) -c $< -o $@ $(cu_compile_flags)

# 编译cpp依赖项,生成mk文件
$(objdir)/%.cpp.mk : $(srcdir)/%.cpp
	@echo Compile depends C++ $<
	@mkdir -p $(dir $@)
	@$(cc) -M $< -MF $@ -MT $(@:.cpp.mk=.cpp.o) $(cpp_compile_flags)
    
# 编译cu文件的依赖项,生成cumk文件
$(objdir)/%.cu.mk : $(srcdir)/%.cu
	@echo Compile depends CUDA $<
	@mkdir -p $(dir $@)
	@$(nvcc) -M $< -MF $@ -MT $(@:.cu.mk=.cu.o) $(cu_compile_flags)

# 定义清理指令
clean :
	@rm -rf $(objdir) $(workdir)/$(name)

# 防止符号被当做文件
.PHONY : clean run $(name)

# 导出依赖库路径,使得能够运行起来
export LD_LIBRARY_PATH:=$(LD_LIBRARY_PATH):$(library_path_export)

9. 结语

非常👍的一篇文章,循序渐进,值得耐心阅读。

下载链接

参考

转载

转载自从零Makefile落地算法大项目强烈建议读原文!!!,如有侵权,请联系删除

;