Эффективное использование GNU Make(C) Владимир Игнатов, 2000Оглавление
0. ПредисловиеВ этой книге я описываю свой опыт работы с утилитой GNU Make и, в частности, мою методику подготовки make-файлов. Я считаю свою методику довольно удобной, поскольку она предполагает:
В начале каждой главы я кратко описываю, о чем в ней будет вестись речь, и какими знаниями нужно обладать, чтобы успешно воспринять излагаемый в главе материал. Для тех, кто чувствует, что недостаточно хорошо ориентируется в предмете разговора, я указываю на дополнительные главы, с которыми следует предварительно ознакомиться. 1. Моя методика использования GNU MakeВ этой главе я описываю свой способ построения make-файлов для сборки проектов с использование программы GNU Make и компилятора GCC (GNU Compiler Collection) . Предполагается, что вы хорошо знакомы с утилитой GNU Make. Если это не так, то прочтите сначала главу 2 - "GNU Make" .1.1. Пример проектаВ качестве примера я буду использовать "гипотический" проект - текстовой редактор. Он состоит из нескольких файлов с исходным текстом на языке C++ (main.cpp, Editor.cpp, TextLine.cpp) и нескольких включаемых файлов (main.h,Editor.h, TextLine.h). Если вы имеете доступ в интернет то "электронный" вариант приводимых в книге примеров можно получить на моей домашней страничке по адресу www.geocities.com/SiliconValley/Office/6533 . Если интернет для вас недоступен, то в Приложении D приведены листинги файлов, которые используются в примерах. 1.2. "Традиционный" способ построения make-файловВ первом примере make-файл построен "традиционным" способом. Все исходные файлы собираемой программы находятся в одном каталоге:
iEdit: main.o Editor.o TextLine.o gcc $^ -o $@ %.o: %.cpp gcc -c $< main.o: main.h Editor.h TextLine.h Editor.o: Editor.h TextLine.h TextLine.o: TextLine.hПервое правило заставляет make перекомпоновывать программу при изменении любого из объектных файлов. Второе правило говорит о том, что объектные файлы зависят от соответствующих исходных файлов. Каждое изменение файла с исходным текстом будет вызывать его перекомпиляцию. Следующие несколько правил указывают, от каких заголовочных файлов зависит каждый из объектных файлов. Такой способ построения make-файла мне кажется неудобным потому что:
1.3. Автоматическое построение списка объектных файлов"Ручное" перечисление всех объектных файлов, входящих в программу - достаточно нудная работа, которая, к счастью, может быть автоматизирована. Разумеется "простой трюк" вроде:
iEdit: *.o gcc $< -o $@ не сработает, так как будут учтены только существующие в данный момент объектные файлы. Я использую чуть более сложный способ, который основан на предположении, что все файлы с исходным текстом должны быть скомпилированы и скомпонованы в собираемую программу. Моя методика состоит из двух шагов:
iEdit: $(patsubst %.cpp,%.o,$(wildcard *.cpp)) gcc $^ -o $@ %.o: %.cpp gcc -c $< main.o: main.h Editor.h TextLine.h Editor.o: Editor.h TextLine.h TextLine.o: TextLine.h Список объектных файлов программы строится автоматически. Сначала с помощью функции wildcard получается список всех файлов с расширением ".cpp", находящихся в директории проекта. Затем, с помощью функции patsubst, полученный таким образом список исходных файлов, преобразуется в список объектных файлов. Make-файл теперь стал более универсальным - с небольшими изменениями его можно использовать для сборки разных программ. 1.4. Автоматическое построение зависимостей от заголовочных файлов"Ручное" перечисления зависимостей объектных файлов от заголовочных файлов - занятие еще более утомительное и неприятное чем "ручное" перечисление объектных файлов. Указывать такие зависимости обязательно нужно - в процессе разработки программы заголовочные файлы могут меняться довольно часто (описания классов, например, традиционно размещаются в заголовочных файлах). Если не указывать зависимости объектных файлов от соответствующих заголовочных файлов, то может сложиться ситуация, когда разные объектные файлы программы будут скомпилированы с использованием разных версии одного и того же заголовочного файла. А это, в свою очередь, может привести к частичной или полной потере работоспособности собранной программы.Перечисление зависимостей "вручную" требует довольно кропотливой работы. Недостаточно просто открыть файл с исходным текстом и перечислить имена всех заголовочных файлов, подключаемых с помощью #include. Дело в том, что одни заголовочные файлы могут, в свою очередь, включать в себя другие заголовочные файлы, так что придется отслеживать всю "цепочку" зависимостей. Утилита GNU Make не сможет самостоятельно построить список зависимостей, поскольку для этого придется "заглядывать" внутрь файлов с исходным текстом - а это, разумеется, лежит уже за пределами ее "компетенции". К счастью, трудоемкий процесс построения зависимостей можно автоматизировать, если воспользоваться помощью компилятора GCC. Для совместной работы с make компилятор GCC имеет несколько опций:
Как видно из таблицы компилятор может работать двумя способами - в одном случае компилятор выдает только список зависимостей и заканчивает работу (опции -M и -MM). В другом случае компиляция происходит как обычно, только в дополнении к объектному файлу генерируется еще и файл зависимостей (опции -MD и -MMD). Я предпочитаю использовать второй вариант - он мне кажется более удобным и экономичным потому что:
Из двух возможных опций -MD и -MMD, я предпочитаю первую потому что:
После того как файлы зависимостей сформированы, нужно сделать их доступными утилите make. Этого можно добиться с помощью директивы include. include $(wildcard *.d) Обратите внимание на использование функции wildcard. Конструкция include *.dбудет правильно работать только в том случае, если в каталоге будет находиться хотя бы один файл с расширением ".d". Если таких файлов нет, то make аварийно завершится, так как потерпит неудачу при попытке "построить" эти файлы (у нее ведь нет на этот счет ни каких инструкций!). Если же использовать функцию wildcard, то при отсутствии искомых файлов, эта функция просто вернет пустую строку. Далее, директива include с аргументом в виде пустой строки, будет проигнорирована, не вызывая ошибки. Теперь можно составить новый вариант make-файла для моего "гипотического" проекта:
iEdit: $(patsubst %.cpp,%.o,$(wildcard *.cpp)) gcc $^ -o $@ %.o: %.cpp gcc -c -MD $< include $(wildcard *.d)После завершения работы make директория проекта будет выглядеть так:
Editor.o: Editor.cpp Editor.h TextLine.hТеперь при изменении любого из файлов - Editor.cpp, Editor.h или TextLine.h, файл Editor.cpp будет перекомпилирован для получения новой версии файла Editor.o. 1.5. "Разнесение" файлов с исходными текстами по директориямПриведенный в предыдущем параграфе make-файл вполне работоспособен и с успехом может быть использован для сборки небольших программ. Однако, с увеличением размера программы, становится не очень удобным хранить все файлы с исходными текстами в одном каталоге. В таком случае я предпочитаю "разносить" их по разным директориям, отражающим логическую структуру проекта. Для этого нужно немного модифицировать make-файл. Чтобы неявное правило%.o: %.cpp gcc -c $< осталось работоспособным, я использую переменную VPATH, в которой перечисляются все директории, где могут располагаться исходные тексты. В следующем примере я поместил файлы Editor.cpp и Editor.h в каталог Editor, а файлы TextLine.cpp и TextLine.h в каталог TextLine:
source_dirs := Editor TextLine search_wildcards := $(addsuffix /*.cpp,$(source_dirs)) iEdit: $(notdir $(patsubst %.cpp,%.o,$(wildcard $(search_wildcards)))) gcc $^ -o $@ VPATH := $(source_dirs) %.o: %.cpp gcc -c -MD $(addprefix -I ,$(source_dirs)) $< include $(wildcard *.d)По сравнению с предыдущим вариантом make-файла он претерпел следующие изменения:
1.6. Сборка программы с разными параметрами компиляцииЧасто возникает необходимость в получении нескольких вариантов программы, которые были скомпилированы по-разному. Типичный пример - отладочная и рабочая версии программы. В таких случаях я использую простую методику:
make compile_flags="-O3 -funroll-loops -fomit-frame-pointer"Обратите внимание, что строка со значением переменной compile_flags заключена в кавычки, так как она содержит пробелы. Командный файл make_debug выглядит аналогично: make compile_flags="-O0 -g" Вот как выглядит makefile для этого примера: source_dirs := Editor TextLine search_wildcards := $(addsuffix /*.cpp,$(source_dirs)) override compile_flags += -pipe iEdit: $(notdir $(patsubst %.cpp,%.o,$(wildcard $(search_wildcards)))) gcc $^ -o $@ VPATH := $(source_dirs) %.o: %.cpp gcc -c -MD $(addprefix -I,$(source_dirs)) $(compile_flags) $< include $(wildcard *.d) Переменная compile_flags получает свое значение из командной строки и, далее, используется при компиляции исходных текстов. Для ускорения работы компилятора, к параметрам компиляции добавляется флажок -pipe. Обратите внимание на необходимость использования директивы override для изменения переменной compile_flags внутри make-файла. 1.7. "Разнесение" разных версий программы по отдельным директориямВ том случае если я собираю несколько вариантов одной и той же программы (например, отладочную и рабочую версию), становится неудобным помещать результаты компиляции в один и тот же каталог. При переходе от одного варианта к другому приходится полностью перекомпилировать программу во избежание нежелательного "смешивания" объектных файлов разных версий.Для решения этой проблемы я помещаю результаты компиляции каждой версии программы в свой отдельный каталог. Так, например, отладочная версия программы (включая все объектные файлы) помещается в каталог debug, а рабочая версия программы - в каталог release:
Главная сложность заключалась в том, чтобы заставить программу make помещать результаты работы в разные директории. Попробовав разные варианты, я пришел к выводу, что самый легкий путь - использование флажка --directory при вызове make. Этот флажок заставляет утилиту перед началом обработки make-файла, сделать каталог, указанный в командной строке, "текущим". Вот, например, как выглядит командный файл make_release, собирающий рабочую версию программы (результаты компиляции помещается в каталог release): mkdir release make compile_flags="-O3 -funroll-loops -fomit-frame-pointer" \ --directory=release \ --makefile=../makefile Команда mkdir введена для удобства - если удалить каталог release, то при следующей сборке он будет создан заново. В случае "составного" имени каталога (например, bin/release) можно дополнительно использовать флажок -p. Флажок --directory заставляет make перед началом работы сделать указанную директорию release текущей. Флажок --makefile укажет программе make, где находится make-файл проекта. По отношению к "текущей" директории release, он будет располагаться в "родительском" каталоге. Командный файл для сборки отладочного варианта программы (make_debug) выглядит аналогично. Различие только в имени директории, куда помещаются результаты компиляции (debug) и другом наборе флагов компиляции: mkdir debug make compile_flags="-O0 -g" \ --directory=debug \ --makefile=../makefileВот окончательная версия make-файла для сборки "гипотического" проекта текстового редактора: program_name := iEdit source_dirs := Editor TextLine source_dirs := $(addprefix ../..,$(source_dirs)) search_wildcards := $(addsuffix /*.cpp,$(source_dirs)) $(program_name): $(notdir $(patsubst %.cpp,%.o, $(wildcard $(search_wildcards) ) ) ) gcc $^ -o $@ VPATH := $(source_dirs) %.o: %.cpp gcc -c -MD $(compile_flags) $(addprefix -I,$(source_dirs)) $< include $(wildcard *.d) В этом окончательном варианте я "вынес" имя исполняемого файла программы в отдельную переменную program_name. Теперь для того чтобы адаптировать этот make-файл для сборки другой программы, в нем достаточно изменить всего лишь несколько первых строк. После запуска командных файлов make_debug и make_release директория с последним примером выглядит так:
В этой главе я изложил свою методику работы с make-файлами. Остальные главы носят более или менее "дополнительный" характер.
2. GNU MakeВ этой главе я кратко опишу некоторые возможности программы GNU Make, которыми я пользуюсь при написании своих make-файлов, а также укажу на ее отличия от "традиционных" версий make. Предполагается, что вы знакомы с принципом работы подобных программ. В противном случае сначала прочтите главу 3 - Утилита make .GNU Make - это версия программы make распространяемая Фондом Свободного Программного Обеспечения (Free Software Foundation - FSF) в рамках проекта GNU ( www.gnu.org ). Получить самую свежую версию программы и документации можно на "домашней страничке" программы www.gnu.org/software/make либо на страничке Paul D. Smith - одного из авторов GNU Make ( www.paulandlesley.org/gmake). Программа GNU Make имеет очень подробную и хорошо написанную документацию, с которой я настоятельно рекомендую ознакомиться. Если у вас нет доступа в интернет, то пользуйтесь документацией в формате Info, которая должна быть в составе вашего дистрибутива Linux. Будьте осторожны с документацией в формате man-странички (man make) - как правило, она содержит лишь отрывочную и сильно устаревшую информацию. 2.1. Две разновидности переменныхGNU Make поддерживает два способа задания переменных, которые несколько различаются по смыслу. Первый способ - традиционный, с помощью оператора '=':compile_flags = -O3 -funroll-loops -fomit-frame-pointerТакой способ поддерживают все варианты утилиты make. Его можно сравнить, например, с заданием макроса в языке Си. #define compile_flags "-O3 -funroll-loops -fomit-frame-pointer"Значение переменной, заданной с помощью оператора '=', будет вычислено в момент ее использования. Например, при обработке make-файла: var1 = one var2 = $(var1) two var1 = three all: @echo $(var2)на экран будет выдана строка "three two". Значение переменной var2 будет вычислено непосредственно в момент выполнения команды echo, и будет представлять собой текущее значение переменной var1, к которому добавлена строка " two". Как следствие - одна и та же переменная не может одновременно фигурировать в левой и правой части выражения, так как это может привести к бесконечной рекурсии. GNU Make распознает подобные ситуации и прерывает обработку make-файла. Следующий пример вызовет ошибку: compile_flags = -pipe $(compile_flags) GNU Make поддерживает также и второй, новый способ задания переменной - с помощью оператора ':=': compile_flags := -O3 -funroll-loops -fomit-frame-pointerВ этом случае переменная работает подобно "обычным" текстовым переменным в каком-нибудь из языков программирования. Вот приблизительный аналог этого выражения на языке C++: string compile_flags = "-O3 -funroll-loops -fomit-frame-pointer";Значение переменной вычисляется в момент обработки оператора присваивания. Если, например, записать var1 := one var2 := $(var1) two var1 := three all: @echo $(var2)то при обработке такого make-файла на экран будет выдана строка "one two". Переменная может "менять" свое поведение в зависимости от того, какой из операторов присваивания был к ней применен последним. Одна и та же переменная на протяжении своей жизни вполне может вести себя и как "макрос" и как "текстовая переменная". Все свои make-файлы я пишу с применением оператора ':='. Этот способ кажется мне более удобным и надежным. Вдобавок это более эффективно, так как значение переменной не вычисляется заново каждый раз при ее использовании. Подробнее о двух способах задания переменных можно прочитать в документации на GNU Make в разделе "The Two Flavors of Variables" . 2.2. Функции манипуляции с текстомУтилита GNU Make содержит большое число полезных функций, манипулирующих текстовыми строками и именами файлов. В частности в своих make-файлах я использую функции addprefix, addsuffix, wildcard, notdir и patsubst. Для вызова функций используется синтаксис $(имя_функции параметр1, параметр2 ... ) Функция addprefix рассматривает второй параметр как список слов разделенных пробелами. В начало каждого слова она добавляет строку, переданную ей в качестве первого параметра. Например, в результате выполнения make-файла: src_dirs := Editor TextLine src_dirs := $(addprefix ../../, $(src_dirs)) all: @echo $(src_dirs)на экран будет выведено ../../Editor ../../TextLine Видно, что к каждому имени директории добавлен префикс "../../". Функция addprefix обсуждается в разделе "Functions for File Names" руководства по GNU Make. Функция addsuffix работает аналогично функции addprefix, только добавляет указанную строку в конец каждого слова. Например, в результате выполнения make-файла: source_dirs := Editor TextLine search_wildcards := $(addsuffix /*.cpp, $(source_dirs)) all: @echo $(search_wildcards) на экран будет выведено Editor/*.cpp TextLine/*.cpp Видно, что к каждому имени директории добавлен суффикс "/*.cpp". Функция addsuffix обсуждается в разделе "Functions for File Names" руководства по GNU Make. Функция wildcard "расширяет" переданный ей шаблон или несколько шаблонов в список файлов, удовлетворяющих этим шаблонам. Пусть в директории Editor находится файл Editor.cpp, а в директории TextLine - файл TextLine.cpp:
search_wildcards := Editor/*.cpp TextLine/*.cpp source_files := $(wildcard $(search_wildcards)) all: @echo $(source_files) на экран будет выведено Editor/Editor.cpp TextLine/TextLine.cpp Видно, что шаблоны преобразованы в списки файлов. Функция wildcard подробно обсуждается в разделе "The Function wildcard" руководства по GNU Make. Функция notdir позволяет "убрать" из имени файла имя директории, где он находится. Например, в результате выполнения make-файла: source_files := Editor/Editor.cpp TextLine/TextLine.cpp source_files := $(notdir $(source_files)) all: @echo $(source_files) на экран будет выведено Editor.cpp TextLine.cppВидно, что из имен файлов убраны "пути" к этим файлам. Функция notdir обсуждается в разделе "Functions for File Names" руководства по GNU Make. Функция patsubst позволяет изменить указанным образом слова, подходящие под шаблон. Она принимает три параметра - шаблон, новый вариант слова и исходную строку. Исходная строка рассматривается как список слов, разделенных пробелом. Каждое слово, подходящее под указанный шаблон, заменяется новым вариантом слова. В шаблоне может использоваться специальный символ '%', который означает "любое количество произвольных символов". Если символ '%' встречается в новом варианте слова (втором параметре), то он заменяется текстом, соответствующим символу '%' в шаблоне. Например, в результате выполнения make-файла: source_files := Editor.cpp TextLine.cpp object_files := $(patsubst %.cpp, %.o, $(source_files)) all: @echo $(object_files) на экран будет выведено Editor.o TextLine.o Видно, что во всех словах окончание ".cpp" заменено на ".o". Функция patsubst имеет второй, более короткий вариант записи для тех случаев, когда надо изменить суффикс слова (например, заменить расширение в имени файла). Более короткий вариант выглядит так: $(имя_переменной:.старый_суффикс=.новый_суффикс)Применяя "короткий" вариант записи предыдущий пример можно записать так: source_files := Editor.cpp TextLine.cpp object_files := $(source_files:.cpp=.o) all: @echo $(object_files) Функция patsubst обсуждается в разделе "Functions for String Substitution and Analysis" руководства по GNU Make. 2.3. Новый способ задания шаблонных правилВ "традиционных" вариантах make шаблонное правило задается с помощью конструкций, наподобие:.cpp.o: gcc $^ -o $@То есть под действие правила попадают файлы с определенными расширениями (".cpp" и ".o" в данном случае). GNU Make поддерживает более универсальный подход - с использованием шаблонов имен файлов. Для задания шаблона используется символ '%', который означает "последовательность любых символов произвольной длины". Символ '%' в правой части правила заменяется текстом, который соответствует символу '%' в левой части. Пользуясь новой формой записи, приведенный выше пример можно записать так: %.o: %.cpp gcc $^ -o $@ В своих make-файлах я пользуюсь новой формой записи шаблонных правил, потому что считаю ее более удобной (шаблонные и нешаблонные правила теперь имеют аналогичный синтаксис) и универсальной (можно задавать не только файлы, отличающиеся своими расширениями). 2.4. Переменная VPATHС помощью переменной VPATH можно задать список каталогов, где шаблонные правила будут искать зависимости. В следующем примере:VPATH := Editor TextLine %.o: %.cpp gcc -c $<make будет искать файлы с расширением ".cpp" сначала в текущем каталоге, а затем, при необходимости, в подкаталогах Editor и TextLine. Я часто использую подобную возможность, так как предпочитаю располагать исходные тексты в иерархии каталогов, отражающих логическую структуру программы. Переменная VPATH описывается в главе "VPATH: Search Path for All Dependencies" руководства по GNU Make. На страничке Paul D. Smith есть статья под названием "How Not to Use VPATH" ( paulandlesley.org/gmake/vpath.html), в которой обсуждается "неправильный" стиль использования переменной VPATH. 2.5. Директива overrideПеременные в GNU Make могут создаваться и получать свое значение разными способами:
compile_flags := -pipe $(compile_flags) all: echo $(compile_flags)Предположим, что переменная compile_flags была задана через командную строку при запуске программы make: make compile_flags="-O0 -g"В результате обработки make-файла на экран будет выведена строка: -O0 -gТо есть попытка изменить значение переменной compile_flags внутри make-файла была проигнорирована. Если все-таки возникает необходимость в изменении переменной, которая была задана с помощью командной строки, нужно использовать директиву override. Директива помещается перед именем переменной, которая должна быть изменена: override compile_flags := -pipe $(compile_flags) all: echo $(compile_flags)Теперь в результате обработки make-файла на экран будет выдана строка: -pipe -O0 -g 2.6. Добавление текста в строкуЧасто возникает необходимость добавить текст к существующей переменной. Для этой цели служит оператор "+=". Добавляемый текст может быть как текстовой константой, так и иметь ссылки на другие переменные:compile_flags += -pipe compile_flags += $(flags) При использовании этого оператора, "тип" переменной (см. раздел 2.1 "Две разновидности переменных") не меняется - "макросы" остаются "макросами", а "текстовые переменные" по-прежнему остаются таковыми. Если переменная задана с помощью командной строки, то по-прежнему для изменения ее значения внутри make-файла нужно использовать директиву override. В следующем примере предполагается, что переменная compile_flags задана в командной строке: override compile_flags += -pipe override compile_flags += $(flags) 2.7. Директива includeС помощью директивы include можно включать в обрабатываемый make-файл другие файлы. Работает она аналогично директиве #include в языках C и C++. Когда встречается эта директива, обработка "текущего" make-файла приостанавливается и make временно "переключается" на обработку указанного в директиве файла. Директива include может оказаться полезной для включения в make-файл каких-либо "общих", или автоматически сгенерированных другими программами фрагментов.В директиве include могут быть указаны одно или несколько имен файлов, разделенных пробелами. В качестве имен файлов можно использовать шаблоны: include common.mak include main.d Editor.d TextLine.d include *.dУказанные в директиве файлы должны существовать - иначе make предпримет попытку "создать" их, а при невозможности этого достигнуть, выдаст сообщение об ошибке. Директива include с пустым списком файлов: includeпросто игнорируется. 2.8. Автоматические переменныеПрограмма GNU Make поддерживает большое число автоматических переменных. В своих make-файлах я использую следующие автоматические переменные:
Полный список автоматических переменных приводится в разделе "Automatic Variables" руководства по GNU Make. 2.9. "Комбинирование" правилВ make-файле могут встречаться несколько правил, имеющих одинаковую цель. В таком случае они как бы "комбинируются вместе". Например, следующие два правила:TextLine.o: TextLine.cpp gcc -c $< TextLine.o: TextLine.hэквивалентны правилу: TextLine.o: TextLine.cpp TextLine.h gcc -c $< Шаблонные и нешаблонные правила также могут "комбинироваться": %.o: %.cpp gcc -c $< TextLine.o: TextLine.h Обратите внимание на то, что в обоих пример только в одном из правил указаны исполняемые команды - именно они и будут при необходимости выполняться. При наличии команд в обоих правилах, make выдаст предупреждающее сообщение и "в расчет" будут приниматься только команды из последнего правила. 2.10. Make-файл, используемый по умолчаниюЕсли при вызове программы GNU Make не указывать явно, какой make-файл следует обрабатывать, то она пытается найти и обработать файлы GNUmakefile, makefile и Makefile (именно в таком порядке). Руководство по GNU Make рекомендует имя Makefile для make-файлов, используемых по умолчанию. При "алфавитной" сортировке имен файлов в директории, такое имя будет располагаться ближе к началу списка.2.11. Специальная цель .PHONYВ традиционных реализациях, у программы make нет надежного способа узнать, чем именно является цель, указанная в правиле. Цель может быть как именем действия, так и именем файла. Исходя только из "внешнего вида" правила, различить эти случаи невозможно. Утилита make просто ищет на диске файл с именем, которое указано в качестве цели. Если такой файл существует, то цель считается именем файла.Для целей, которые являются именами действий, такой подход не очень хорош. Во-первых, имя такой цели может случайно совпасть с именем какого-либо файла или директории. И, во-вторых, make просто нерационально тратит свое время, занимаясь поиском несуществующих файлов. В утилите GNU Make имеется способ явного объявления целей абстрактными. Для этого используется механизм "специальных целей". Специальная цель - это имя, которое имеет специальное значение, когда используется в качестве цели. Для того, например, чтобы объявить перечисленные цели абстрактными, достаточно поместить их в правило со специальной целью .PHONY. В следующем примере цель clean объявляется абстрактной: .PHONY: clean clean: rm *.o *.dВсе возможные специальные цели описаны в главе Special Built-in Target Names" руководства по GNU Make. 3. Утилита makeУтилита make, входящая в состав практически всех Unix-подобных операционных систем - это традиционное средство, применяемое для сборки программных проектов. Она является универсальной программой для решения широкого круга задач, где одни файлы должны автоматически обновляться при изменении других файлов.При запуске программа make читает файл с описанием проекта (make-файл) и, интерпретируя его содержимое, предпринимает необходимые действия. Файл с описанием проекта представляет собой текстовой файл, где описаны отношения между файлами проекта, и действия, которые необходимо выполнить для его сборки. 3.1. ПравилаОсновным "строительным элементом" make-файла являются правила (rules). В общем виде правило выглядит так: <цель_1> <цель_2> ... <цель_n>: <зависимость_1> <зависимость_2> ... <зависимость_n> <команда_1> <команда_2> ... <команда_n>Цель (target) - это некий желаемый результат, способ достижения которого описан в правиле. Цель может представлять собой имя файла. В этом случае правило описывает, каким образом можно получить новую версию этого файла. В следующем примере: iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEdit целью является файл iEdit (исполняемый файл программы). Правило описывает, каким образом можно получить новую версию файла iEdit (скомпоновать из перечисленных объектных файлов). Цель также может быть именем некоторого действия. В таком случае правило описывает, каким образом совершается указанное действие. В следующем примере целью является действие clean (очистка). clean: rm *.o iEditПодобного рода цели называются псевдоцели (pseudotargets) или абстрактные цели (phony targets). Зависимость (dependency)- это некие "исходные данные", необходимые для достижения указанной в правиле цели. Можно сказать что зависимость - это "предварительное условие" для достижения цели. Зависимость может представлять собой имя файла. Этот файл должен существовать, для того чтобы можно было достичь указанной цели. В следующем правиле: iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEdit файлы main.o, Editor.o и TextLine.o являются зависимостями. Эти файлы должны существовать для того, чтобы стало возможным достижение цели - построение файла iEdit. Зависимость также может быть именем некоторого действия. Это действие должно быть предварительно выполнено перед достижением указанной в правиле цели. В следующем примере зависимость clean_obj является именем действия (удалить объектные файлы программы): clean_all: clean_obj rm iEdit clean_obj: rm *.o Для того чтобы цель clean_all была достигнута, нужно сначала выполнить действие (достигнуть цели) clean_obj. Команды - это действия, которые необходимо выполнить для обновления либо достижения цели. В следующем примере: iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEdit командой является вызов компилятора GCC. Утилита make отличает строки, содержащие команды, от прочих строк make-файла по наличию символа табуляции (символа с кодом 9) в начале строки. В приведенном выше примере строка: gcc main.o Editor.o TextLine.o -o iEditдолжна начинаться с символа табуляции. 3.2. Алгоритм работы makeТипичный make-файл проекта содержит несколько правил. Каждое из правил имеет некоторую цель и некоторые зависимости. Смыслом работы make является достижение цели, которую она выбрала в качестве главной цели (default goal). Если главная цель является именем действия (то есть абстрактной целью), то смысл работы make заключается в выполнении соответствующего действия. Если же главная цель является именем файла, то программа make должна построить самую "свежую" версию указанного файла.3.2.1 Выбор главной целиГлавная цель может быть прямо указана в командной строке при запуске make. В следующем примере make будет стремиться достичь цели iEdit (получить новую версию файла iEdit): make iEditА в этом примере make должна достичь цели clean (очистить директорию от объектных файлов проекта): make cleanЕсли не указывать какой-либо цели в командной строке, то make выбирает в качестве главной первую, встреченную в make-файле цель. В следующем примере: iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEdit main.o: main.cpp gcc -c main.cpp Editor.o: Editor.cpp gcc -c Editor.cpp TextLine.o: TextLine.cpp gcc -c TextLine.cpp clean: rm *.oиз четырех перечисленных в make-файле целей (iEdit, main.o, Editor.o, TextLine.o, clean) по умолчанию в качестве главной будет выбрана цель iEdit. Схематично, "верхний уровень" алгоритма работы make можно представить так: make() { главная_цель = ВыбратьГлавнуюЦель() ДостичьЦели( главная_цель ) } 3.2.2 Достижение целиПосле того как главная цель выбрана, make запускает "стандартную" процедуру достижения цели. Сначала в make-файле ищется правило, которое описывает способ достижения этой цели (функция НайтиПравило). Затем, к найденному правилу применяется обычный алгоритм обработки правил (функция ОбработатьПравило).ДостичьЦели( Цель ) { правило = НайтиПравило( Цель ) ОбработатьПравило( правило ) } 3.2.3 Обработка правилОбработка правила разделяется на два основных этапа. На первом этапе обрабатываются все зависимости, перечисленные в правиле (функция ОбработатьЗависимости). На втором этапе принимается решение - нужно ли выполнять указанные в правиле команды (функция НужноВыполнятьКоманды). При необходимости, перечисленные в правиле команды выполняются (функция ВыполнитьКоманды).ОбработатьПравило( Правило ) { ОбработатьЗависимости( Правило ) если НужноВыполнятьКоманды( Правило ) { ВыполнитьКоманды( Правило ) } } 3.2.4 Обработка зависимостейФункция ОбработатьЗависимости поочередно проверяет все перечисленные в правиле зависимости. Некоторые из них могут оказаться целями каких-нибудь правил. Для этих зависимостей выполняется обычная процедура достижения цели (функция ДостичьЦели). Те зависимости, которые не являются целями, считаются именами файлов. Для таких файлов проверяется факт их наличия. При их отсутствии, make аварийно завершает работу с сообщением об ошибке.ОбработатьЗависимости( Правило ) { цикл от i=1 до Правило.число_зависимостей { если ЕстьТакаяЦель( Правило.зависимость[ i ] ) { ДостичьЦели( Правило.зависимость[ i ] ) } иначе { ПроверитьНаличиеФайла( Правило.зависимость[ i ] ) } } } 3.2.5 Обработка командНа стадии обработки команд решается вопрос - нужно ли выполнять описанные в правиле команды или нет. Считается, что нужно выполнять команды если:
НужноВыполнятьКоманды( Правило ) { если Правило.Цель.ЯвляетсяАбстрактной() return true // цель является именем файла если ФайлНеСуществует( Правило.Цель ) return true цикл от i=1 до Правило.Число_зависимостей { если Правило.Зависимость[ i ].ЯвляетсяАбстрактной() return true иначе // зависимость является именем файла { если ВремяМодефикации( Правило.Зависимость[ i ] ) > ВремяМодефикации( Правило.Цель ) return true } } return false } 3.3. Абстрактные цели и имена файловКаким образом make отличает имена действий от имен файлов? Традиционные варианты make поступают просто. Сначала ищется файл с таким именем. Если файл найден, то считается что цель или зависимость являются именем файла.В противном случае считается, что данное имя является либо именем несуществующего файла, либо именем действия. Различия между этими двумя вариантами не делается, поскольку оба случая обрабатываются одинаково. Подобный подход не слишком хорош по следующим соображениям. Во-первых, утилита make не слишком рационально расходует время, занимаясь поиском несуществующих имен файлов, которые на самом деле являются именами действий. Во-вторых, при подобном подходе, имена действий не должны совпадать с именами каких-либо файлов или директорий. Иначе подобный алгоритм даст сбой, и make-файл будет работать неправильно. Некоторые версии make предлагают свои варианты решения этой проблемы. Так, например, в утилите GNU Make имеется механизм (специальная цель .PHONY), с помощью которого можно указать, что данное имя является именем действия. 3.4. Пример работы makeРассмотрим, как утилита make будет обрабатывать такой make-файл:iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEdit main.o: main.cpp gcc -c main.cpp Editor.o: Editor.cpp gcc -c Editor.cpp TextLine.o: TextLine.cpp gcc -c TextLine.cpp clean: rm *.oПредположим, что в директории с проектом находятся следующие файлы:
makeЦель не указана в командной строке, поэтому запускается алгоритм выбора цели (функция ВыбратьГлавнуюЦель). Главной целью становится файл iEdit (первая цель из первого правила). Цель iEdit передается функции ДостичьЦели. Эта функция ищет правило, которое описывает обрабатываемую цель. В данном случае, это первое правило make-файла. Для найденного правила запускается процедура обработки (функция ОбработатьПравило). Сначала поочередно обрабатываются описанные в правиле зависимости (функция ОбработатьЗависимости). Первая зависимость - объектный файл main.o. Поскольку в make-файле есть правило с такой целью (функция ЕстьТакаяЦель возвращает true), то для цели main.o запускается процедура ДостичьЦели. Функция ДостичьЦели ищет правило, где описана цель main.o. Эта цель описана во втором правиле make-файла. Для этого правила запускается функция ОбработатьПравило. Функция ОбработатьПравило запускает процесс обработки зависимостей (функция ОбработатьЗависимости). Во втором правиле указана единственная зависимость - main.cpp. Такой цели в make-файле не существует, поэтому считается, что зависимость main.cpp является именем файла. Далее, проверяется наличие этого файла на диске (функция ПроверитьНаличиеФайла) - такой файл существует. На этом процесс обработки зависимостей завершается. После обработки зависимостей, функция ОбработатьПравило принимает решение о том, нужно ли выполнять указанные в правиле команды (функция НужноВыполнятьКоманды). Цели правила (файла main.o) не существует, поэтому команды нужно выполнять. Функция ВыполнитьКоманды запускает указанную в правиле команду (компилятор GCC), в результате чего создается файл main.o. Цель main.o достигнута (объектный файл main.o построен). Теперь make возвращается к обработке остальных зависимостей первого правила. Зависимости Editor.o и TextLine.o обрабатываются аналогично. Для них выполняются те же действия, что и для зависимости main.o. После того, как все зависимости (main.o, Editor.o и TextLine.o) обработаны, решается вопрос о необходимости выполнения указанных в правиле команд (функция НужноВыполнятьКоманды). Поскольку цель (iEdit) является именем файла, который в данный момент не существует, то принимается решение выполнить описанную в правиле команду (функция ВыполнитьКоманды). Содержащаяся в правиле команда запускает компилятор GCC, в результате чего создается исполняемый файл iEdit. Главная цель (iEdit)таким образом достигнута. На этом программа make завершает свою работу. 3.5. Еще один пример работы makeРассмотрим, как будет действовать утилита make, если для обработки описанного в предыдущей главе make-файла, она будет вызвана следующим образом:make cleanЦель явно указана в командной строке, поэтому главной целью становится абстрактная цель clean. Цель clean передается функции ДостичьЦели. Эта функция ищет правило, которое описывает обрабатываемую цель. Это будет пятое правило make-файла. Для найденного правила запускается процедура обработки (функция ОбработатьПравило). Поскольку в правиле не указано каких-либо зависимостей, make сразу переходит к этапу обработки указанных в правиле команд. Цель является именем действия, поэтому команды нужно выполнять. Указанные в правиле команды выполняются, и цель clean, таким образом, считается достигнутой. На этом программа make завершает работу. 3.6. ПеременныеВозможность использования переменных внутри make-файла - очень удобное и часто используемое свойство make. В традиционных версиях утилиты, переменные ведут себя подобно макросам языка Си. Для задания значения переменной используется оператор присваивания. Например, выражение:obj_list = main.o Editor.o TextLine.oприсваивает переменной obj_list значение "main.o Editor.o TextLine.o" (без кавычек). Пробелы между символом '=' и началом первого слова игнорируются. Следующие за последним словом пробелы также игнорируются. Значение переменной можно использовать с помощью конструкции: $(имя_переменной) Например, при обработке такого make-файла: dir_list = . .. src/include all: echo $(dir_list)на экран будет выведена строка: . .. src/include Переменные могут не только содержать текстовые строки, но и "ссылаться" на другие переменные. Например, в результате обработки make-файла: optimize_flags = -O3 compile_flags = $(optimize_flags) -pipe all: echo $(compile_flags) на экран будет выведено: -O3 -pipe Во многих случаях использование переменных позволяет упростить make-файл и повысить его наглядность. Для того чтобы облегчить модификацию make-файла, можно разместить "ключевые" имена и списки в отдельных переменных и поместить их в начало make-файла: program_name = iEdit obj_list = main.o Editor.o TextLine.o $(program_name): $(obj_list) gcc $(obj_list) -o $(program_name) ... Адаптация такого make-файла для сборки другой программы сведется к изменению нескольких начальных строк. 3.7. Автоматические переменныеАвтоматические переменные - это переменные со специальными именами, которые "автоматически" принимают определенные значения перед выполнением описанных в правиле команд. Автоматические переменные можно использовать для "упрощения" записи правил. Такое, например, правило:iEdit: main.o Editor.o TextLine.o gcc main.o Editor.o TextLine.o -o iEditс использованием автоматических переменных можно записать следующим образом: iEdit: main.o Editor.o TextLine.o gcc $^ -o $@Здесь $^ и $@ являются автоматическими переменными. Переменная $^ означает "список зависимостей". В данном случае при вызове компилятора GCC она будет ссылаться на строку "main.o Editor.o TextLine.o". Переменная $@ означает "имя цели" и будет в этом примере ссылаться на имя "iEdit". Иногда использование автоматических переменных совершенно необходимо - например, в шаблонных правилах (о них пойдет речь в следующей главе). 3.8. Шаблонные правилаШаблонные правила (implicit rules или pattern rules) - это правила, которые могут быть применены к целой группе файлов. В этом их отличие от обычных правил - описывающих отношения между конкретными файлами.Традиционные реализации make поддерживают так называемую "суффиксную" форму записи шаблонных правил: .<расширение_файлов_зависимостей>.<расширение_файлов_целей>: <команда_1> <команда_2> ... <команда_n>Например, следующее правило говорит о том, что все файлы с расширением "o" зависят от соответствующих файлов с расширением "cpp": .cpp.o: gcc -c $^Обратите внимание на использование автоматической переменной $^ для передачи компилятору имени файла-зависимости. Поскольку шаблонное правило может применяться к разным файлам, использование автоматических переменных - это единственный способ узнать для каких файлов сейчас задействуется правило. Шаблонные правила позволяют упростить make-файл и сделать его более универсальным. Рассмотрим простой проектный файл: iEdit: main.o Editor.o TextLine.o gcc $^ -o $@ main.o: main.cpp gcc -c $^ Editor.o: Editor.cpp gcc -c $^ TextLine.o: TextLine.cpp gcc -c $^Все исходные тексты программы обрабатываются одинаково - для них вызывается компилятор GCC. С использованием шаблонных правил, этот пример можно переписать так: iEdit: main.o Editor.o TextLine.o gcc $^ -o $@ .cpp.o: gcc -c $^Когда make ищет в файле проекта правило, описывающее способ достижения искомой цели (см. главу 3.2.2. "Достижение цели", функция НайтиПравило), то в расчет принимаются и шаблонные правила. Для каждого из них проверяется - нельзя ли задействовать это правило для достижения искомой цели. Приложение A. Редактирование make-файлов в разных операционных системамиЕсли, наряду с операционной системой Linux, вы работаете с операционными системами фирмы Microsoft (DOS, Windows), то при редактировании make-файлов в разных системах могут возникнуть определенные трудности.Проблема состоит в том, что принятый в Unix-подобных операционных системах формат хранения текстовых файлов, несколько отличается от формата DOS/Windows. В Unix каждая строка текстового файла заканчивается символом "перевод строки" (код 0x0A). В DOS и Windows текстовые строки разделяются парой символов - "возврат каретки", "перевод строки" (0x0D, 0x0A). Linux-версия программы GNU Make будет нормально работать с make-файлами, написанными в среде Linux. Версия утилиты GNU Make для Windows также будет нормально работать с make-файлами, подготовленными в среде Windows. Проблема возникнет лишь в том случае, если попытаться обработать Linux-версией программы GNU Make текстовой файл, подготовленный в среде DOS или Windows. Подобная ситуация может возникнуть "нечаянно" - достаточно лишь "сохранить" make-файл для среды Linux в текстовом редакторе DOS/Windows, чтобы он оказался "испорчен". В отличие от компилятора GCC, который просто игнорирует символы "возврат каретки", GNU Make рассматривает их как "обычные" символы, которые вполне могут быть частью имени. В результате все слова, находящиеся в конце строк, искажаются, так как сзади к ним добавляется невидимый символ "возврат каретки". В следующем примере имя файла TextLine.o будет искажено (его длина будет составлять одиннадцать символов из-за невидимого "возврата строки"): iEdit: main.o Editor.o TextLine.o gcc $^ -o $@ Разумеется, на диске не найдется файла с таким "странным" именем и make-файл будет работать неверно. Дело осложняется еще и тем, что выдаваемые на экран диагностические сообщения также искажаются (при выводе на экран символ "возврат каретки" возвращает курсор в начало строки) и зачастую представляют собой лишь бессмысленные "обрывки" слов. Описанную проблему можно решить разными способами. Организационный метод решения - никогда не пытаться редактировать make-файл, находясь в "чужеродной" для него среде. Другой возможный подход - "принудительно" удалять из make-файла символы "возврат каретки" (либо "вручную" - текстовым редактором, либо с помощью подходящей программы). Приложение B. Организация иерархии каталогов в сложных проектахДля сложных проектов, состоящих из большого количества файлов, я предпочитаю более сложную организацию каталогов, чем та, которая приводилась в качестве примера в разделе 1.7. "Разнесение разных версий программы по отдельным директориям". Основная идея заключается в том, чтобы файлы с разным "назначением" помещались в разные каталоги. В моих проектах дерево каталогов выглядит примерно так:
В директорию doc я помещаю различные текстовые файлы - документацию, замечания, список ошибок и тому подобное. Здесь же располагается и файл README.txt. В каталоге project находится make-файл проекта и командные файлы, используемые для сборки программы в разных конфигурациях. В каталог src я помещаю исходные тексты программы. Внутри директории src имеется своя иерархия каталогов, отражающая логическую структуру программы. Вот пример make-файла, который работает с подобной структурой директорий проекта: program_name := iEdit source_dirs := . Editor TextLine include_dirs := /c/aproj/lib /c/aproj/lib/linux link_flags := -static source_dirs := $(addprefix ../../src/, $(source_dirs) ) source_files := $(wildcard $(addsuffix /*.cpp, $(source_dirs) ) ) object_files := $(notdir $(source_files) ) object_files := $(object_files:.cpp=.o) $(program_name): $(object_files) gcc $^ -o $@ $(link_flags) -pipe VPATH := $(source_dirs) %.o: %.cpp gcc $< -c $(compile_flags) $(addprefix -I , $(include_dirs)) -MD -pipe 2>log include $(wildcard *.d)Список директорий, где располагаются файлы с исходными текстами (source_dirs), задается относительно каталога src. Вот как выглядит командный файл, собирающий отладочную версию программы: mkdir -p ../bin/linux_debug make compile_flags="-O0 -g" \ --directory=../bin/linux_debug \ --makefile=../../project/MakefileКомандный файл, собирающий рабочую версию программы выглядит аналогично: mkdir -p ../bin/linux_release make compile_flags="-O3 -funroll-loops -fomit-frame-pointer" \ --directory=../bin/linux_release \ --makefile=../../project/Makefile Приложение C. Компилятор GCCGNU Compiler Collection (GCC) - это семейство компиляторов с языков C, C++ и Object-C, которые объединены общей технологией и распространяются в рамках проекта GNU. Домашняя страничка компилятора находится по адресу www.gnu.org/software/gcc/gcc.htmlЭтот компилятор является "стандартным" средством для компиляции всех программ, входящих в проект GNU. GCC также является основным компилятором операционной системы Linux - с его помощью компилируется ядро системы. Версии компилятораКомпилятор GCC развивается весьма динамично - программа улучшается, исправляются обнаруженные ошибки, добавляются новые возможности. Всегда желательно знать с какой версией компиляторы вы в данный момент работаете. Возможно, что эта версия еще не поддерживает нужные вам возможности или содержит ошибки, которые могут повлиять на работоспособность компилируемой программы. Узнать версию компилятора GCC можно с помощью ключа -v:gcc -v Отладка"Стандартным" средством для отладки программ, скомпилированных компилятором GCC, является отладчик GDB. Этот отладчик свободно распространяется в рамках проекта GNU. Домашняя страничка отладчика находится по адресу www.gnu.org/software/gdb/gdb.html.Для подготовки отладочной версии программы с помощью компилятора GCC, достаточно отключить оптимизацию и включить генерацию отладочной информации. Для этого я использую следующие опции компиляции:
Отладчик GDB имеет текстовой интерфейс командной строки. Мне этот интерфейс кажется не очень удобными, поэтому я пользуюсь графической оболочкой DataDisplayDebugger (DDD). Эта оболочка является надстройкой над "текстовыми" отладчиками, реализующей для них удобный графический интерфейс. Программа DDD также входит в проект GNU. Ее домашняя страничка находится по адресу www.gnu.org/software/ddd . DataDisplayDebugger работает в среде X-Windows. Рабочий вариантПри компиляции рабочего варианта программы, я включаю максимальную оптимизацию по скорости. Возможно это приводит к некоторому увеличению размера программы, но я считаю это не слишком важным. Вряд ли кто-нибудь заметит увеличение размера программы на пять-десять килобайт. В то же время быстродействия программам всегда не хватает.Компилятор GCC имеет большое количество опций, управляющих процессом кодогенерации и оптимизации, с которыми вы можете экспериментировать, добиваясь максимального быстродействия программы. Для своих проектов я использую следующие настройки:
Обработка исключенийЕсли вы используете механизм исключений (exceptions) языка C++, то при компиляции должна быть включена соответствующая опция:
Статическая и динамическая компоновкаПо умолчанию компилятор компонует собранную программу с динамическими версиями стандартных библиотек. Это не всегда удобно. Для того чтобы стандартные библиотеки компоновались статически, нужно использовать опцию --static. В этом случае генерируется полностью "статический" код, который для своей работы не требует наличия каких-либо загружаемых библиотек.Получение листингаЧасто бывает полезным иметь ассемблерный листинг кода, генерируемого компилятором. С помощью такого листинга можно:
Обратите внимание на то, что указание флажка -S просто "останавливает" компилятор после фазы генерации ассемблерного листинга, то есть процесс компиляции прерывается. Как следствие - процесс сборки программы и процесс генерации ассемблерных листингов "несовместимы" между собой. Можно либо получать листинги, либо собирать программу, но не то и другое одновременно. Для получения листингов я обычно создаю отдельный командный файл, который среди прочих опций компиляции содержит флажки -S и -fverbose-asm. Весьма полезная возможность компилятора - помещать в листинг список всех опций компиляции, которые были включены в данный момент. Дело в том, что включение одних опций (например -O3) может "автоматически" приводить к включению других опций, а документация к GCC не всегда точна в описании подобных зависимостей. Некоторые версии GCC всегда помещают в листинг список используемых опций, другие версии делают это только при наличии флажка -fverbose-asm. Переназначение ошибок в файлПо умолчанию, компилятор GCC выдает ошибки в стандартный поток сообщений об ошибках (файл с дескриптором 2). Иногда это не очень удобно - сообщения об ошибках могут быть очень длинными подробными а "прокрутка" экрана назад не всегда доступна. Поэтому я обычно перенаправляю сообщения компилятора в файл. Этот файл потом можно просмотреть любым стандартным средством. Вот, например, как может выглядеть правило для компиляции исходных текстов:%.o: %.cpp gcc -c $< 2>log Опция -pipeКомпилятор GCC обрабатывает программу за несколько проходов, помещая промежуточные результаты компиляции во временные файлы. Процесс компиляцию можно ускорить, если воспользоваться опцией pipe. При включении этой опции, различные этапы компиляции начинают "общаться" между собой не через временные файлы, а через каналы обмена (pipes).Тексты с символом "возврат каретки"В отличие от утилиты GNU Make, компилятор GCC вполне "лояльно" относится к наличию символов "возврат каретки" в компилируемых текстах - такие символы попросту игнорируются. Поэтому, проблем при компиляции исходных текстов, подготовленных в среде DOS/Windows, возникнуть не должно.Приложение D. "Гипотический" проект - текстовой редакторВ первой главе "Моя методика использования GNU Make" в качестве примера рассматривается "гипотический" проект - текстовой редактор. Он состоит из трех файлов с исходным текстом на языке C++ (main.cpp, Editor.cpp и TextLine.cpp), а также трех заголовочных файлов (main.h, Editor.h, TextLine.h). Ниже приведены листинги этих файлов.Файл main.cpp: #include "main.h" void main() { } Файл main.h: // main.h Файл Editor.cpp: #include "Editor.h" Файл Editor.h: #include "TextLine.h" Файл TextLine.cpp: #include "TextLine.h" Файл TextLine.h: // TextLine.h |