前言

  在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. 如何让 Spring Security 「少管闲事」

    记两种让 Spring Security「少管闲事」的方法. 遇到问题 一个应用对外提供 Rest 接口,接口的访问认证通过 Spring Security OAuth2 控制,token 形式为 J ...

  2. MySQL与Oracle 差异比较之二函数

    函数 编号 类别 ORACLE MYSQL 注释 1 数字函数 round(1.23456,4) round(1.23456,4) 一样:ORACLE:select round(1.23456,4) ...

  3. AOP 日志切面

    AOP把软件的功能模块分为两个部分:核心关注点和横切关注点.业务处理的主要功能为核心关注点,而非核心.需要拓展的功能为横切关注点.AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点进行分 ...

  4. 【机器学*】k*邻算法-01

    k临*算法(解决分类问题): 已知数据集,以及该数据对应类型 给出一个数据x,在已知数据集中选择最接*x的k条数据,根据这k条数据的类型判断x的类型 具体实现: from numpy import * ...

  5. A Tutorial on Energy-Based Learning

    目录 概 主要内容 损失函数 Energy Loss Generalized Perceptron Loss Generalized Margin Loss Hinge Loss Log Loss L ...

  6. Adversarial Self-Supervised Contrastive Learning

    目录 概 主要内容 Linear Part 代码 Kim M., Tack J. & Hwang S. Adversarial Self-Supervised Contrastive Lear ...

  7. Sentry 开发者贡献指南 - SDK 开发(性能监控:Sentry SDK API 演进)

    内容整理自官方开发文档 本文档的目标是将 Sentry SDK 中性能监控功能的演变置于上下文中. 我们首先总结了如何将性能监控添加到 Sentry 和 SDK, 然后我们讨论 identified ...

  8. 简单的制作ssl证书,并在nginx和IIS中使用

    2020年最后一篇博文收官,提前祝各位园友新年快乐 现在的后端开发,动不动就是需要https,或者说是需要ssl证书,既然没有正版的证书,那么我们只能自己制作ssl的证书了. 说明:证书的制作采用的是 ...

  9. 更便捷的Mybatis增强插件——EasyMybatis

    easy-mybatis是一个对Mybatis的增强框架(插件).在Spring集成Mybatis的基础上,将项目开发中对数据库的常用操作统一化.使用本框架可以很便捷的对数据库进行操作,提高开发效率, ...

  10. Linux密码文件介绍

    1. 查看shadow文件内容```cat /etc/shadow```可以看到shadow文件内容,例如:```root:$1$Bg1H/4mz$X89TqH7tpi9dX1B9j5YsF.:148 ...