Makefile, который охватывает интерфейсы (.h файлы) - PullRequest
0 голосов
/ 29 декабря 2018

Я реализую иерархию Collection, и в этом проекте мне нужны абстрактные классы, у которых нет функций для реализации, поэтому создание этих файлов .cpp для этих классов представляется излишним.У меня есть Makefile, который отлично работает с файлами .cpp, но в этом случае возникли некоторые проблемы.

Файлы, содержащие абстрактные классы (каждая функция является абстрактной):

 -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 голосов
/ 30 декабря 2018

Как вы знаете, цель файла заголовка в C ++ - быть #include -обработанной препроцессором, когда он предварительно обрабатывает файл .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.

Вот вопросы, на которые вам нужно ответить в make-файле:

  • От каких файлов зависит source.o?
  • Что нужно сделать, когда source.o не обновлен (т. е. не существует или старше некоторых файлов, от которых он зависит).

В Make-speak - файлы, которые X зависит от того, называются ли предварительными условиями из X , и действия, которые должны быть выполнены, чтобы X обновляться до рецепт для X .

Итак, ваш make-файл должен сказать, что:

  • 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:

$ 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)

Вы можете исследовать его особенности в руководстве 1 Обратите внимание, в частности, на два отличия от 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 исчез, но make-файл все еще работает!

$ 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 имеет репертуар встроенных правил , и одно из этих встроенных правил является рецептом по умолчанию для созданияfile.o от file.cpp.Рецепт по умолчанию:

%.o: %.cpp:
    $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $@ $<

Так что нам не нужно сообщать make, что, например, rectangle.o зависит от rectangle.cpp, или указывать, что делать, если эта зависимость делает rectangle.o-of-дата.Если требуется rectangle.o для обновления и находит rectangle.cpp, то встроенныйправило говорит ему скомпилировать rectangle.cpp и вывести rectangle.o.

Но make не имеет встроенного правила, сообщающего, что rectangle.o зависит от rectangle.h или main.o зависит от shape.h или triangle.h.Существует бесконечное множество таких возможных зависимостей, поскольку между именем объектного файла и именами файлов заголовков вообще нет систематической связи, которая может быть включена при компиляции исходного файла для создания этого объектного файла.

Следовательно, зависимости объектных файлов от заголовка файлов do должны быть прописаны в make-файле:

main.o: rectangle.h triangle.h shape.h
rectangle.o: rectangle.h shape.h
triangle.o: triangle.h shape.h

Теперь расшифровка заголовочного файлаТакие зависимости «от руки», как это практично, когда наш проект нелепо прост, как prog.Но в реальных проектах это не практично.Там могут быть сотни исходных файлов и сотни файлов заголовков, и один исходный файл может рекурсивно включать заголовки из заголовков изнутри заголовков ... Обычно для нас нереально распутать эти рекурсии, когда нам нужно написать make-файл.

Однако для компилятор (или, строго говоря, препроцессор) не является нереальным: он должен делать это именно тогда, когда обрабатывает исходный файл.

Так чтоОбычный способ работы с зависимостями заголовочного файла при работе с 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 в виде шаблонного правила Наше шаблонное правило:

%.o: %.cpp
    $(CXX) -c -MMD -o $@ $<

вызывает компилятор C ++ ($(CXX)) для компиляции file.cpp и вывода file.o и передает ему параметр препроцессора -MMD.

Эта опция говорит препроцессору написатьдополнительный выходной файл, называемый file.d, если объектный файл file.o, и file.d будет make-файлом , который выражает все предпосылки 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 - это мини-make-файл, который задает предварительные требования 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.Но поскольку они никогда не будут существовать, пока вы не запустите make в первый раз, попытка include их обязательно приведет к неудаче, когда вы do запустите make в первый раз.Проблема курицы и яйца.

Выход из этой проблемы - просто игнорировать неудачу include $(DEPS), если $(DEPS) еще не существует, и поэтомумы пишем:

-include $(DEPS)

вместо просто:

include $(DEPS)

Префикс - к команде в make-файле говорит make игнорировать ошибку.

Youможно глубже погрузиться в генерацию автозависимостей, прочитав Генерация автозависимостей


[1]
0 голосов
/ 29 декабря 2018

collection, set, list и queue являются только заголовками: они не будут генерировать какой-либо объектный код для себя (например, через g ++), будут только цели, которые ссылаются на них.

Вы можете, например, написать collection.cpp, который включает в себя только collection.h.

Опять же, уместно ли даже иметь такой подход?Будучи «чисто виртуальными классами», им действительно нужен файл реализации для себя?Не достаточно ли включить их определение в цели?

Либо удалите .o s из списка зависимостей, либо запишите «пустые» файлы реализации, чтобы он мог сгенерировать для них объектный код.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...