Однано для компоновщиков старого образца слишком трудно проверять
тождественность нетривиальных констант и убирать ненужные повторы.
Кроме того, простые случаи гораздо более обиходны и потому более
важны для генерации хорошего кода.
4.3.1 Один Заголовочный Файл
Проще всего решить проблему разбиения программы на несколько
файлов поместив функции и определения данных в подходящее число
исходных файлов и описав типы, необходимые для их взаимодействия, в
одном заголовочном файле, который включается во все остальные
- стр 114 -
файлы. Для программы калькулятора можно использовать четыре .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
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() { /* ... */ }
- стр 115 -
Это обеспечивает то, что компилятор обнаружит любую
несогласованность в типах, указанных для имени. Например, если бы
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, наконец, выглядит так:
- стр 116 -
// 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()
используется почти каждой функцией программы, а сама использует
только . Это обычная для функции ошибок ситуация, поэтому
error() следует отделить от main():
- стр 117 -
// error.h: обработка ошибок
extern int no_errors;
extern double error(char* s);
// error.c
#include
#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
#include "table.h"
const TBLSZ = 23;
name* table[TBLSZ];
name* look(char* p; int ins) { /* ... */ }
Заметьте, что описания функций работы со строками теперь
включаются из . Это исключает еще один возможный источник
ошибок.
- стр 118 -
// 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
#include
#include "error.h"
#include "lex.h"
token_value curr_tok;
double number_value;
char name_string[256];
token_value get_token() { /* ... */ }
Интефейс синтаксического анализатора совершенно прозрачен:
// syn.c: описания для синтаксического анализа и вычисления
extern double expr();
extern double term();
extern double prim();
// syn.c: определения для синтаксического анализа и вычисления
#include "error.h"
#include "lex.h"
#include "syn.h"
double prim() { /* ... */ }
double term() { /* ... */ }
double expr() { /* ... */ }
Главная программа, как всегда, тривиальна:
- стр 119 -
// main.c: главная программа
#include
#include "error.h"
#include "lex.h"
#include "syn.h"
#include "table.h"
#include
main(int argc, char* argv[]) { /* ... */ }
Сколько заголовочных файлов использовать в программе, зависит от
многих факторов. Многие из этих факторов сильнее связаны с тем, как
ваша система работает с заголовочными файлами, нежели с C++.
Например, если в вашем редакторе нет средств, позволяющих
одновременно видеть несколько файлов, использование большого числа
файлов становится менее привлекательным. Аналогично, если
открывание и чтение 10 файлов по 50 строк в каждом требует заметно
больше времени, чем чтение одного файла в 500 строк, вы можете
дважды подумать, прежде чем использовать в небольшом проекте стиль
множественных заголовочных файлов. Слово предостережения: набор из
десяти заголовочных файлов плюс стандартные заголовочные файлы
обычно легче поддаются управлению. С другой стороны, если вы
разбили описания в большой программе на логически минимальные по
размеру заголовочные файлы (помещая каждое описание структуры в
свой отдельный файл и т.д.), у вас легко может получиться
неразбериха из сотен файлов.
4.3.3 Скрытие Данных
Используя заголовочные файлы пользователь может определять явный
интерфейс, чтобы обеспечить согласованное использование типов в
программе. С другой стороны, пользователь может обойти интерфейс,
задаваемый заголовочным файлом, вводя в .c файлы описания extern.
Заметьте, что такой стиль компоновки не рекомендуется:
// file1.c: // "extern" не используется
int a = 7;
const c = 8;
void f(long) { /* ... */ }
// file2.c: // "extern" в .c файле
extern int a;
extern const c;
extern f(int);
int g() { return f(a+c); }
Поскольку описания extern в file2.c не включаются вместе с
определениями в файле file1.c, компилятор не может проверить
согласованность этой программы. Следовательно, если только
загрузчик не окажется гораздо сообразительнее среднего, две ошибки
в этой программе останутся, и их придется искать программисту.
Пользователь может защитить файл от такой недисциплинированной
компоновки, описав имена, которые не предназначены для общего
- стр 120 -
пользования, как static, чтобы их областью видимости был файл, и
они были скрыты от остальных частей программы. Например:
// table.c: определения таблицы имен
#include "error.h"
#include
#include "table.h"
const TBLSZ = 23;
static name* table[TBLSZ];
name* look(char* p; int ins) { /* ... */ }
Это гарантирует, что любой доступ к table действительно будет
осуществляться именно через look(). "Прятать" константу TBLSZ не
обязательно.
4.4 Файлы как Модули
В предыдущем разделе .c и .h файлы вместе определяли часть
программы. Файл .h является интерфейсом, который используют другие
части программы; .c файл задает реализацию. Такой объект часто
называют модулем. Доступными делаются только те имена, которые