makefile快速入门
前言
在linux上开发c/c++代码,基本都会使用make和makefile作为编译工具。我们也可以选择cmake或qmake来代替,不过它们只负责生成makefile,最终用来进行编译的依然是makefile。如果你也是c/c++开发人员,无论你使用什么工具,makefile都是必须掌握的。特别是当你打算编写开源项目的时候,手动编写一个makefile非常重要。本文的目的就是让大家快速了解makefile。
了解makefile
makefile的官方文档[1] 学习makefile的最佳方式就是直接查阅官方说明
一般的makefile文件会包含几个部分:定义变量、目标、依赖、方法段。下面就是一个基础的makefile大概的样子:
1 TARGET=test
2 OBJS=main.o foo.o bar.o
3 CC=gcc
4
5 $(TARGET):$(OBJS)
6 $(CC) $^ -o $@
1-3行定义了变量,第5行冒号前的部分代表目标,表示这部分编译工作的最终目的。冒号后面的部分是目标的依赖,表示要生成这个目标需要哪些预先准备工作。第6行是方法段,代表具体的方法。第5-6行组成了一个编译片段。一个makefile可以包含多个编译片段,方法段也可以有多行。一个编译片段的依赖可以是其他片段的目标,这样当执行make的时候,它就会根据依赖关系处理执行次序。一个makefile文件不能出现重名的目标名,且当你执行make的时候,它会默认执行第一条编译片段,如果第一条编译片段并没有其他依赖,make不会继续向下执行(这一点很重要,后面会有说明)。
除此以外,makefile还可以通过include的方式包含其它makefile文件,因此我们也可以将公共的部分写到一起。在makefile里,我们也可以编写或调用shell脚本。
常见变量和函数介绍
作为学习前的准备,我们先介绍几个常见的概念:
1. 关于makefile的命名
你可以使用全小写或首字母大写的方式来命名,或者你也可以起任何你喜欢的名字,通过make -f的方式来运行。不过我强烈建议你使用makefile或Makefile,并且在所有的项目中保持统一。
2. 声明变量和使用变量
makefile中声明变量的方式是=或:=,使用:=的方式主要是为了处理循环依赖,这个规则可以参考shell脚本。使用变量的方式是$()。除了我们自定义的变量以外,makefile也有预定义的变量。常见的有:
(1) CC: C编译器的名称,默认是cc。通常如果我们是c++程序会改写它
(2) CXX: c++编译器的名称,默认是g++
(3) RM: 删除程序,默认值为rm -f
(4) CFLAGS: c编译器的选项,无默认值
(5) CXXFLAGS: c++编译器的选项,无默认值
(6) $*: 不包含扩展名的目标文件名称
(7) $+: 所有的依赖文件,以空格分开,并以出现的先后顺序,可能包含重复的依赖文件
(8) $<: 第一个依赖文件的名称
(9) $@: 目标文件的完整名称
(10) $^: 所有不重复的依赖文件,以空格分开
(11) MAKE: 就是make命令本身
(12) CURDIR: makefile的当前路径
3. 常见函数方法介绍
函数调用是makefile的一大特点,调用的共同方式是将函数名以及入参放在$()中,函数名和参数之间以[空格]分开,参数之间用[逗号]分开。除了makefile预定义的函数以外,我们还可以编写自己的函数,函数内部使用$(数字)的方式使用参数。
1 define <Funcname>
2 echo $(1)
3 echo $(2)
4 endef
(1) call: 自定函数的调用方式,第一个入参是函数名,后面是函数入参
(2) wildcard: 通配符函数,表示通配某路径下的所有文件,通常我们是将所有*.cpp或*.h文件选择出来单独处理
(3) patsubst: 替换函数,经常和wildcard联合使用,例如将*.cpp全部替换成*.o,后文有详细的使用方法
(4) foreach: 循环函数,会根据空格将字符串分片处理,我们可以用来处理多个目标的编译或多个文件路径的扫描
(5) notdir: 获取到路径的最后一段文件名
(6) strip: 去掉字符串前后的空格
(7) shell: 用于在makefile中执行shell脚本
4. 条件分支
makefile也可以根据条件,选择不同的处理分支。方式如下:
ifeq ()
else
endif
或者
ifndef
else
endif
条件分支在我的日常开发中不建议使用,因为很容易让makefile变得晦涩难读。毕竟是做编译用的工具,为了方便维护还是不要弄的太复杂。
5. 关于伪目标
A phony target is one that is not really the name of a file; rather it is just a name for a recipe to be executed when you make an explicit request. There are two reasons to use a phony target: to avoid a conflict with a file of the same name, and to improve performance.
对于伪目标官方提供的解释是这样的: 伪目标不是一个真实存在的文件名,它只表示了一个编译的目标。使用伪目标的意义在于:1,避免makefile中的命名重复;2,提高性能。最常用的伪目标就是clean,为了确保我们声明的目标在makefile路径下不会重现同名的文件。伪目标的编写如下:
clean:
$(RM) $(OBJS) $(TARGET) .PHONY:clean
多目录编译和动态库
通常只要我们开发的不是一个demo程序,一个项目都会包含自己的目录结构,某些项目还包含自己的动态库需要在编译时导出。对于多目录的编译,网上的方法很多,这里我只介绍一个我个人比较推荐的方式。所有目录下的源码都在主makefile中编译,如果是动态库目录则单独在动态库所在的目录下编写一个makefile,然后让主目录中的makefile来调用。和编译可执行程序不同,编译动态库有以下三个注意点:
1. LDLIBS=-shard: 告诉编译器,需要生成共享库
2. CXXFLAGS=-fPIC: 这个是C++的编译选项,在将.cpp生成.o文件的时候,由于通常我们使用自动推导,因此我们需要用这个变量指明编译要生成与为位置无关的代码,否则在连接环节会报错
3. 编译目标需要以lib开头.so结尾
一个完整的例子
下面以一个相对完整的例子作为总结,在这个例子中有对源码的编译,也有对动态库的编译和导出,还包含了安装环节。为了方便项目管理,我使用的项目结构如下:
项目
|
-- bin # 可执行程序的所在目录
|
-- include # 内部和外部头文件的所在目录。开发初期,这里只会保存外部依赖的头文件,项目内部的头文件是在编译后自动复制进去的,目的是方便在安装换环节统一处理
|
-- lib # 动态库所在目录。和include一样,开发初期只包含依赖的动态库,项目内部的动态库是在编译后复制进去的
|
-- src # 源码目录
项目源码如下,你可以直接复制并根据文件头部注释中的路径来生成
./foo/foo.h 和 ./foo/foo.cpp

