Заголовочные Файлы

<!--StartFragment -->
  • Один Заголовочный Файл

  • Множественные Заголовочные Файлы

  • Скрытие Данных

Типы во всех описаниях одного и того же объекта должны быть согласованными. Один из способов это достичь мог бы состоять в обеспечении средств проверки типов в компоновщике, но большинство компоновщиков - образца 1950-х, и их нельзя изменить по практическим соображениям. Другой подход состоит в обеспечении того, что исходный текст, как он передается на рассмотрение компилятору, или согласован, или содержит информацию, которая позволяет компилятору обнаружить несогласованности. Один несовершенный, но простой способ достичь согласованности состоит во включении заголовочных файлов, содержащих интерфейсную информацию, в исходные файлы, в которых содержится исполняемый код и/или определения данных.

Механизм включения с помощью #include - это чрезвычайно простое средство обработки текста для сборки кусков исходной программы в одну единицу (файл) для ее компиляции. Директива

#include \"to_be_included\"

замещает строку, в которой встретилось #include, содержимым файла \"to_be_included\". Его содержимым должен быть исходный текст на C++, поскольку дальше его будет читать компилятор. Часто включение обрабатывается отдельной программой, называемой C препроцессором, которую CC вызывает для преобразования исходного файла, который дал программист, в файл без директив включения перед тем, как начать собственно компиляцию. В другом варианте эти директивы обрабатывает интерфейсная система компилятора по мере того, как они встречаются в исходном тексте. Если программист хочет посмотреть на результат директив включения, можно воспользоваться командой

CC -E file.c

для препроцессирования файла file.c точно также, как это сделала бы CC перед запуском собственно компилятора. Для включения файлов из стандартной директории включения вместо кавычек используются угловые скобки < и >. Например:

#include // из стандартной директории включения
#define \"myheader.h\" // из текущей директории

Использование <> имеет то преимущество, что в программу фактическое имя директории включения не встраивается (как правило, сначала просматривается /usr/include/CC, а потом usr/include). К сожалению, пробелы в директиве include существенны:

#include < stream.h > // не найдет

Может показаться, что перекомпилировать файл заново каждый раз, когда он куда-либо включается, расточительно, но время компиляции такого файла обычно слабо отличается от времени, которое необходимо для чтения его некоторой заранее откомпилированной формы. Причина в том, что текст программы является довольно компактным представлением программы, и в том, что включаемые файлы обычно содержат только описания и не содержат программ, требующих от компилятора значительного анализа.

Следующее эмпирическое правило относительно того, что следует, а что не следует помещать в заголовочные файлы, является не требованием языка, а просто предложением по разумному использованию аппарата #include.

В заголовочном файле могут содержаться:

Определения типов struct point { int x, y; }
Описания функций extern int strlen(const char*);
Определения inline-функций inline char get() { return *p++; }
Описания данных extern int a;
Определения констант const float pi = 3.141593
Перечисления enum bool { false, true };
Директивы include #include
Определения макросов #define Case break;case
Комментарии /* проверка на конец файла */

но никогда
Определения обычных функций char get() { return *p++; }
Определения данных int a;
Определения сложных константных объектов const tbl[] = { /* ... */ }


В системе UNIX принято, что заголовочные файлы имеют суффикс (расширение) .h. Файлы, содержащие определение данных или функций, должны иметь суффикс .c. Такие файлы часто называют, соответственно, \".h файлы\" и \".c файлы". Следует заметить, что в C++ макросы гораздо менее полезны, чем в C, поскольку C++ имеет такие языковые конструкции, как const для определения констант и inline для исключения расходов на вызов функции.

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

Один Заголовочный Файл

Проще всего решить проблему разбиения программы на несколько файлов поместив функции и определения данных в подходящее число исходных файлов и описав типы, необходимые для их взаимодействия, в одном заголовочном файле, который включается во все остальные файлы. Для программы калькулятора можно использовать четыре .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() { /* ... */ }

Это обеспечивает то, что компилятор обнаружит любую несогласованность в типах, указанных для имени. Например, если бы 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[]) { /* ... */ }

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

Множественные Заголовочные Файлы

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

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

// 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) { /* ... */ }
 

 


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

 


// 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() { /* ... */ }

Главная программа, как всегда, тривиальна:

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

Скрытие Данных

Используя заголовочные файлы пользователь может определять явный интерфейс, чтобы обеспечить согласованное использование типов в программе. С другой стороны, пользователь может обойти интерфейс, задаваемый заголовочным файлом, вводя в .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, компилятор не может проверить согласованность этой программы. Следовательно, если только загрузчик не окажется гораздо сообразительнее среднего, две ошибки в этой программе останутся, и их придется искать программисту.

Пользователь может защитить файл от такой недисциплинированной компоновки, описав имена, которые не предназначены для общего пользования, как 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 не обязательно.



Опубликовал admin
23 Мар, Вторник 2004г.



Программирование для чайников.