前言

  在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快速入门的更多相关文章

  1. Makefile 快速入门

    Makefile 速成 标签: Makefile编译器 2015-06-06 18:07 2396人阅读 评论(1) 收藏 举报  分类: C/C++(132)  Linux & MAC(19 ...

  2. Make 和 Makefile快速入门

    前言 一个项目,拥有成百上千的源程序文件,编译链接这些源文件都是有规则的.Makefile是整个工程的编译规则集合,只需要一个make命令,就可以实现“自动化编译”.make是一个解释makefile ...

  3. Linux快速入门04-扩展知识

    这部分是快速学习的最后一部分知识,其中最重要的内容就是源码的打包和软件的安装的学习,由于个人的Linux学习目的就是自己能在阿里云Ubuntu上搭建一个简单的nodejs发布环境. Linux系列文章 ...

  4. CMake快速入门教程-实战

    http://www.ibm.com/developerworks/cn/linux/l-cn-cmake/ http://blog.csdn.net/dbzhang800/article/detai ...

  5. 转:CMake快速入门教程-实战

    CMake快速入门教程:实战 收藏人:londonKu     2012-05-07 | 阅:10128  转:34    |   来源   |  分享               0. 前言一个多月 ...

  6. Emacs快速入门

    Emacs 快速入门 Emacs 启动: 直接打emacs, 如果有X-windows就会开视窗. 如果不想用X 的版本, 就用 emacs -nw (No windows)起动. 符号说明 C-X ...

  7. QuickJS 快速入门 (QuickJS QuickStart)

    1. QuickJS 快速入门 (QuickJS QuickStart) 1. QuickJS 快速入门 (QuickJS QuickStart) 1.1. 简介 1.2. 安装 1.3. 简单使用 ...

  8. NOI Linux 快速入门指南

    目录 关于安装 NOI Linux 系统配置 网络 输入法 编辑器 1. gedit 打开 配置 外观展示 2. vim 打开 配置 使用 makefile 编译运行 1. 编写 makefile 2 ...

  9. Web Api 入门实战 (快速入门+工具使用+不依赖IIS)

    平台之大势何人能挡? 带着你的Net飞奔吧!:http://www.cnblogs.com/dunitian/p/4822808.html 屁话我也就不多说了,什么简介的也省了,直接简单概括+demo ...

随机推荐

  1. LeetCode1239串联字符串的最大长度

    题目 给定一个字符串数组 arr,字符串 s 是将 arr 某一子序列字符串连接所得的字符串,如果 s 中的每一个字符都只出现过一次,那么它就是一个可行解. 请返回所有可行解 s 中最长长度. 解题 ...

  2. 1434 区间LCM

    1434 区间LCM 基准时间限制:1 秒 空间限制:131072 KB 一个整数序列S的LCM(最小公倍数)是指最小的正整数X使得它是序列S中所有元素的倍数,那么LCM(S)=X. 例如,LCM(2 ...

  3. 【算法】01-数据结构概述(注意区分jvm堆与堆/jvm栈与栈)

    [算法]01-数据结构概述(注意区分jvm堆与堆/jvm栈与栈) 欢迎关注b站账号/公众号[六边形战士夏宁],一个要把各项指标拉满的男人.该文章已在github目录收录. 屏幕前的大帅比和大漂亮如果有 ...

  4. 基于Spring MVC + Spring + MyBatis的【物流系统 - 公司信息管理】

    资源下载:https://download.csdn.net/download/weixin_44893902/45601768 练习点设计:模糊查询.删除.新增 一.语言和环境 实现语言:JAVA语 ...

  5. 【操作系统】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 多路复 ...

  6. linux修改配置文件关闭终端失效问题

    当前shell环境为 交互式login-shell(非图形化界面环境) /etc/profile /etc/bash.bashrc ~/.profile ~/bashrc 当前环境为 交互式非logi ...

  7. Echart可视化学习(一)

    文档的源代码地址,需要的下载就可以了(访问密码:7567) https://url56.ctfile.com/f/34653256-527823386-04154f 正文: 创建需要的目录结构及文件 ...

  8. nuxt中iview按需加载配置

    在plugins/iview.js中修改 初始代码如下 import Vue from 'vue' import iView from 'iview' import locale from 'ivie ...

  9. js实现工具函数中groupBy数据分组

    数据 this.tableData = [ {id: 1, name: '测试', number: 1, price: 0}, {id: 2, name: '测试', number: 1, price ...

  10. 【Spring专场】「AOP容器」不看源码就带你认识核心流程以及运作原理

    前提回顾 前一篇文章主要介绍了spring核心特性机制的IOC容器机制和核心运作原理,接下来我们去介绍另外一个较为核心的功能,那就是AOP容器机制,主要负责承接前一篇代理模式机制中动态代理:JDKPr ...