straustrup2 (852740), страница 30
Текст из файла (страница 30)
Должен бытьсогласован по типам и входной текст, обрабатываемый транслятором, и связываемые частипрограммы. Есть простой, хотя и несовершенный, способ добиться согласованности описаний вразличных файлах. Это: включить во входные файлы, содержащие операторы и определения данных,заголовочные файлы, которые содержат интерфейсную информацию.Средством включения текстов служит макрокоманда #include, которая позволяет собрать в один файл(единицу трансляции) несколько исходных файлов программы.
Команда#include "включаемый-файл"заменяет строку, в которой она была задана, на содержимое файла включаемый-файл. Естественно,это содержимое должно быть текстом на С++, поскольку его будет читать транслятор. Как правило,операция включения реализуется отдельной программой, называемой препроцессором С++. Онавызывается системой программирования перед собственно трансляцией для обработки таких командво входном тексте. Возможно и другое решение: часть транслятора, непосредственно работающая свходным текстом, обрабатывает команды включения файлов по мере их появления в тексте. В тойсистеме программирования, в которой работает автор, чтобы увидеть результат команд включенияфайлов, нужно задать команду:99Бьерн Страуструп.Язык программирования С++CC -E file.cЭта команда для обработки файла file.c запускает препроцессор (и только!), подобно тому, как командаCC без флага -E запускает сам транслятор.Для включения файлов из стандартных каталогов (обычно каталоги с именем INCLUDE) надо вместокавычек использовать угловые скобки < и >.
Например:#include <stream.h>#include "myheader.h"// включение из стандартного каталога// включение из текущего каталогаВключение из стандартных каталогов имеет то преимущество, что имена этих каталогов никак несвязаны с конкретной программой (обычно вначале включаемые файлы ищутся в каталоге/usr/include/CC, а затем в /usr/include). К сожалению, в этой команде пробелы существенны:#include < stream.h>// <stream.h> не будет найденБыло бы нелепо, если бы каждый раз перед включением файла требовалась его перетрансляция.Обычно включаемые файлы содержат только описания, а не операторы и определения, требующиесущественной трансляторной обработки. Кроме того, система программирования можетпредварительно оттранслировать заголовочные файлы, если, конечно, она настолько развита, чтоспособна сделать это, не изменяя семантики программы.Укажем, что может содержать заголовочный файл:Определения типовШаблоны типовОписания функцийОпределенияфункций-подстановокОписания данныхОпределения константПеречисленияОписания именКоманды включения файловМакроопределенияКомментарииstruct point { int x, y; };template<class T>class V { /* ...
*/ }extern int strlen(const char*);inline char get() { return *p++; }extern int a;const float pi = 3.141593;enum bool { false, true };class Matrix;#include <signal.h>#define Case break;case/* проверка на конец файла */Перечисление того, что стоит помещать в заголовочный файл, не является требованием языка, этопросто совет по разумному использованию включения файлов. С другой стороны, в заголовочномфайле никогда не должно быть:Определений обычных функцийОпределений данныхОпределений составных константchar get() { return *p++; }int a;const tb[i] = { /* ... */ };По традиции заголовочные файлы имеют расширение .h, а файлы, содержащие определения функцийили данных, расширение .c.
Иногда их называют "h-файлы" или "с-файлы" соответственно. Используюти другие расширения для этих файлов: .C, cxx, .cpp и .cc. Принятое расширение вы найдете в своемсправочном руководстве. Макросредства описываются в $$4.7. Отметим только, что в С++ онииспользуются не столь широко, как в С, поскольку С++ имеет определенные возможности в самомязыке: определения констант (const), функций-подстановок (inline), дающие возможность более простойоперации вызова, и шаблонов типа, позволяющие порождать семейство типов и функций ($$8).Совет помещать в заголовочный файл определения только простых, но не составных, константобъясняется вполне прагматической причиной.
Просто большинство трансляторов не настолькоразумно, чтобы предотвратить создание ненужных копий составной константы. Вообще говоря, болеепростой вариант всегда является более общим, а значит транслятор должен учитывать его в первуюочередь, чтобы создать хорошую программу.4.3.1 Единственный заголовочный файлПроще всего разбить программу на несколько файлов следующим образом: поместить определения100Бьерн Страуструп.Язык программирования С++всех функций и данных в некоторое число входных файлов, а все типы, необходимые для связи междуними, описать в единственном заголовочном файле.
Все входные файлы будут включать заголовочныйфайл. Программу калькулятора можно разбить на четыре входных файла .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 может иметь такой вид:101Бьерн Страуструп.Язык программирования С++// 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). В таком случае тексткаждой единицы трансляции может начинаться тысячами строк заголовочных файлов.
Содержимоезаголовочных файлов библиотеки, как правило, стабильно и меняется редко. Здесь очень пригодилсябы претранслятор, который обрабатывает его. По сути, нужен язык специального назначения со своимтранслятором. Но устоявшихся методов построения такого претранслятора пока нет.4.3.2 Множественные заголовочные файлыРазбиение программы в расчете на один заголовочный файл больше подходит для небольшихпрограмм, отдельные части которых не имеют самостоятельного назначения.
Для таких программдопустимо, что по заголовочному файлу нельзя определить, чьи описания там находятся и по какойпричине. Здесь могут помочь только комментарии. Возможно альтернативное решение: пусть каждаячасть программы имеет свой заголовочный файл, в котором определяются средства, предоставляемыедругим частям. Теперь для каждого файла .c будет свой файл .h, определяющий, что можетпредоставить первый. Каждый файл .c будет включать как свой файл .h, так и некоторые другие файлы.h, исходя из своих потребностей.Попробуем использовать такую организацию программы для калькулятора.
Заметим, что функцияerror() нужна практически во всех функциях программы, а сама использует только <iostream.h>. Такаяситуация типична для функций, обрабатывающих ошибки. Следует отделить ее от файла main.c:// error.h: обработка ошибокextern int no_of_errors;extern double error(const char* s);// error.c#include <iostream.h>#include "error.h"int no_of_errors;102Бьерн Страуструп.Язык программирования С++double error(const char* s) { /* ...
*/ }При таком подходе к разбиению программы каждую пару файлов .c и .h можно рассматривать какмодуль, в котором файл .h задает его интерфейс, а файл .c определяет его реализацию.Таблица имен не зависит ни от каких частей калькулятора, кроме части обработки ошибок. Теперь этотфакт можно выразить явно:// table.h: описание таблицы имен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); }// table.h: определение таблицы имен#include "error.h"#include <string.h>#include "table.h"const int TBLSZ = 23;name* table[TBLSZ];name* look(const char* p, int ins) { /* ... */ }Заметьте, что теперь описания строковых функций берутся из включаемого файла <string.h>.