涵盖接口的Makefile(.h文件)

时间:2018-12-28 23:07:31

标签: c++ makefile g++ polymorphism

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

  

包含抽象类的文件(每个函数都是抽象的):

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

这些文件包含具体功能:

 -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吗?

2 个答案:

答案 0 :(得分:2)

您知道,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不是最新版本(即不存在或比某些版本旧)需要做什么 依赖的文件)。

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

因此您的makefile需要这样说:

  • source.o取决于source.cpp
  • source.o取决于header.h
  • 如果source.o不是最新的,则必须编译source.cpp以产生source.o

header.h而言,仅此而已。

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

shape.h

#ifndef SHAPE_H
#define SHAPE_H

struct shape {
    virtual ~shape() = default;
    virtual double area() const = 0;
};

#endif

rectangle.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

rectangle.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;
}

制作文件(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是用稍微专业的样式写的,则看起来像是:

制作文件(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 )通常 combine 指定目标的先决条件,并指定其目标 食谱。所以:

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,然后找到内置的 规则告诉它编译rectangle.cpp并输出rectangle.o

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

因此,对象文件对 header 文件 do 的依赖性必须为 在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 再以一种更加专业的风格,应该是:

制作文件(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将是一个 makefile ,表示所有 预处理器通过解析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(3) 等效于:

制作文件(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编辑。但是因为它们永远不会存在 您是第一次运行make,尝试include肯定会失败 第一次执行时运行make。鸡和鸡蛋的问题。

解决该问题的方法只是忽略 include $(DEPS)的失败 如果$(DEPS)还不存在,那就是我们这样写的原因:

-include $(DEPS)

而不只是:

include $(DEPS)

在制作文件中将-前缀为命令会告诉make忽略失败。

通过阅读Auto-Dependency Generation

,您可以更深入地研究自动依赖性生成


[1]

答案 1 :(得分:0)

collectionsetlistqueue仅是标头:它们不会为自己生成任何目标代码(例如,通过g ++),而只会生成目标代码链接他们。

例如,您可以编写仅包含collection.cpp的{​​{1}}。

再说一次,采用这种方法是否有意义?作为“纯虚拟类”,他们真的需要自己的实现文件吗?在目标中包含它们的定义还不够吗?

在依赖项列表中删除collection.h,或者编写“空”实现文件,以便它可以为它们生成目标代码。