Главная страница
Навигация по странице:

  • 4.1 Введение

  • 4.2 Связывание

  • 4.3 Заголовочные файлы

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

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

  • Бьерн Страуструп. Язык программирования С Второе дополненное издание


    Скачать 2.87 Mb.
    НазваниеБьерн Страуструп. Язык программирования С Второе дополненное издание
    Дата30.01.2020
    Размер2.87 Mb.
    Формат файлаpdf
    Имя файлаStraustrup-Yazyk_programmirovaniya_c.pdf
    ТипДокументы
    #106559
    страница9 из 35
    1   ...   5   6   7   8   9   10   11   12   ...   35
    ГЛАВА 4.
    Итерация присуща человеку, а рекурсия - богу.
    - Л. Дойч
    Все нетривиальные программы состоят из нескольких раздельно транслируемых единиц, по традиции называемых файлами. В этой главе описано, как раздельно транслируемые функции могут вызывать друг друга, каким образом они могут иметь общие данные, и как добиться непротиворечивости типов, используемых в разных файлах программы. Подробно обсуждаются функции, в том числе: передача параметров, перегрузка имени функции, стандартные значения параметров, указатели на функции и, естественно, описания и определения функций. В конце главы обсуждаются макровозможности языка.
    4.1 Введение
    Роль файла в языке С++ сводится к тому, что он определяет файловую область видимости ($$R.3.2).
    Это область видимости глобальных функций (как статических, так и подстановок), а также глобальных переменных (как статических, так и со спецификацией const). Кроме того, файл является традиционной единицей хранения в системе, а также единицей трансляции. Обычно системы хранят, транслируют и представляют пользователю программу на С++ как множество файлов, хотя существуют системы, устроенные иначе. В этой главе будет обсуждаться в основном традиционное использование файлов.
    Всю программу поместить в один файл, как правило, невозможно, поскольку программы стандартных функций и программы операционной системы нельзя включить в текстовом виде в программу пользователя. Вообще, помещать всю программу пользователя в один файл обычно неудобно и непрактично. Разбиения программы на файлы может облегчить понимание общей структуры программы и дает транслятору возможность поддерживать эту структуру. Если единицей трансляции является файл, то даже при небольшом изменении в нем следует его перетранслировать. Даже для программ не слишком большого размера время на перетрансляцию можно значительно сократить, если ее разбить на файлы подходящего размера.
    Вернемся к примеру с калькулятором. Решение было дано в виде одного файла. Когда вы попытаетесь его транслировать, неизбежно возникнут некоторые проблемы с порядком описаний. По крайней мере одно "ненастоящее" описание придется добавить к тексту, чтобы транслятор мог разобраться в использующих друг друга функциях expr(), term() и prim(). По тексту программы видно, что она состоит из четырех частей: лексический анализатор (сканер), собственно анализатор, таблица имен и драйвер.
    Однако, этот факт никак не отражен в самой программе. На самом деле калькулятор не был запрограммирован именно так. Так не следует писать программу. Даже если не учитывать все рекомендации по программированию, сопровождению и оптимизации для такой "зряшной" программы, все равно ее следует создавать из нескольких файлов хотя бы для удобства.
    Чтобы раздельная трансляция стала возможной, программист должен предусмотреть описания, из которых транслятор получит достаточно сведений о типах для трансляции файла, составляющего только часть программы. Требование непротиворечивости использования всех имен и типов для программы, состоящей из нескольких раздельно транслируемых частей, так же справедливо, как и для программы, состоящей из одного файла. Это возможно только в том случае, когда описания, находящиеся в разных единицах трансляции, будут согласованы. В вашей системе программирования имеются средства, которые способны установить, выполняется ли это. В частности, многие противоречия обнаруживает редактор связей. Редактор связей - это программа, которая связывает по именам раздельно транслируемые части программы. Иногда его по ошибке называют загрузчиком.
    4.2 Связывание
    Если явно не определено иначе, то имя, не являющееся локальным для некоторой функции или класса, должно обозначать один и тот же тип, значение, функцию или объект во всех единицах трансляции данной программы. Иными словами, в программе может быть только один нелокальный тип, значение, функция или объект с данным именем. Рассмотрим для примера два файла:
    // file1.c

    Бьерн Страуструп.
    Язык программирования С++
    98 int a = 1; int f() { /* какие-то операторы */ }
    // file2.c extern int a; int f(); void g() { a = f(); }
    В функции g() используются те самые a и f(), которые определены в файле file1.c. Служебное слово extern показывает, что описание a в файле file2.c является только описанием, но не определением.
    Если бы присутствовала инициализация a, то extern просто проигнорировалось бы, поскольку описание с инициализацией всегда считается определением. Любой объект в программе может определяться только один раз. Описываться же он может неоднократно, но все описания должны быть согласованы по типу. Например:
    // file1.c: int a = 1; int b = 1; extern int c;
    // file2.c: int a; extern double b; extern int c;
    Здесь содержится три ошибки: переменная a определена дважды ("int a;" - это определение, означающее "int a=0;"); b описано дважды, причем с разными типами; c описано дважды, но неопределено. Такие ошибки (ошибки связывания) транслятор, который обрабатывает файлы по отдельности, обнаружить не может, но большая их часть обнаруживается редактором связей.
    Следующая программа допустима в С, но не в С++:
    // file1.c: int a; int f() { return a; }
    // file2.c: int a; int g() { return f(); }
    Во-первых, ошибкой является вызов f() в file2.c, поскольку в этом файле f() не описана. Во-вторых, файлы программы не могут быть правильно связаны, поскольку a определено дважды.
    Если имя описано как static, оно становится локальном в этом файле. Например:
    // file1.c: static int a = 6; static int f() { /* ... */ }
    // file2.c: static int a = 7; static int f() { /* ... */ }
    Приведенная программа правильна, поскольку a и f определены как статические. В каждом файле своя переменная a и функция f().
    Если переменные и функции в данной части программы описаны как static, то в этой части программы проще разобраться, поскольку не нужно заглядывать в другие части. Описывать функции как статические полезно еще и по той причине, что транслятору предоставляется возможность создать более простой вариант операции вызова функции. Если имя объекта или функции локально в данном файле, то говорят, что объект подлежит внутреннему связыванию. Обратно, если имя объекта или функции нелокально в данном файле, то он подлежит внешнему связыванию.
    Обычно говорят, что имена типов, т.е. классов и перечислений, не подлежат связыванию. Имена

    Бьерн Страуструп.
    Язык программирования С++
    99 глобальных классов и перечислений должны быть уникальными во всей программе и иметь единственное определение. Поэтому, если есть два даже идентичных определения одного класса, это - все равно ошибка:
    // file1.c: struct S { int a; char b; }; extern void f(S*);
    // file2.c: struct S { int a; char b; }; void f(S* p) { /* ... */ }
    Но будьте осторожны: опознать идентичность двух описаний класса не в состоянии большинство систем программирования С++. Такое дублирование может вызвать довольно тонкие ошибки (ведь классы в разных файлах будут считаться различными).
    Глобальные функции-подстановки подлежат внутреннему связыванию, и то же по умолчанию справедливо для констант. Синонимы типов, т.е. имена typedef, локальны в своем файле, поэтому описания в двух данных ниже файлах не противоречат друг другу:
    // file1.c: typedef int T; const int a = 7; inline T f(int i) { return i+a; }
    // file2.c: typedef void T; const int a = 8; inline T f(double d) { cout<Константа может получить внешнее связывание только с помощью явного описания:
    // file3.c: extern const int a; const int a = 77;
    // file4.c: extern const int a; void g() { cout<В этом примере g() напечатает 77.
    4.3 Заголовочные файлы
    Типы одного объекта или функции должны быть согласованы во всех их описаниях. Должен быть согласован по типам и входной текст, обрабатываемый транслятором, и связываемые части программы. Есть простой, хотя и несовершенный, способ добиться согласованности описаний в различных файлах. Это: включить во входные файлы, содержащие операторы и определения данных, заголовочные файлы, которые содержат интерфейсную информацию.
    Средством включения текстов служит макрокоманда #include, которая позволяет собрать в один файл
    (единицу трансляции) несколько исходных файлов программы. Команда
    #include "включаемый-файл" заменяет строку, в которой она была задана, на содержимое файла включаемый-файл. Естественно, это содержимое должно быть текстом на С++, поскольку его будет читать транслятор. Как правило, операция включения реализуется отдельной программой, называемой препроцессором С++. Она вызывается системой программирования перед собственно трансляцией для обработки таких команд во входном тексте. Возможно и другое решение: часть транслятора, непосредственно работающая с входным текстом, обрабатывает команды включения файлов по мере их появления в тексте. В той системе программирования, в которой работает автор, чтобы увидеть результат команд включения файлов, нужно задать команду:

    Бьерн Страуструп.
    Язык программирования С++
    100
    CC -E file.c
    Эта команда для обработки файла file.c запускает препроцессор (и только!), подобно тому, как команда
    CC без флага -E запускает сам транслятор.
    Для включения файлов из стандартных каталогов (обычно каталоги с именем INCLUDE) надо вместо кавычек использовать угловые скобки < и >. Например:
    #include // включение из стандартного каталога
    #include "myheader.h" // включение из текущего каталога
    Включение из стандартных каталогов имеет то преимущество, что имена этих каталогов никак не связаны с конкретной программой (обычно вначале включаемые файлы ищутся в каталоге
    /usr/include/CC, а затем в /usr/include). К сожалению, в этой команде пробелы существенны:
    #include < stream.h> // не будет найден
    Было бы нелепо, если бы каждый раз перед включением файла требовалась его перетрансляция.
    Обычно включаемые файлы содержат только описания, а не операторы и определения, требующие существенной трансляторной обработки. Кроме того, система программирования может предварительно оттранслировать заголовочные файлы, если, конечно, она настолько развита, что способна сделать это, не изменяя семантики программы.
    Укажем, что может содержать заголовочный файл:
    Определения типов struct point { int x, y; };
    Шаблоны типов template 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
    Макроопределения #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 Единственный заголовочный файл
    Проще всего разбить программу на несколько файлов следующим образом: поместить определения

    Бьерн Страуструп.
    Язык программирования С++
    101 всех функций и данных в некоторое число входных файлов, а все типы, необходимые для связи между ними, описать в единственном заголовочном файле. Все входные файлы будут включать заголовочный файл. Программу калькулятора можно разбить на четыре входных файла .c: lex.c, syn.c, table.c и main.c.
    Заголовочный файл dc.h будет содержать описания каждого имени, которое используется более чем в одном .c файле:
    // dc.h: общее описание для калькулятора
    #include 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 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 может иметь такой вид:

    Бьерн Страуструп.
    Язык программирования С++
    102
    // 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() нужна практически во всех функциях программы, а сама использует только . Такая ситуация типична для функций, обрабатывающих ошибки. Следует отделить ее от файла main.c:
    // error.h: обработка ошибок extern int no_of_errors; extern double error(const char* s);
    // error.c
    #include
    #include "error.h" int no_of_errors;

    Бьерн Страуструп.
    Язык программирования С++
    103 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
    #include "table.h" const int TBLSZ = 23; name* table[TBLSZ]; name* look(const char* p, int ins) { /* ... */ }
    Заметьте, что теперь описания строковых функций берутся из включаемого файла . Тем самым удален еще один источник ошибок.
    // lex.h: описания для ввода и лексического анализа enum token_value {
    NAME, NUMBER, END,
    PLUS='+', MINUS='-', MUL='*',
    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.h: описания для синтаксического анализа и вычислений extern double expr(); extern double term(); extern double prim();
    // syn.c: определения для синтаксического анализа и вычислений
    #include "error.h"
    #include "lex.h"

    Бьерн Страуструп.
    Язык программирования С++
    104
    #include "syn.h" double prim() { /* ... */ } double term() { /* ... */ } double expr() { /* ... */ }
    Как обычно, определение основной программы тривиально:
    // main.c: основная программа
    #include
    #include "error.h"
    #include "lex.h"
    #include "syn.h"
    #include "table.h" int main(int argc, char* argv[]) { /* ... */ }
    Какое число заголовочных файлов следует использовать для данной программы зависит от многих факторов. Большинство их определяется способом обработки файлов именно в вашей системе, а не собственно в С++. Например, если ваш редактор не может работать одновременно с несколькими файлами, диалоговая обработка нескольких заголовочных файлов затрудняется. Другой пример: может оказаться, что открытие и чтение 10 файлов по 50 строк каждый занимает существенно больше времени, чем открытие и чтение одного файла из 500 строк. В результате придется хорошенько подумать, прежде чем разбивать небольшую программу, используя множественные заголовочные файлы. Предостережение: обычно можно управиться с множеством, состоящим примерно из 10 заголовочных файлов (плюс стандартные заголовочные файлы). Если же вы будете разбивать программу на минимальные логические единицы с заголовочными файлами (например, создавая для каждой структуры свой заголовочный файл), то можете очень легко получить неуправляемое множество из сотен заголовочных файлов.
    1   ...   5   6   7   8   9   10   11   12   ...   35


    написать администратору сайта