Бьерн Страуструп - Язык программирования С++. Главы 2-4
Страница 53. Единственный заголовочный файл


 

4.3.1 Единственный заголовочный файл

 Проще всего разбить программу на несколько файлов следующим
 образом: поместить определения всех функций и данных в некоторое
 число входных файлов, а все типы, необходимые для связи между
 ними, описать в единственном заголовочном файле. Все входные
 файлы будут включать заголовочный файл. Программу
 калькулятора можно разбить на четыре входных файла .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).
 В таком случае текст каждой единицы трансляции может начинаться
 тысячами строк заголовочных файлов. Содержимое заголовочных
 файлов библиотеки, как правило, стабильно и меняется редко. Здесь
 очень пригодился бы претранслятор, который обрабатывает его. По сути,
 нужен язык специального назначения со своим транслятором. Но устоявшихся
 методов построения такого претранслятора пока нет.

 
« Предыдущая статья   Следующая статья »