Как вы знаете, цель файла заголовка в 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]