前言

  在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. 【LeetCode】227. Basic Calculator II 解题报告(Python)

    [LeetCode]227. Basic Calculator II 解题报告(Python) 标签(空格分隔): LeetCode 作者: 负雪明烛 id: fuxuemingzhu 个人博客: h ...

  2. Capstone CS5265替代LT8711龙迅|Typec转HDMI4K60HZ投屏转换方案

    LT8711是一款高性能C型/DP1.2至HDMI2.0转换器,设计用于将USB typec或DP1.2源连接至HDMI2.0接收器.LT8711集成了兼容DP1.2的接收机和兼容HDMI2.0的发射 ...

  3. 基于Spring MVC + Spring + MyBatis的【医院就诊挂号系统】

    资源下载:https://download.csdn.net/download/weixin_44893902/21727306 一.语言和环境 1.实现语言: JAVA语言. 2.环境要求: MyE ...

  4. Vulnhub实战-rtemis靶机👻

    Vulnhub实战-rtemis靶机 下载地址:http://www.vulnhub.com/entry/r-temis-1,649/ 描述 通过描述我们知道这个靶机有两个flag 主机发现 通过nm ...

  5. Qos 0/1/2的理解

    Qos 0/1/2的理解 Qos 0 最多一次的传输 消息是基于TCP/IP网络传输的.没有回应,在协议中也没有定义重传的语义.消息可能到达服务器1次,也可能根本不会到达. Qos 1 至少一次的传输 ...

  6. C#自定义转换(implicit 或 explicit)

    C#的类型转换分为显式转换和隐式转换,显式转换需要自己声明转换类型,而隐式转换由编译器自动完成,无需我们声明,如: //long需要显式转换成int long l = 1L; int i = (int ...

  7. C/C++ Qt 运用JSON解析库 [基础篇]

    JSON是一种简单的轻量级数据交换格式,Qt库为JSON的相关操作提供了完整的类支持,使用JSON解析文件之前需要先通过TextStream流将文件读入到字符串变量内,然后再通过QJsonDocume ...

  8. Laravel Redis分布式锁实现源码分析

    首先是锁的抽象类,定义了继承的类必须实现加锁.释放锁.返回锁拥有者的方法. namespace Illuminate\Cache; abstract class Lock implements Loc ...

  9. Centos7 logrotate日志切割

    https://www.cnblogs.com/kevingrace/p/6307298.html 在当前目录下 vim  *****  文件 需要切的日志  /home/soft/app/logs/ ...

  10. xshell 所选的用户密钥未在远程主机上注册;无法加载密钥

    他山之石 https://zhuanlan.zhihu.com/p/92528287 安全起见,服务器最近的安全策略准备进行更改,逐渐由原来的密码登录更换为密钥登录认证. 于是今天把服务器上的id_r ...