При создании больших программ разработчиков подстерегает ошибка иного рода, которая проявляется в основном на стадии разработки, но может завести такой проект почти в глухой тупик. Такая ошибка возникает в тех случаях, когда при компиляции и компоновке программы один или несколько файлов ресурсов оказываются устаревшими по сравнению с соответствующими объектными файлами. Если такое случится, то полученная в результате исполняемая программа не будет функционировать так, как предусмотрено в последней реализации исходного кода. Наверно каждый, кто когда-либо участвовал в создании или сопровождении большого программного проекта, сталкивался с подобной проблемой. Чтобы помочь избежать обескураживающих ошибок подобного типа, большинство компиляторов С имеют в своем составе утилиту под названием МАКЕ, которая помогает синхронизировать файлы ресурсов и объектные файлы. (Точное название МАКЕ-утилиты, соответствующей вашему компилятору, может немного отличаться от МАКЕ, поэтому для большей уверенности проверьте себя по документации, прилагаемой к компилятору.)
Программа МАКЕ автоматизирует процесс перекомпиляции крупных программ, скомпонованных из нескольких файлов. Очень часто в процессе разработки программы во многие файлы вносится масса незначительных изменений. После этого программа повторно компилируется и тестируется. К сожалению, довольно легко забыть, какие именно файлы нуждаются в перекомпиляции. В такой ситуации вы можете или перекомпилировать все файлы и потерять массу времени, или случайно пропустить файл, который обязательно необходимо перекомпилировать. А это, в свою очередь, может повлечь за собой необходимость в дополнительной, иногда многочасовой, отладке. Программа МАКЕ решает эту проблему посредством автоматической перекомпиляции только тех файлов, которые претерпели изменения.
Примеры, приведенные в этом разделе, совместимы с программами МАКЕ, поставляемыми вместе с Microsoft C/C++. На сегодняшний день, Microsoft-версия программы МАКЕ носит название NMAKE. Эти примеры будут также работать со многими другими широко распространенными МАКЕ-утилитами, а общие концепции, описанные здесь, применимы для всех МАКЕ-программ.
На заметку | В последние годы программы МАКЕ стали очень изощренными. Примеры, приведенные здесь, иллюстрируют только основные возможности МАКЕ. Стоит подробнее изучить утилиту МАКЕ, поддерживаемую вашим компилятором. Она может содержать такие функциональные особенности, которые окажутся исключительно полезными в вашей среде разработки. |
В своей работе утилита МАКЕ руководствуется сборочным файлом проекта или так называемым make-файлом, который содержит перечень выходных файлов (target files), зависимых файлов (dependent files) и команд. Для генерации выходного файла необходимо наличие файлов, от которых он зависит. Например, от Т.С зависит файл T.OBJ, поскольку Т.С необходим для создания T.OBJ. В процессе работы утилиты МАКЕ производится сравнение даты выходного файла с датой файла, от которого он зависит. (В данном случае под термином «дата» подразумевается и календарная дата, и время.) Если выходной файл старше, т.е. имеет более позднюю дату создания, чем файл, от которого он зависит (или если выходной файл вообще отсутствует), выполняется указанная последовательность команд. Если эта последовательность команд использует выходные файлы, при построении которых используются файлы, от которых они зависят, то при необходимости модифицируются также и эти используемые файлы[1]. Когда процесс МАКЕ завершится, все выходные файлы будут обновлены. Следовательно, в правильно построенном сборочном файле проекта все исходные файлы, которые требуют компиляции, автоматически компилируются и компонуются, образуя новый исполняемый модуль. Таким образом, данная утилита следит, чтобы все изменения в исходных файлах были отражены в соответствующих им объектных файлах.
В общем виде make-файл выглядит следующим образом:
target_file1: dependent_file list
command_sequence
target_file2: dependent_file list
command_sequence
target_file3: dependent_file list
command_sequence
.
.
.
target_fileN: dependent_file list
command_sequence
Имя выходного файла должно начинаться в крайней левой позиции; за ним должно следовать двоеточие и список файлов, от которых он зависит. Последовательность команд, соответствующая каждому выходному файлу, должна предваряться как минимум одним пробелом или одним знаком табуляции. Перед комментариями должен стоять знак #, а сами комментарии могут следовать за списком зависимых файлов и/или последовательностью команд. Кроме того, их можно написать в отдельных строках. Спецификации выходных файлов должны отделяться друг от друга по крайней мере одной пустой строкой.
Самое главное при работе с make-файлом — учитывать следующую особенность: выполнение make-файла заканчивается сразу после того, как удастся обработать первую же цепочку зависимостей. Это означает, что необходимо разрабатывать свои make-файлы таким образом, чтобы зависимости составляли иерархическую структуру. Запомните, что ни одна зависимость не будет считаться успешно обработанной до тех пор, пока все подчиненные ей зависимости (т.е. зависимости более низкого уровня) не будут разрешены.
Чтобы лучше понять, как работает утилита МАКЕ, давайте рассмотрим очень простую программу. Она состоит из четырех файлов под названием TEST.H, TEST.C, TEST2.C и TEST3.C. Рис. 27.2 иллюстрирует данную ситуацию. (Чтобы лучше понять, о чем идет речь, введите каждую часть программы в указанные файлы.)
TEST.H
extern int count;
TEST.C
#include <stdio.h>
void test2(void), test3(void);
int count = 0;
int main(void)
{
printf("count = %d\n", count);
test2( );
printf("count = %d\n", count);
test3( );
printf("count = %d\n", count);
return 0;
}
TEST2.C
#include <stdio.h>
#include "test.h"
void test2(void)
{
count = 30;
}
TEST3.C
#include <stdio.h>
#include "test.h"
void test3(void)
{
count = -100;
}
Рис. 27.2. Простая программа, состоящая из четырех файлов
Если в своей работе вы используете Visual C++, следующий make-файл перекомпилирует данную программу после того, как вы внесете в них какие-нибудь изменения:
test.exe: test.h test.obj test2.obj test3.obj
cl test.obj test2.obj test3.obj
test.obj: test.c test.h
cl -c test.c
test2.obj: test2.c test.h
cl -c test2.c
test3.obj: test3.c test.h
cl -c test3.c
По умолчанию программа MAKE выполняет директивы, содержащиеся в файле под названием MAKEFILE. Однако, как правило, разработчики предпочитают применять другие имена для своих сборочных файлов проекта. Задать другое имя make-файла можно с помощью опции -f в командной строке. Например, если упоминавшийся ранее make-файл называется TEST, то, чтобы с помощью программы NMAKE фирмы Microsoft скомпилировать необходимые модули и создать исполняемый модуль, в командной строке следует набрать нечто подобное следующей строке:
nmake -f test
(Эта команда подходит для программы NMAKE фирмы Microsoft. Если вы пользуетесь другой утилитой МАКЕ, возможно, придется использовать другое имя опции.)
Очень большое значение в сборочном файле проекта имеет очередность спецификаций, поскольку, как уже упоминалось ранее, МАКЕ прекращает выполнение содержащихся в файле директив сразу после того, как она полностью обработает первую зависимость[2]. Например, допустим, что ранее упоминавшийся make-файл был изменен, так что он стал выглядеть следующим образом:
# Это неправильный make-файл.
test.obj: test.c test.h
cl -c test.c
test2.obj: test2.c test.h
cl -c test2.c
test3.obj: test3.c test.h
cl -c test3.c
test.exe: test.h test.obj test2.obj test3.obj
cl test.obj test2.obj test3.obj
Теперь работа будет выполнена неправильно, если файл TEST.H (или любой другой исходный файл) будет изменен. Это происходит потому, что последняя директива (которая создает новый TEST.EXE) больше не будет выполняться.
Использование макросов в MAKE
МАКЕ позволяет определять макросы в make-файле. Имена этих макросов являются просто метками-заполнителями информации, которая в действительности будет определена или в командной строке, или в макроопределении из make-файла. Общая форма определения макроса следующая:
имя_макроса=определение
Если в макроопределении используется символ пробела, то такое определение следует заключить в двойные кавычки.
После определения макроса его можно использовать в make-файле следующим образом:
$(имя_макроса)
Вместо каждого вхождения такого оператора подставляется его макроопределение. Например, в следующем make-файле макрос LIBFIL позволяет указать редактору связей библиотеку:
LIBFIL = graphics.lib
prog.exe: prog.obj prog2.obj prog3.obj
cl prog.obj prog2.obj prog3.obj $(LIBFIL)
Многие МАКЕ-программы имеют дополнительные функциональные возможности, поэтому очень важно внимательно познакомиться с документацией, поставляемой вместе с компилятором.
[1]Все происходит как при вычислении сложной функции: сначала вычисляются аргументы, от которых она зависит, а если они, в свою очередь, являются сложными функциями, то при необходимости сначала вычисляются их аргументы и т.д.
[2]Т.е. как только построит первый зависимый файл.