c++ - 涵盖接口(interface)的Makefile(.h文件)

标签 c++ makefile g++ polymorphism

我正在实现Collection层次结构,在这个项目中,我需要一些没有实现功能的抽象类,因此为这些类创建.cpp文件似乎很多余。
我有一个Makefile与.cpp文件配合使用,但是在这种情况下会出现一些问题。

The files that includes abstract classes (every function is abstract):

 -collection.h
 -set.h
 -list.h
 -queue.h

These are files includes concrete functions:

 -hashSet.h
 -hashSet.cpp
 -arrayList.h
 -arrayList.cpp
 -linkedList.h
 -linkedList.cpp
 -iterator.h
 -iterator.cpp


我的Makefile在下面
obj = main.o collection.o set.o list.o queue.o hashSet.o arrayList.o iterator.o

output : $(obj)
    g++ -g -Wno-deprecated -std=c++11 -ansi -pedantic -Wall $(obj) -o output

main.o : main.cpp
    g++  -g -Wno-deprecated -std=c++11 -c main.cpp

%.o : %.cpp %.h
    g++  -g -Wno-deprecated -std=c++11 -c $<

clean :
    rm *.o output

当前错误:
make: *** No rule to make target 'collection.o', needed by 'output'.  Stop.

您能帮我重新设计Makefile吗?

最佳答案

如您所知,C++中的头文件的目的是通过#include -ed
预处理器,当它预处理.cpp文件时,使其简单地成为一部分
编译.cpp文件时编译器使用的源代码的百分比。

因此,头文件header.h永远不会单独编译,也没有相应的目标文件header.o曾经生产过。 header.h#include,例如source.cppsource.cpp已编译,
包括header.h的内容,生成的目标文件是source.o
source.o显然取决于source.cpp:每当更改source.cpp时,您
需要重新编译以产生新的source.o。但是因为source.cpp包含header.hsource.o取决于header.h同样是正确的:因此,每当更改header.h时,
您再次需要重新编译source.cpp以产生新的source.o

