From:从零Makefile落地算法大项目,完整案例教程 — 杜佬牛逼🐂🍺
为什么要学makefile:
- 相比cmake,Makefile更加轻量简洁,侵入性低,掌控力强,语法更少更简单;
- 使用Makefile你可以进行更细粒度的掌控,虽然cmake简化了这些,但是对于大型项目,细节的掌控是必须的;
- 对于算法落地,我们会面临各种库包,理清楚非常有利于降低问题的发生;
准备环境
- VSCode (Visual Studio Code) 作为IDE,(VSCODE YYDS)
- 安装VSCode的C++插件
- 准备Linux系统(Ubuntu),推荐使用VSCode的SSH插件远程连接服务器(另一个电脑)进行开发。本地电脑可以是windows/mac
- 熟悉C++的基本语法,我们主讲Makefile但是会有C++编码部分(不多)
目录
1. g++指令介绍
1.1 g++/gcc是什么,有什么区别
g++
和gcc
都是gnu
推出的cpp
编译器,时代不同g++
和gcc
都可以进行cpp
编译g++
和gcc
一样,都属于driver
,即驱动编译,他会调用cclplus/ccl/ld/as
等指令实现编译链接等工作,他们俩只是默认选项上的处理不同- 这里我们采用
g++
而不是gcc
g++
等价于gcc -xc++ -lstdc++ -shared-libgcc
参考:https://www.zhihu.com/question/20940822
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
|| | —- | |
| |
| |
|
2. C++编译链接 / 编译时和运行时
2.1 C++编译链接流程图
2.2 C++的声明和实现
:::info 声明代表一个函数的符号或者ID,实现则代表函数的具体执行的代码 :::
int add(int, int);
int add(int a, int b);
声明不关心参数名称是什么,也不关心返回值是什么,也就是说int add(int a, int b)
和int add(int, int)
是一样的。
int add(int a, int b)
{
return a + b;
}
2.3 C++的编译过程-案例
2.3.1 代码结构,main.cpp和test.cpp
在main.cpp中有个函数声明:int add(int a, int b);
,具体实现在test.cpp中。
2.3.2 main.cpp 和 test.cpp 的汇编代码
从两者的汇编代码,可以看到
main.s
里面没有add
函数的具体实现,只有call _Z3addii
操作;add
的具体实现在test.s
里面
2.3.3 带有命名空间时的名字编码
2.4 C++编译过程
关于编码的结论是:
- c++中的函数、符号、变量会被编码
- 函数的编码关心的是:函数名称、所在命名空间、参数类型
- 函数的编码不关心的是:返回值类型
关于编译的结论是:
调用一个函数,会生成call 函数名称的代码,没有函数的具体实现。具体实现是在链接阶段。
2.5 C++链接过程
2.6 C++实际的链接过程
:::info
任何链接的符号(例如
**add**
),会在所有链接的对象中查找实现,例如这里在 **test.o**
、**lib3rd.so**
、**libpkg.a**
中找
如果没找到,则**undefined reference _Z3addii**
;
如果找到多个,则**multiple definition of add**
;
任何符号的实现全局只能有一个。
:::
2.7 若add函数在动态库lib3rd.so
中时
:::info
如果链接的符号在
**so**
中,**out.bin**
会引用这个符号的**so**
文件,在运行时动态加载**lib3rd.so**
后,再调用**add**
函数。
:::
2.8 若add函数在静态库,libpkg.a中时
:::info
如果链接的符号在
**a**
中,out.bin会将add的实现代码完整复制到**out.bin**
中,在运行**out.bin**
时,不需要**libpkg.a**
。
:::
2.9 编译链接成一个完整程序的过程
2.10 C++链接时,查找so文件、a文件方式的方式
g++ -lpkg -l3rd main.o -o out.bin
当程序链接时,如何决定链接的是哪个**so**
、**a**
文件呢?
是按照如下依据来的:
g++ -lname
:表示链接一个动态库或者静态库(.so
or.a
),名字是libname.so/libname.a
g++ -Lfolder
:表示配置一个动态/静态库的查找目录g++
查找so/a
文件的地方有3个,按照下列优先顺序查找第一顺序:应用程序的当前目录(当前目录不同于程序所在目录)
- 第二顺序:
out.bin
中储存的rpath
(run path)。readelf -d out.bin
指令可以查看文件的runpath
信息。如果该选项指定了依旧失效,说明依赖的so文件还存在更多依赖,,却没有在其他目录明确(常用) - 第三顺序:环境变量指定的目录(例如LD_LIBRARY_PATH)
2.12 C++编译时,头文件的查找方式
g++ -Ifolder
:这里是小写的L,不是大写的I,**-lfolder**
当程序编译时,头文件该如何查找呢:g++ -Ifolder
:表示配置一个头文件查找目录
当**#include "path/name.hpp"**
使用双引号时:
编译器会在当前文件的目录下查找path/name.hpp
,例如我们在/data/a.cpp
中写了#include "path/name.hpp
,最终确认的路径是/data/path/name.hpp
当#include
- 第一顺序:以
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基础-解决的问题是什么
编译代码是一个很耗时的事情尤其是代码量大、CPU差时(边缘端)。参考官方文档,查看更多定义:http://www.gnu.org/software/make/manual/make.html
make
指令执行时,默认会查找当前目录下的makefile
、Makefile
文件作为代码文件,当然也可以make -f abc
的方式,指定make
运行的Makefile
代码Makefile
主要解决的问题是,描述生成依赖关系,根据生成和依赖文件修改时间新旧决定是否执行command。可手动调用g++
进行编译,重点是,如果每次编译,都是全体代码参与,对于没有修改部分的代码编译,是浪费时间。项目文件越多,这个问题越严重。而Makefile帮我们解决这个问题- Makefile的重点有:描述依赖关系、command(生成文件的指令)
我们只需要学习Makefile的基本操作足以应付项目需求即可,并不需要学习全部语法
3.2 Makefile基础-代码域
3.3 Makefile基础-语法
数据类型,字符串和字符串数组
- 定义变量,
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}
,在这个位置解释为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
|
| | —- | —- |
- 例如:
生成项可以没有依赖项,那么如果该生成项文件不存在,command将永远执行
3.2 依赖关系定义
第一次执行
make a.o
时,由于a.o
不存在,执行了command
- 第二次执行
make a.o
时,由于a.cpp
时间没有比a.o
新,打印a.o is up to date
,不需要编译 -
3.3 编译和链接结合起来
定义好依赖后,
make out.bin
,会自动查找依赖关系,并自动按照顺序执行command
- 这是
makefile
为我们解决的核心问题,剩下就是如何玩的更方便罢了。比如自动检索a.cpp
、b.cpp
,自动定义a.o
依赖a.cpp
。等等3.4 总结
- 变量赋值有4种方式
var = 123
,var := 123
,var ?= 123
,var += 123
。其中var := 123
常用,var += 123
常用 - 取变量值有两种,
$(var)
or${var}
。小括号大括号均可以 - 数据类型只有字符串和字符串数组,空格隔开表示多个元素
$(function arguments)
是调用make
内置函数的方法,具体可以参考官方文档的函数大全。但是常用的其实只有少数两个即可- 依赖关系定义中,如果代码修改时间比生成的更新/生成不存在时,
command
会执行。否则只会打印main.o is up to date
。这是makefile
解决的核心问题 - 依赖关系可以链式的定义,即b依赖a,c依赖b,而
make
会自动链式的查找并根据时间执行command
command
是shell
指令,可以使用$(var)
来将变量用到其中。前面加@
表示执行执行时不打印原指令内容。否则默认打印指令后再执行指令-
3.5 未解决的问题
通常,我们的cpp会比较多,总不会每次都写全
a.o: a.cpp
吧?- 通常,我们的cpp会多级目录,总不会每个目录都写
Makefile
吧? - 我不希望生成的
a.o
和a.cpp
在一起,我想统一放到其他位置可以吗?4. 基于Makefile的标准工程结构
4.1 Makefile工程结构
一个标准工程,我们做如下定义:
这里简单定义了
foo.hpp
和foo.cpp
,目的是链接为可执行程序后,可以执行-
4.3 解决多级目录cpp检索问题
4.4 替换src/为objs/,o文件放到objs中
srcs is[src/download.cpp src/main.cpp] objs is[src/download.o src/main.o] objs is[objs/download.o objs/main.o]
4.5 定义依赖关系,通配
objs/%.o
和src/%.cpp
代表了通配依赖关系,模式匹配,%相当于变量部分4.6 为o文件创建目录
4.6.1 编译失败,因为目录不存在
原因是,试图创建
objs/foo/foo.o
文件时失败。因为objs/foo
这个目录不存在造成。对于高版本g++(例如9.0)不会报错并为你创建objs/foo目录。因此我们需要创建
objs/foo
目录,需要执行类似mkdir -p dir($@)
,通过dir($@)
获取其目录后创建,这里的mkdir -p
指多级目录也一并创建。4.6.2 使用
mkdir -p $(dir $@)
获取生成项目录4.7 链接所有o文件生成可执行程序
我们定义
workspace/pro
的生成,依赖自所有的o
文件。pro
是我们的可执行程序4.8 完善一下Makefile
|
|
- 添加make pro
,简洁的编译程序
- 添加make run
,编译后顺便执行一下,注意: cd到workspace是为了让运行程序后的当前工作目录在workspace中
- 添加make clean
,清理编译后的垃圾
- 添加.PHONY
,让我们作为指令存在的东西,不要被视作为文件。即make这东西时永远执行command
| | —- | —- |
4.9 完整版本的Makefile
修改一个cpp后,重新编译连接,得到不同的结果,这就是我们想要的。
5. 基于Makefile实现的完整功能项目
5.1 Makefile工程-一个复杂的例子,实现http请求
实现的目的:
- 具有两个依赖,openssl、libcurl,存在include、libs依赖
- 可以锻炼一个完整的相对完善的工程案例。还可以锻炼到代码调试
实现的效果:
- 实现一个程序,可以从任何网站上下载东西
准备:
- 下载openssl:https://www.openssl.org/source/old/1.1.1/openssl-1.1.1j.tar.gz
- 这是用于实现加密通信的加密算法库。用于访问https开头的链接
创建build目录,用于储存下载后的文件,准备用来编译
- 创建lean目录,用于存放编译后的结果,作为依赖项目录
-
5.3 编译openssl
cd openssl-1.1.1j ./config --prefix=/media/darrenzhang/Workdirs/trt-pro/makefile_tutorial_project/lean/openssl-1.1.1j make all -j16 && make install -j16 // 这里-j16是同时16个线程执行操作
./config
是配置并生成Makefile,指定install到/media/darrenzhang/Workdirs/trt-pro/makefile_tutorial_project/lean/openssl-1.1.1j
目录- 请把这里的lean目录修改为你当前自己想放的位置
make all -j16 && make install -j16
这里-j16是同时16个线程执行操作,编译后,执行安装5.4 编译libcurl
cd /media/darrenzhang/Workdirs/trt-pro/makefile_tutorial_project/build/curl-7.78.0 ./configure --prefix=/media/darrenzhang/Workdirs/trt-pro/makefile_tutorial_project/lean/curl7.78.0 \ --with-openssl=/media/darrenzhang/Workdirs/trt-pro/makefile_tutorial_project/lean/openssl-1.1.1j make all -j16 && make install -j16
--prefix
同样是为了设置安装目录,最后编译好的curl放在哪里--with-openssl
指定刚才我们编译安装后的目录./configure
同样是为了配置curl生成Makefile文件- 执行
make all -j16
实现编译 - 执行
make install -j16
实现安装5.5 编译后结果
至此我们得到头文件库文件5.6 配置IntellSense和browse路径
变量${workspaceFolder}
代表了我们的当前目录,即/media/darrenzhang/Workdirs/trt-pro/makefile_tutorial_project
。5.7 配置Makefile
至此整个makefile已经非常完备了。该makefile可以通用了5.8 coding
将代码放在./src/
下 ```cppinclude “download.hpp”
include
include
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, message: %s\n", code, curl_easy_strerror(code));
}
curl_easy_cleanup(curl);
return response;
}
```cpp
#ifndef DOWNLOAD_HPP
#define DOWNLOAD_HPP
#include <string>
std::string download(const std::string &url);
#endif // DOWNLOAD_HPP
#include <stdio.h>
#include <fstream>
#include "download.hpp"
using namespace std;
int main()
{
// 下载一个图片文件
string sxai = download(
"https://link.zhihu.com/?target=http%3A//www.zifuture.com%3A1556/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;
}
文件下载成功,至此。整个http的访问工程就达成了。你学会如何控制,头文件、库文件路径了吗?还有o文件存放工作目录等。
6. 分析程序依赖项
6.1 使用readelf -d workspace/pro
分析
6.2 使用ldd workspace/pro
分析
ldd
指令可以查看该程序依赖的so
,查找到的具体路径。看看是否符合预期。
因为如果不符合预期,会发生莫名其妙的错误,这个千万要不得。我们一定要让他符合我们的预期。
7. 配置C++的调试功能
7.1 配置task.json
task.json是配置用来执行调试之前的编译工作。即,按下F5,编译程序,进入调试
7.2 配置launch.json
这个文件可用通过直接按下F5后自动产生,也可以手动敲哈
- 如果有参数,可以加到args中
-
7.3 进行调试
好了,我们在main.cpp的29行这个文字左侧点击后后个红点,作为断点,然后按下F5键,看看会怎么样
7.4 界面介绍
7.5 恭喜
- 到这里,恭喜你,已经掌握了如何使用
- Makefile在linux下开发的技能了! Congratulations!!!
8. 头文件修改后自动编译
关于最后一个扩展知识点考虑:如果头文件修改了,那么依赖头文件的cpp会发生编译吗?如果不发生编译,会带来什么后果?
如果头文件修改,依赖头文件的cpp没有发生编译,你可能会面临莫名的错误,当然这种事情并不常发生。
解决方案:如果头文件修改,make clean
后再编译即可。但是这并不是好的解决方案。
- 我们有如下代码。头文件a.hpp中定义了Number 888
make run后打印的888
![]() ![]() |
修改成555 后继续make run 发现并没有编译a.cpp 。因此输出依旧是888 |
---|---|
8.2 分析原因
- 原因:缺少
a.o
对hpp
依赖关系的定义。makefile
中没有定义a.o : a.hpp
,没有要求编译a.cpp
需要检查a.hpp
的时间 - 解决方案:直接增加
a.o : a.cpp a.hpp
吗?是可以。强制要求a.o
生成时检查a.hpp
8.3 头文件依赖问题 - 解决方案
问题:如果增加a.o : a.hpp
。但是a.hpp
来自于a.cpp
的#include
语法。我们可能随时修改a.cpp
依赖不同的hpp
文件,如何自动完成?
使用**g++ -MM a.cpp -MF a.mk -MT prefix/a.o**
生成**makefile**
文件**a.mk**
该指令产生a.mk
文件,里面写了a.cpp
依赖的头文件。a.mk
本身就是makefile
语法。他解析了a.cpp
的全部头文件后分析。所有依赖的头文件,并写成makefile
语法
-MM
分析 #include "a.hpp"
,引号类型的包含-M
分析全部包含,尖括号和引号-MF
指定储存路径-MT
指定依赖项名称
通过include a.mk包含生成的文件,使其生效
- 我们使用
g++ -MM a.cpp -MF a.mk -MT a.o
- 为了使编译后的
a.mk
生效,我们可以通过include a.mk
包含进来
整合起来,注意,这里include a.mk
修改为-include a.mk
就不会提示报错了。
集成到HTTP项目中
把代码拆分出头文件用于检验效果