// ./foo/foo.h
#ifndef FOO_H_
#define FOO_H_ class Foo
{
public:
explicit Foo();
}; #endif
foo.h

#include "foo.h"
#include <iostream> using namespace std; Foo::Foo()
{
cout << "Create Foo" << endl;
}
foo.cpp
./xthread/xthread.h和./xthread/xthread.cpp

// ./xthread/xthread.h
#ifndef XTHREAD_H
#define XTHREAD_H #include <thread>
class XThread
{
public:
virtual void Start();
virtual void Wait(); private:
virtual void Main() = 0;
std::thread th_;
}; #endif
xthread.h

#include "xthread.h"
#include <iostream> using namespace std; void XThread::Start()
{
cout << "Start XThread" << endl;
th_ = std::thread(&XThread::Main, this);
} void XThread::Wait()
{
cout << "Wait XThread Start..." << endl;
th_.join();
cout << "Wait XThread End..." << endl;
}
xthread.cpp
./main.cpp

// ./main.cpp
#include <iostream>
#include "foo/foo.h"
#include "xthread.h" using namespace std; class XTask : public XThread
{
public:
void Main() override
{
cout << "XTask main start..." << endl;
this_thread::sleep_for(chrono::seconds(3));
cout << "XTask main end..." << endl;
}
}; int main(int argc, char *argv[])
{
cout << "hello" << endl;
Foo foo;
XTask task;
task.Start();
task.Wait();
return 0;
}
main.cpp
main和foo只进行源码编译,xthread是动态库。在编译顺序上,需要先编译xthread并将头文件和动态库文件分别导出到include和lib下,再编译源码。最后执行make install,将所有动态库拷贝至/usr/lib目录,可执行文件拷贝至/usr/bin目录。如果你的动态库还需要给其它项目使用,你还需要将它的头文件拷贝到/usr/include目录下。
根据上面介绍的方法,我们首先编写xthread所在的makefile:
# ./xthread/makefile
TARGET=libxthread.so LDLIBS:=-shared
CXXFLAGS:=-std=c++11 -fPIC SRCS:=$(wildcard *.cpp)
HEADS:=$(wildcard *.h)
OBJS:=$(patsubst %.cpp,%.o,$(SRCS)) $(TARGET):$(OBJS)
$(CXX) $(LDFLAGS) $^ -o $@ $(LDLIBS) install:$(TARGET)
cp $(TARGET) ../../lib
cp $(HEADS) ../../include clean:
$(RM) $(OBJS) $(TARGET) .PHONY:clean install
这一步完成以后,makefile可以单独执行。执行make install会先执行$(TARGET)所在的编译片段。
编写主目录下的makefile,并可以通过主目录下的makefile控制xthread的编译执行:
# ./makefile
TARGET=hello
SRC_PATH=$(CURDIR) $(CURDIR)/foo
SRCS=$(foreach dir,$(SRC_PATH),$(wildcard $(dir)/*.cpp))
OBJS=$(patsubst %.cpp,%.o,$(SRCS))
CXXFLAGS=-std=c++11 -I../include
LDFLAGS=-L../lib
LDLIBS=-lpthread -lxthread
CC=$(CXX)
INSTALL_DIR=/usr $(TARGET):$(OBJS) depends
$(CC) $(LDFLAGS) $(OBJS) -o $@ $(LDLIBS)
@cp $(TARGET) ../bin depends:
$(MAKE) install -C $(CURDIR)/xthread -f makefile install:$(TARGET)
cp ../bin/$(TARGET) $(INSTALL_DIR)/bin
cp ../lib/*.so $(INSTALL_DIR)/lib clean:
$(RM) $(OBJS) $(TARGET)
$(MAKE) clean -C $(CURDIR)/xthread .PHONY: clean install depends
主目录的$(TARGET)有一个depends,属于伪目标,会被预先执行。CXXFLAGS表明了编译需要的外部头文件的搜索目录,LDFLAGS表明了外部依赖库的搜索目录,LDLIBS说明编译过程具体需要哪些动态库。并且会将编译的可执行文件复制到../bin目录下。
其它的细节,建议读者跟着做一遍应该可以掌握。
makefile快速入门的更多相关文章
- Makefile 快速入门
Makefile 速成 标签: Makefile编译器 2015-06-06 18:07 2396人阅读 评论(1) 收藏 举报 分类: C/C++(132) Linux & MAC(19 ...
- Make 和 Makefile快速入门
前言 一个项目,拥有成百上千的源程序文件,编译链接这些源文件都是有规则的.Makefile是整个工程的编译规则集合,只需要一个make命令,就可以实现“自动化编译”.make是一个解释makefile ...
- Linux快速入门04-扩展知识
这部分是快速学习的最后一部分知识,其中最重要的内容就是源码的打包和软件的安装的学习,由于个人的Linux学习目的就是自己能在阿里云Ubuntu上搭建一个简单的nodejs发布环境. Linux系列文章 ...
- CMake快速入门教程-实战
http://www.ibm.com/developerworks/cn/linux/l-cn-cmake/ http://blog.csdn.net/dbzhang800/article/detai ...
- 转:CMake快速入门教程-实战
CMake快速入门教程:实战 收藏人:londonKu 2012-05-07 | 阅:10128 转:34 | 来源 | 分享 0. 前言一个多月 ...
- Emacs快速入门
Emacs 快速入门 Emacs 启动: 直接打emacs, 如果有X-windows就会开视窗. 如果不想用X 的版本, 就用 emacs -nw (No windows)起动. 符号说明 C-X ...
- QuickJS 快速入门 (QuickJS QuickStart)
1. QuickJS 快速入门 (QuickJS QuickStart) 1. QuickJS 快速入门 (QuickJS QuickStart) 1.1. 简介 1.2. 安装 1.3. 简单使用 ...
- NOI Linux 快速入门指南
目录 关于安装 NOI Linux 系统配置 网络 输入法 编辑器 1. gedit 打开 配置 外观展示 2. vim 打开 配置 使用 makefile 编译运行 1. 编写 makefile 2 ...
- Web Api 入门实战 (快速入门+工具使用+不依赖IIS)
平台之大势何人能挡? 带着你的Net飞奔吧!:http://www.cnblogs.com/dunitian/p/4822808.html 屁话我也就不多说了,什么简介的也省了,直接简单概括+demo ...
随机推荐
- fastapi(一)
废话不多说,直接上代码. 目录结构, 由于我也是刚开始学这个框架,只是了解了怎么注册蓝图,JWT的集成,数据库的集成,想了解更多,自行打开官方文档去详细阅读.fastapi官网文档链接 创建一个mai ...
- [opencv]使用g++编译opencv程序演示
gcc/g++ 编译命令 1. gcc -E source_file.c -E,只执行到预编译.直接输出预编译结果. 2. gcc -S source_file.c -S,只执行到源代码到汇编代码的 ...
- 每天学一点——python注释规范
python注释规范 python注释语法 这个是注释 注释是不影响代码运行的 当然注释也是有书写规范的,就像图片中的 注释前面#加空格再加上这条代码的注释(单行注释用#) 不然你会得到下面的结果 * ...
- html基础 有语义的布局标签
- RabbitMQ 消息中间件 的下载与安装【window10】
1.前言 弄了好久,才终于把 rabbitmq装好 ,本来是很简单的,但是,安装有个要求就是路径不能有中文字符, 虽然可以安装,但是无法运行,需要修改路径名为非中文字符后重装rabbitmq才可以运行 ...
- react将HTML字符串解析为HTML标签
当后台返回的数据是字符串html的话,我们可以利用dangerouslySetInnerHTML属性来把字符串转换成html标签 function showhtml(htmlString){ var ...
- 第10组 Alpha冲刺 (3/6)
1.1基本情况 ·队名:今晚不睡觉 ·组长博客:https://www.cnblogs.com/cpandbb/p/13971668.html ·作业博客:https://edu.cnblogs.co ...
- Go语言系列之网络编程
现在我们几乎每天都在使用互联网,我们前面已经学习了如何编写Go语言程序,但是如何才能让我们的程序通过网络互相通信呢?本章我们就一起来学习下Go语言中的网络编程. 关于网络编程其实是一个很庞大的领域,本 ...
- vue项目配置及代理解决跨域
axios数据请求 1.下载模块:npm install axios 2.axios特点: 1.支持在浏览器当中发起XMLHttpRequest请求 2.支持Promise 3.自动转换json数据 ...
- Keepalived高可用、四层负载均衡
目录 Keepalived高可用 高可用简介 常用的工具 问题 名称解释 VRRP协议 部署keepalived 下载安装 Keepalived配置 保证nginx配置一样 解决keepalived的 ...