我正在实现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吗?
答案 0 :(得分:2)
您知道,C ++中的头文件的目的是#include
-ed,由
预处理器,当它对.cpp
文件进行预处理时,它可以简单地成为一部分
.cpp
文件被编译时编译器消耗的源代码。
因此,头文件header.h
从未单独编译,也没有相应的目标文件header.o
曾经生产过。 header.h
是#include
的版本,例如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
不是最新版本(即不存在或比某些版本旧)需要做什么
依赖的文件)。在说说中, 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.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
是用稍微专业的样式写的,则看起来像是:
制作文件(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 $@ $^
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
。有无限的多样性
这种可能的依赖关系,因为根本没有系统的关系
在目标文件的名称和可能是的头文件的名称之间
包括在编译源文件以生成目标文件时。
因此,对象文件对 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 :(得分:0)
collection
,set
,list
和queue
仅是标头:它们不会为自己生成任何目标代码(例如,通过g ++),而只会生成目标代码链接他们。
例如,您可以编写仅包含collection.cpp
的{{1}}。
再说一次,采用这种方法是否有意义?作为“纯虚拟类”,他们真的需要自己的实现文件吗?在目标中包含它们的定义还不够吗?
在依赖项列表中删除collection.h
,或者编写“空”实现文件,以便它可以为它们生成目标代码。