这些是您需要在Makefile中回答的问题:

  • source.o依赖哪些文件?
  • 如果source.o不是最新的(即不存在或比某些版本旧,则需要做什么)
    所依赖的文件)。

  • 在Make-speak中,X依赖的文件称为X的先决条件,
    而使X保持最新状态所必须执行的操作就是X的配方。

    因此,您的makefile需要说:
  • source.o取决于source.cpp
  • source.o取决于header.h
  • 如果source.o不是最新的,则必须编译source.cpp以产生source.o

  • header.h而言,仅此而已。

    这是一个具体的示例,例如您的类层次结构项目
    具有仅 header 的抽象基类:-

    shape.h
    #ifndef SHAPE_H
    #define SHAPE_H
    
    struct shape {
        virtual ~shape() = default;
        virtual double area() const = 0;
    };
    
    #endif
    

    矩形.h
    #ifndef RECTANGLE_H
    #define RECTANGLE_H
    
    #include <shape.h>
    
    struct rectangle : shape {
        rectangle(double length, double width);
        ~rectangle() override = default;
        double area() const override;
    private:
        double _length;
        double _width;
    };
    
    #endif
    

    triangle.h
    #ifndef TRIANGLE_H
    #define TRIANGLE_H
    
    #include <shape.h>
    
    struct triangle : shape {
        triangle(double side1, double side2, double side3);
        ~triangle() override = default;
        double area() const override;
    private:
        double _side1;
        double _side2;
        double _side3;
    };
    
    #endif
    

    矩形.cpp
    #include "rectangle.h"
    
    rectangle::rectangle(double length, double width)
    : _length(length),_width(width){}
    
    double rectangle::area() const {
        return _length * _width;
    }
    

    triangle.cpp
    #include "triangle.h"
    #include <cmath>
    
    triangle::triangle(double side1, double side2, double side3)
    : _side1(side1),_side2(side2),_side3(side3){}
    
    double triangle::area() const {
        double halfperim = (_side1 + _side2 + _side3) / 2;
        double area2ed = halfperim *
            (halfperim - _side1) * (halfperim - _side2) * (halfperim - _side3);
        return std::sqrt(area2ed);
    }
    

    main.cpp
    #include <shape.h>
    #include <triangle.h>
    #include <rectangle.h>
    #include <memory>
    #include <iostream>
    
    int main()
    {
        std::unique_ptr<shape> s{new rectangle{2,3}};
        std::cout << "Rectangular shape's area is " << s->area() << std::endl;
        s.reset(new triangle{3,4,5});
        std::cout << "Triangular shape's area is " << s->area() << std::endl;
        return 0;
    }
    

    Makefile(1)
    # Builds program `prog`
    
    .PHONY: clean   # `clean` is a phony target, not a real file
    
    prog: main.o rectangle.o triangle.o     # Prerequisites of `prog`
    
    prog:   # This is how to make `prog` up-to-date
        g++ -o $@ $^    # Link all the prerequisites (`$^`), output the target (`$@`)
    
    main.o: main.cpp shape.h rectangle.h triangle.h     # Prerequisites of `main.o`
    rectangle.o: rectangle.cpp rectangle.h shape.h      # Prerequisites of `rectangle.o`
    triangle.o: triangle.cpp triangle.h shape.h         # Prerequisites of `triangle.o`
    
    %.o:    # This is how to make any `*.o` file up-to-date
        g++ -c -o $@ $<     # Compile the first prerequisite (`$<`), output the target
    
    clean:
        rm -f prog main.o rectangle.o triangle.o
    
    Makefile以不切实际的风格编写,以最大程度地减少干扰
    并强调指定目标先决条件之间的区别
    并指定使其保持最新状态的操作。但这是正确的并且可以运行
    第一次像:
    $ make
    g++ -c -o main.o main.cpp     # Compile the first prerequisite (`main.cpp`), output the target
    g++ -c -o rectangle.o rectangle.cpp     # Compile the first prerequisite (`rectangle.cpp`), output the target
    g++ -c -o triangle.o triangle.cpp     # Compile the first prerequisite (`triangle.cpp`), output the target
    g++ -o prog main.o rectangle.o triangle.o    # Link all the prerequisites (`main.o rectangle.o triangle.o`), output the target (`prog`)
    

    之后prog运行如下:
    $ ./prog
    Rectangular shape's area is 6
    Triangular shape's area is 6
    

    如果您修改triangle.cpp,则triangle.oprog将过时。
    我们可以使用touch shell命令伪造一个修改:
    $ touch triangle.cpp
    $ make
    g++ -c -o triangle.o triangle.cpp     # Compile the first prerequisite (`triangle.cpp`), output the target
    g++ -o prog main.o rectangle.o triangle.o    # Link all the prerequisites (`main.o rectangle.o triangle.o`), output the target (`prog`)
    

    如果您修改rectangle.h,那么rectangle.omain.oprog将过时:
    $ touch rectangle.h
    $ make
    g++ -c -o main.o main.cpp     # Compile the first prerequisite (`main.cpp`), output the target
    g++ -c -o rectangle.o rectangle.cpp     # Compile the first prerequisite (`rectangle.cpp`), output the target
    g++ -o prog main.o rectangle.o triangle.o    # Link all the prerequisites (`main.o rectangle.o triangle.o`), output the target (`prog`)
    

    而且,如果您修改shape.h(抽象基类),则所有对象文件以及prog都将过时:
    $ touch shape.h
    $ make
    g++ -c -o main.o main.cpp     # Compile the first prerequisite (`main.cpp`), output the target
    g++ -c -o rectangle.o rectangle.cpp     # Compile the first prerequisite (`rectangle.cpp`), output the target
    g++ -c -o triangle.o triangle.cpp     # Compile the first prerequisite (`triangle.cpp`), output the target
    g++ -o prog main.o rectangle.o triangle.o    # Link all the prerequisites (`main.o rectangle.o triangle.o`), output the target (`prog`)
    

    如果Makefile以更专业的风格编写,它将看起来像:

    Makefile(2)
    SRCS := main.cpp rectangle.cpp triangle.cpp
    OBJS := $(SRCS:.cpp=.o)
    
    .PHONY: all clean
    
    all: prog
    
    prog: $(OBJS)
        $(CXX) -o $@ $^
    
    main.o: rectangle.h triangle.h shape.h
    rectangle.o: rectangle.h shape.h
    triangle.o: triangle.h shape.h
    
    clean:
        $(RM) prog $(OBJS)
    

    您可以在the manual 1中研究其功能。
    特别注意与Makefile(1)的两个区别:-

    1 )通常将指定目标的先决条件与指定目标
    食谱。所以:
    prog: $(OBJS)
        $(CXX) -o $@ $^
    

    只是一种较短的书写方式:
    prog: $(OBJS)
    
    prog:
        $(CXX) -o $@ $^
    

    或确实:
    prog: main.o
    prog: rectangle.o
    prog: triangle.o
        $(CXX) -o $@ $^
    
    makeprog的所有前提条件组合到一个列表中并执行配方
    如果目标相对于任何一个目标都已过时。

    2 )制作*.o文件的配方已消失,但makefile
    仍然有效!
    $ make clean
    rm -f prog main.o rectangle.o triangle.o
    $ make
    g++    -c -o main.o main.cpp
    g++    -c -o rectangle.o rectangle.cpp
    g++    -c -o triangle.o triangle.cpp
    g++ -o prog main.o rectangle.o triangle.o
    

    那是因为make的曲目集为built-in rules
    这些内置规则之一是从file.o制作file.cpp的默认方法。默认配方为:
    %.o: %.cpp:
        $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $@ $<
    

    因此,我们不需要告诉make,例如rectangle.o取决于rectangle.cpp或者告诉它如果该依赖关系使rectangle.o过时了该怎么办。如果它
    需要rectangle.o是最新的并找到rectangle.cpp,然后内置
    rule告诉它编译rectangle.cpp并输出rectangle.o

    但是make没有内置规则告诉它rectangle.o取决于rectangle.hmain.o取决于shape.htriangle.h。有无限的多样性
    这种可能的依赖关系,因为根本没有系统的关系
    在目标文件的名称和可能是的头文件的名称之间
    包括在编译源文件以生成该目标文件时。

    因此,对象文件对头文件的依赖关系必须是
    在makefile中阐明:
    main.o: rectangle.h triangle.h shape.h
    rectangle.o: rectangle.h shape.h
    triangle.o: triangle.h shape.h
    

    现在像这样手动“拼写”出头文件依赖性
    当我们的项目非常简单时,例如prog。但是在现实生活中的项目中
    这是不实际的。可能有数百个源文件和数百个
    头文件和一个源文件可以递归地包含来自
    在标题内从标题内...通常,我们不现实
    需要编写makefile时,请解开这些递归。

    但是,对于编译器(或严格来说是预处理器)来说,这并非不切实际。
    来解开它们:在预处理源文件时,它必须完全这样做。

    因此,正常工作时处理头文件依赖项的常规方法
    与GNU Make和GCC一起,利用了现有GCC预处理器的功能
    为了解决这个问题。使用此功能重写Makefile再以一种更加专业的风格,它将是:

    Makefile(3)
    SRCS := main.cpp rectangle.cpp triangle.cpp
    OBJS := $(SRCS:.cpp=.o)
    DEPS := $(SRCS:.cpp=.d)
    
    .PHONY: all clean
    
    all: prog
    
    prog: $(OBJS)
        $(CXX) -o $@ $^
    
    %.o: %.cpp
        $(CXX) -c -MMD -o $@ $<
    
    clean:
        $(RM) prog $(OBJS) $(DEPS)
    
    -include $(DEPS)
    

    您会在这里看到我们带回了一个制作file.o的方法file.cpp,以pattern-rule的形式
    我们的模式规则:
    %.o: %.cpp
        $(CXX) -c -MMD -o $@ $<
    

    调用C++编译器($(CXX))来编译file.cpp并输出file.o,以及
    将其传递给预处理器
    选项 -MMD

    此选项告诉预处理器写一个附加的输出文件,称为file.d,如果
    目标文件是file.o,而file.d将是一个表达所有
    预处理器通过解析file.o发现的file.cpp的先决条件(不包括系统头文件)。

    我们来看一下:
    $ make clean
    rm -f prog main.o rectangle.o triangle.o main.d rectangle.d triangle.d
    $ make
    g++ -c -MMD -o main.o main.cpp
    g++ -c -MMD -o rectangle.o rectangle.cpp
    g++ -c -MMD -o triangle.o triangle.cpp
    g++ -o prog main.o rectangle.o triangle.o
    
    $ cat main.d
    main.o: main.cpp shape.h triangle.h rectangle.h
    
    $ cat rectangle.d
    rectangle.o: rectangle.cpp rectangle.h shape.h
    
    $ cat triangle.d
    triangle.o: triangle.cpp triangle.h shape.h
    

    如您所见,file.d是一个微型makefile,符合先决条件
    file.o
    DEPS := $(SRCS:.cpp=.d)
    

    使$(DEPS)进入列表main.d rectangle.d triangle.d。和:
    -include $(DEPS)
    

    Makefile(3)中包括所有这些微型makefile。所以Makefile(3)
    等效于:

    Makefile(4)
    SRCS := main.cpp rectangle.cpp triangle.cpp
    OBJS := $(SRCS:.cpp=.o)
    DEPS := $(SRCS:.cpp=.d)
    
    .PHONY: all clean
    
    all: prog
    
    prog: $(OBJS)
        $(CXX) -o $@ $^
    
    %.o: %.cpp
        $(CXX) -c -MMD -o $@ $<
    
    clean:
        $(RM) prog $(OBJS) $(DEPS)
    
    main.o: main.cpp shape.h triangle.h rectangle.h
    
    rectangle.o: rectangle.cpp rectangle.h shape.h
    
    triangle.o: triangle.cpp triangle.h shape.h
    

    这种使预处理器找出头文件的技术
    过于复杂而无法通过脑力找出的依赖是
    通常称为自动依赖生成,这是专业的
    处理您所问问题的方法。

    您可能已经注意到,只有一点障碍。那些.d文件
    make运行模式%.o: %.cpp的配方时由处理器创建。
    并且必须在include中使用Makefile -ed。但是因为它们永远不会存在
    您是第一次运行make,尝试对其进行include肯定会失败
    当您第一次运行make时。鸡和蛋的问题。

    从该问题中脱颖而出只是忽略include $(DEPS)的失败
    如果$(DEPS)尚不存在,那就是我们这样写的原因:
    -include $(DEPS)
    

    不仅仅是:
    include $(DEPS)
    

    在生成文件中将-前缀为命令可以告诉make忽略故障。

    您可以通过阅读Auto-Dependency Generation来更深入地研究自动依赖性生成

    [1]
  • 6.2 The Two Flavors of Variables
  • 6.3.1 Substitution References
  • 4.6 Phony Targets
  • 10.3 Variables Used by Implicit Rules
  • 关于c++ - 涵盖接口(interface)的Makefile(.h文件),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53965259/

    相关文章:

    c++ - RVO/NRVO 和公共(public)未定义复制构造函数

    c++ - 在这种情况下,为什么 g++ 和 clang 会破坏 namespace 抽象?

    c++ - std::function 的 typename 关键字

    c - 在 TM4c1294XL 中嵌入 python 解释器

    c++ - 使用 C API 更改 Tensorflow 推理的线程数

    c - -lpthread 在 C Makefile 中的位置

    c++ - 将参数传递给 Makefile 以更改编译后的代码

    c++ - constexpr 构造函数在 GCC 在编译时评估时给出不同的结果

    c++ - 如何有效地使用 std::async 对指针数组执行操作

    c++ - Box2d - 从 body 获取夹具的宽度还是我应该尝试?