Причина того, почему в заголовочных файлах допускается определение простых констант, но не допускается определение сложных константных объектов, прагматическая. В принципе, сложность тут только в том, чтобы сделать допустимым дублирвание определений переменных (даже определения функций можно было бы дублировать). Однако для компоновщиков старого обраца слишком трудно проверять тождественность нетривиальных констант и убирать ненужные повторы. Кроме того, простые слчаи гораздо более обиходны и потому более важны для генерации хорошего кода.

4.3.1 Один заголовочный файл

Проще всего решить проблему разбиения программы на неколько файлов поместив функции и определения данных в подхдящее число исходных файлов и описав типы, необходимые для их взаимодействия, в одном заголовочном файле, который включаеся во все остальные файлы. Для программы калькулятора можно использовать четыре .c файла: lex.c, syn.c, table.c и main.c, и заголовочный файл dc.h, содержащий описания всех имен, кторые используются более чем в одном .c файле:

// dc.h: общие описания для калькулятора

enum token_value (* NAME, NUMBER, END, PLUS='+', MINUS='-', MUL='*', DIV='/', PRINT=';', ASSIGN='=', LP='(', RP=')' *);

extern int no_of_errors; extern double error(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(char* p, int ins = 0); inline name* insert(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 TBLSZ = 23; name* table[TBLSZ];

name* look(char* p; int ins) (* /* ... */ *)

Заметьте, что table.c сам описывает стандартные функции для работы со строками, поэтому никакой проверки согласованости этих описаний нет. Почти всегда лучше включать заголвочный файл, чем описывать имя в .c файле как extern. При этом может включаться «слишком много», но это обычно не окзывает серьезного влияния на время, необходимое для компилции, и как правило экономит время программиста. В качестве примера этого, обратите внимание на то, как strlen() заново описывается в main() (ниже). Это лишние нажатия клавиш и воможный источник неприятностей, поскольку компилятор не может проверить согласованность этих двух определений. На самом дле, этой сложности можно было бы избежать, будь все описания extern помещены в dc.h, как и предлагалось сделать. Эта «нережность» сохранена в программе, поскольку это очень типично для C программ, очень соблазнительно для программиста, и чаще приводит, чем не приводит, к ошибкам, которые трудно обнаржить, и к программам, с которыми тяжело работать. Вас предуредили!

И main.c, наконец, выглядит так:

// main.c: инициализация, главный цикл и обработка ошибок

#include «dc.h»

int no_of_errors;

double error(char* s) (* /* ... */ *)

extern int strlen(const char*);

main(int argc, char* argv[]) (* /* ... */ *)

Важный случай, когда размер заголовочных файлов станвится серьезной помехой. Набор заголовочных файлов и библиотеку можно использовать для расширения языка множеством общи специальноприкладных типов (см. Главы 5-8). В таких случаях не принято осуществлять чтение тысяч строк заголовоных файлов в начале каждой компиляции. Содержание этих файлов обычно «заморожено» и изменяется очень нечасто. Наиболее плезным может оказаться метод затравки компилятора содержанием этих заголовочных фалов. По сути, создается язык специального назначения со своим собственным компилятором. Никакого стадартного метода создания такого компилятора с затравкой не принято.

4.3.2 Множественные заголовочные файлы

Стиль разбиения программы с одним заголовочным файлом наиболее пригоден в тех случаях, когда программа невелика и ее части не предполагается использовать отдельно. Поэтому то, что невозможно установить, какие описания зачем помещены в заголовочный файл, несущественно. Помочь могут комментарии. Другой способ – сделать так, чтобы каждая часть программы имела свой заголовочный файл, в котором определяются предотавляемые этой частью средства. Тогда каждый .c файл имеет соответствующий .h файл, и каждый .c файл включает свой собтвенный (специфицирующий то, что в нем задается) .h файл и, возможно, некоторые другие .h файлы (специфицирующие то, что ему нужно).

Рассматривая организацию калькулятора, мы замечаем, что error() используется почти каждой функцией программы, а сама использует только «stream.h». Это обычная для функции ошибок ситуация, поэтому error() следует отделить от main():

// error.h: обработка ошибок

extern int no_errors;

extern double error(char* s);

// error.c

#include «stream.h» #include «error.h»

int no_of_errors;

double error(char* s) (* /* ... */ *)

При таком стиле использования заголовочных файлов .h файл и связанный с ним .c файл можно рассматривать как мдуль, в котором .h файл задает интерфейс, а .c файл задает реализацию. Таблица символов не зависит от остальной части калькулятора за исключением использования функции ошибок. Это можно сделать явным:

// table.h: описания таблицы имен

struct name (* char* string; name* next; double value; *);

extern name* look(char* p, int ins = 0); inline name* insert(char* s) (* return look(s,1); *)

// table.c: определения таблицы имен

#include «error.h» #include «string.h» #include «table.h»

const TBLSZ = 23; name* table[TBLSZ];

name* look(char* p; int ins) (* /* ... */ *)

Заметьте, что описания функций работы со строками теперь включаются из «string.h». Это исключает еще один возможный источник ошибок.

// lex.h: описания для ввода и лексического анализа

enum token_value (* NAME, NUMBER, END, PLUS='+', MINUS='-', MUL='*', DIV='/', PRINT=';', ASSIGN='=', LP='(', RP=')' *);

extern token_value curr_tok; extern double number_value; extern char name_string[256];

extern token_value get_token();

Этот интерфейс лексического анализатора достаточно бепорядочен. Недостаток в надлежащем типе лексемы обнаруживает себя в необходимости давать пользователю get_token() фактческие лексические буферы number_value и name_string.

// lex.c: определения для ввода и лексического анализа

#include «stream.h» #include «ctype.h» #include «error.h» #include «lex.h»

token_value curr_tok; double number_value; char name_string[256];

token_value get_token() (* /* ... */ *)


Перейти на страницу:
Изменить размер шрифта: