目录
前言
转自:从零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.cpp
和test.cpp
具体内容如下
2.3.2 main.cpp的汇编代码
指令g++ -S main.cpp -o main.s
生成main.s
,main.cpp
的汇编代码如下
2.3.3 test.cpp的汇编代码
指令g++ -S test.cpp -o test.s
生成test.s
,test.cpp
的汇编代码如下
2.3.4 两者汇编代码对比
main.s
和test.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.o
、lib3rd.so
、libpkg.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基础-解决的问题是什么
- 使用GCC的命令行编译代码在单个文件下较方便,当工程文件逐渐增多时,使用GCC命令编译就会变得力不从心。此时我们需要借助项目构造工具make帮助我们完成编译工作
- GNU make是一个命令工具,是一个解释makefile中指令的命令工具,一边来说大多数的IDE都有这个命令
- make工具在构建项目时需要加载一个叫做makefile的文件,makefile关系到了整个工程的编译规则。一个工程中的源文件不计其数,其类型、功能、模板分别放在若干个目录中,Makefile定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。
- Makefile为我们带来了极大地好处——“自动化编译”,一旦写好,只需要一个make命令,整个工程就完全自动编译,极大的提升了软件开发的效率。
make
指令执行时,默认会查找当前目录下的makefile或Makefile作为代码文件,当然也可以make -f abc
的方式指定make运行的Makefile代码,因此在一个项目中可以有多个makefile文件,分别位于不同的目录中。- Makefile主要解决的问题是描述生成依赖关系,根据生成和依赖文件修改时间新旧决定是否执行command。虽然可手动调用g++编译,但如果每次编译时都是全体代码参与,对于没有修改部分的代码进行编译是浪费时间。而项目文件越多,这个问题越严重。Makefile可以帮我们解决这个问题
- Makefile的重点有:描述依赖关系、command(生成文件的指令)
- 学习Makefile的基础操作足以应付项目需求即可,并不需要学习全部语法
- 参考官方文档,可查看更多定义GNU make
- 参考自爱编程的大丙的Makefile
3.2 Makefile基础-代码域
Makefile包含变量定义域、依赖定义域以及command域,如下所示。当执行make main.o
时,会检查main.o
和main.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.cpp
、b.cpp
,自动定义a.o
依赖a.cpp
等
3.3.4 总结
变量赋值有4种方式
var = 123
,var := 123
,var ?= 123
,var += 123
。其中var := 123
和var += 123
常用取变量值有2种方式
$(var)
,${var}
。小括号大括号都可以数据类型只有字符串和字符串数组,空格隔开表示多个元素
$(function arguments)
是调用make内置函数的方法,具体可以参考官方文档的函数大全。但常用的其实只有少数两个即可依赖关系定义种,如果代码修改时间比生成的更新或生成不存在时,command会执行
依赖关系可以链式的定义,即b依赖a,c依赖b,而make会自动链式的查找并根据时间执行command
command是
shell
指令,可以使用$(var)来将变量用到其中。前面加@表示执行时不打印原指令内容。否则默认打印指令后再执行指令make不写具体生成名称,则会选择依赖关系中的第一项生成
4. 基于Makefile的标准工程结构
4.1 Makefile工程结构
一个标准工程做如下定义:
- 具有src目录,存放代码,可能有多级,例如
main.cpp
,foo/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++的调试功能
一、相关疑问
.vscode
文件夹是什么?.vscode是一个隐藏文件夹,其主要作用是以.json的方式保存相关配置信息,如编译任务的配置、调试任务的配置、工作环境的配置等等。
c_cpp_properties.json
是什么?c_cpp_properties.json是C/C++插件UI界面的json形式,其主要作用是配置C/C++插件代码的智能提示,方便代码的书写
task.json
是什么?task.json是用来配置编译文件的信息,用于生成可执行文件
launch.json
是什么?launch.json是用来配置调试文件的信息,比如指定调试语言环境,指定调试类型等等。
二、生成上述文件操作
c_cpp_properties.json
ctrl+shift+p
显示命令行面板,键入C/C++:Edit Configurations(JSON)
,选择即可
tasks.json
终端->配置任务->使用模板创建tasks.json文件->Other
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.hpp
和a.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++工程模板,参考自here,
makefile
文件如下,方便自己以后查看
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. 结语
非常👍的一篇文章,循序渐进,值得耐心阅读。
下载链接
参考
- gcc和g++是什么关系?
- GNU make官方文档
- 爱编程的大丙的Makefile
- make官方文档的函数大全
- VScode创建json文件
- 配置VScode的launch.json文件
- vscode做C++开发
- shouxieai的makefile-tutorial
转载
转载自从零Makefile落地算法大项目,强烈建议读原文!!!,如有侵权,请联系删除