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 ...
随机推荐
- LeetCode1239串联字符串的最大长度
题目 给定一个字符串数组 arr,字符串 s 是将 arr 某一子序列字符串连接所得的字符串,如果 s 中的每一个字符都只出现过一次,那么它就是一个可行解. 请返回所有可行解 s 中最长长度. 解题 ...
- 1434 区间LCM
1434 区间LCM 基准时间限制:1 秒 空间限制:131072 KB 一个整数序列S的LCM(最小公倍数)是指最小的正整数X使得它是序列S中所有元素的倍数,那么LCM(S)=X. 例如,LCM(2 ...
- 【算法】01-数据结构概述(注意区分jvm堆与堆/jvm栈与栈)
[算法]01-数据结构概述(注意区分jvm堆与堆/jvm栈与栈) 欢迎关注b站账号/公众号[六边形战士夏宁],一个要把各项指标拉满的男人.该文章已在github目录收录. 屏幕前的大帅比和大漂亮如果有 ...
- 基于Spring MVC + Spring + MyBatis的【物流系统 - 公司信息管理】
资源下载:https://download.csdn.net/download/weixin_44893902/45601768 练习点设计:模糊查询.删除.新增 一.语言和环境 实现语言:JAVA语 ...
- 【操作系统】I/O多路复用 select poll epoll
@ 目录 I/O模式 I/O多路复用 select poll epoll 事件触发模式 I/O模式 阻塞I/O 非阻塞I/O I/O多路复用 信号驱动I/O 异步I/O I/O多路复用 I/O 多路复 ...
- linux修改配置文件关闭终端失效问题
当前shell环境为 交互式login-shell(非图形化界面环境) /etc/profile /etc/bash.bashrc ~/.profile ~/bashrc 当前环境为 交互式非logi ...
- Echart可视化学习(一)
文档的源代码地址,需要的下载就可以了(访问密码:7567) https://url56.ctfile.com/f/34653256-527823386-04154f 正文: 创建需要的目录结构及文件 ...
- nuxt中iview按需加载配置
在plugins/iview.js中修改 初始代码如下 import Vue from 'vue' import iView from 'iview' import locale from 'ivie ...
- js实现工具函数中groupBy数据分组
数据 this.tableData = [ {id: 1, name: '测试', number: 1, price: 0}, {id: 2, name: '测试', number: 1, price ...
- 【Spring专场】「AOP容器」不看源码就带你认识核心流程以及运作原理
前提回顾 前一篇文章主要介绍了spring核心特性机制的IOC容器机制和核心运作原理,接下来我们去介绍另外一个较为核心的功能,那就是AOP容器机制,主要负责承接前一篇代理模式机制中动态代理:JDKPr ...