我正在实现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吗?
如您所知,C ++中头文件的目的是由预处理器在预处理#include
文件时进行.cpp
-ed,这样它就会成为编译器在.cpp
文件时使用的源代码的一部分。编译。
因此,头文件header.h
永远不会单独编译,也不会生成相应的目标文件header.o
。 header.h
是#include
-ed,比如source.cpp
;编译source.cpp
,包括header.h
的内容,生成的目标文件是source.o
。
source.o
显然取决于source.cpp
:每当source.cpp
改变时,你需要重新编译它以产生一个新的source.o
。但由于source.cpp
包括header.h
,同样正确的source.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
而言,这就是全部。
这是一个具体的例子,类似于你的类层次结构项目及其只有标题的抽象基类:
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;
}
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.o
和prog
将会变得过时。我们可以使用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.o
,main.o
和prog
将变得过时:
$ 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 manual1中调查其功能特别注意与Makefile
(1)的两个不同之处: -
1)通常将指定目标的先决条件与指定其配方相结合。所以:
prog: $(OBJS)
$(CXX) -o $@ $^
只是一种较短的写作方式:
prog: $(OBJS)
prog:
$(CXX) -o $@ $^
或者确实:
prog: main.o
prog: rectangle.o
prog: triangle.o
$(CXX) -o $@ $^
make
将prog
的所有先决条件合并到一个列表中,并且如果目标相对于任何目标已过时,则执行配方。
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.h
或main.o
依赖于shape.h
或triangle.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
将是一个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
是一个迷你制作文件,它指出了file.o
的先决条件。
DEPS := $(SRCS:.cpp=.d)
使$(DEPS)
进入名单main.d rectangle.d triangle.d
。和:
-include $(DEPS)
包括Makefile
(3)中的所有迷你制作文件。所以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)
将-
前缀到makefile中的命令会告诉make
忽略失败。
您可以通过阅读Auto-Dependency Generation深入了解自动依赖生成
collection
,set
,list
和queue
只是标题:它们不会为自己生成任何目标代码(例如通过g ++),只有链接它们的目标才会生成。
例如,您可以编写仅包含collection.cpp
的collection.h
。
那么,这种方法甚至是否相关?作为“纯虚拟类”,他们真的需要一个自己的实现文件吗?是不是在目标中包含了他们的定义?
删除依赖项列表中的.o
s或编写“空”实现文件,以便它可以为它们生成目标代码。