• 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目录下。

    其它的细节,建议读者跟着做一遍应该可以掌握。

  • 相关阅读:
    JQuery性能优化
    分页控件X.PagedList.Mvc
    《转》sql 、linq、lambda 查询语句的区别
    Linq的连表查询
    MVC页面直接F5出错
    详解集合
    Json的序列化与反序列化
    《转》dbcontext函数
    《转》jquery中的$.ajax的success与error
    cocos creator基础-(二十七)httpclient Get POST
  • 原文地址:https://www.cnblogs.com/learnhow/p/15863951.html
Copyright © 2020-2023  润新知