|
Проще всего разбить программу на несколько файлов следующим образом: поместить определения всех функций и данных в некоторое число входных файлов, а все типы, необходимые для связи между ними, описать в единственном заголовочном файле. Все входные файлы будут включать заголовочный файл. Программу калькулятора можно разбить на четыре входных файла .c: lex.c, syn.c, table.c и main.c. Заголовочный файл dc.h будет содержать описания каждого имени, которое используется более чем в одном .c файле:
// dc.h: общее описание для калькулятора #include <iostream.h> enum token_value { NAME, NUMBER, END, PLUS='+', MINUS='-', MUL='*', DIV='/', PRINT=';', ASSIGN='=', LP='(', RP=')' }; extern int no_of_errors; extern double error(const char* s); extern token_value get_token(); extern token_value curr_tok; extern double number_value; extern char name_string[256]; extern double expr(); extern double term(); extern double prim(); struct name { char* string; name* next; double value; }; extern name* look(const char* p, int ins = 0); inline name* insert(const char* s) { return look(s,1); }
Если не приводить сами операторы, lex.c должен иметь такой вид:
// lex.c: ввод и лексический анализ #include "dc.h" #include <ctype.h> token_value curr_tok; double number_value; char name_string[256]; token_value get_token() { /* ... */ }
Используя составленный заголовочный файл, мы добьемся, что описание каждого объекта, введенного пользователем, обязательно окажется в том файле, где этот объект определяется. Действительно, при обработке файла lex.c транслятор столкнется с описаниями
extern token_value get_token();
// ...
token_value get_token() { /* ... */ }
Это позволит транслятору обнаружить любое расхождение в типах,
указанных при описании данного имени. Например, если бы функция
get_token() была описана с типом token_value, но определена с
типом int, трансляция файла lex.c выявила бы ошибку: несоответствие
типа.
Файл syn.c может иметь такой вид:
// syn.c: синтаксический анализ и вычисления #include "dc.h" double prim() { /* ... */ } double term() { /* ... */ } double expr() { /* ... */ }
Файл table.c может иметь такой вид:
// table.c: таблица имен и функция поиска #include "dc.h" extern char* strcmp(const char*, const char*); extern char* strcpy(char*, const char*); extern int strlen(const char*); const int TBLSZ = 23; name* table[TBLSZ]; name* look(char* p, int ins) { /* ... */ }
Отметим, что раз строковые функции описаны в самом файле table.c,
транслятор не может проверить согласованность этих описаний по типам.
Всегда лучше включить соответствующий заголовочный файл,
чем описывать в файле .c некоторое имя как extern. Это может
привести к включению "слишком многого", но такое включение нестрашно,
поскольку не влияет на скорость выполнения программы и ее размер, а
программисту позволяет сэкономить время. Допустим, функция strlen() снова
описывается в приведенном ниже файле main.c. Это только лишний
ввод символов и потенциальный источник ошибок, т.к. транслятор
не сможет обнаружить расхождения в двух описаниях strlen() (впрочем,
это может сделать редактор связей). Такой проблемы не возникло бы,
если бы в файле dc.h содержались все описания extern, как первоначально
и предполагалось. Подобная небрежность присутствует в нашем примере,
поскольку она типична для программ на С. Она очень естественна
для программиста, но часто приводит к ошибкам и таким программам,
которые трудно сопровождать. Итак, предупреждение сделано!
Наконец, приведем файл main.c:
// main.c: инициализация, основной цикл, обработка ошибок #include "dc.h" double error(char* s) { /* ... */ } extern int strlen(const char*); int main(int argc, char* argv[]) { /* ... */ }
В одном важном случае заголовочные файлы вызывают большое неудобство. С помощью серии заголовочных файлов и стандартной библиотеки расширяют возможности языка, вводя множество типов (как общих, так и рассчитанных на конкретные приложения; см. главы 5-9). В таком случае текст каждой единицы трансляции может начинаться тысячами строк заголовочных файлов. Содержимое заголовочных файлов библиотеки, как правило, стабильно и меняется редко. Здесь очень пригодился бы претранслятор, который обрабатывает его. По сути, нужен язык специального назначения со своим транслятором. Но устоявшихся методов построения такого претранслятора пока нет.