С.Б. Липпман, Ж. Лажойе - Язык программирования С++ Вводный курс (1114944), страница 62
Текст из файла (страница 62)
Например:}type_specifier object_name;Определение объекта имеет две формы:type_specifier object_name = initializer;Вот, например, определение obj1. Здесь obj1 инициализируется значением 97:int obj1 = 97;Следующая инструкция задает obj2, хотя начальное значение не задано:int obj2;Объект, определенный в глобальной области видимости без явной инициализации,гарантированно получит нулевое значение. Таким образом, в следующих двух примерахint var1 = 0;и var1, и var2 будут равны нулю:int var2;Глобальный объект можно определить в программе только один раз.
Поскольку ондолжен быть объявлен в исходном файле перед использованием, то для программы,состоящей из нескольких файлов, необходима возможность объявить объект, неопределяя его. Как это сделать?С помощью ключевого слова extern, аналогичного объявлению функции: оно указывает,что объект определен в другом месте – в этом же исходном файле или в другом.Например:extern int i;Эта инструкция “обещает”, что в программе имеется определение, подобноеint i;376С++ для начинающихextern-объявление не выделяет места под объект. Оно может встретиться несколько раз водном и том же исходном файле или в разных файлах одной программы. Однако обычнонаходится в общедоступном заголовочном файле, который включается в те модули, где// заголовочный файлextern int obj1;extern int obj2;// исходный файлint obj1 = 97;необходимо использовать глобальный объект:int obj2;Объявление глобального объекта с указанием ключевого слова extern и с явнойинициализацией считается определением.
Под этот объект выделяется память, и другиеextern const double pi = 3.1416; // определениеопределения не допускаются:const double pi; // ошибка: повторное определение piКлючевое слово extern может быть указано и при объявлении функции – для явногообозначения его подразумеваемого смысла: “определено в другом месте”. Например:extern void putValues( int*, int );8.2.2. Сопоставление объявлений в разных файлахОдна из проблем, вытекающих из возможности объявлять объект или функцию в разныхфайлах, – вероятность несоответствия объявлений или их расхождения в связи смодификацией программы.
В С++ имеются средства, помогающие обнаружить такиеразличия.Предположим, что в файле token.C функция addToken() определена как имеющая одинпараметр типа unsigned char. В файле lex.C, где эта функция вызывается, в ее// ---- в файле token.C ---int addToken( unsigned char tok ) { /* ... */ }// ---- в файле lex.C ----определении указан параметр типа char.extern int addToken( char );Вызов addToken() в файле lex.C вызывает ошибку во время связывания программы.Если бы такое связывание прошло успешно, можно представить дальнейшее развитиесобытий: скомпилированная программа была протестирована на рабочей станции SunSparc, а затем перенесена на IBM 390. Первый же запуск потерпел неудачу: даже самыепростые тесты не проходили.
Что случилось?Вот часть объявлений набора лексем:377С++ для начинающихconst unsigned char INLINE = 128;const unsigned char VIRTUAL = 129;curTok = INLINE;// ...Вызов addToken() выглядит так:addToken( curTok );Тип char реализован как знаковый в одном случае и как беззнаковый в другом. Неверноеобъявление addToken() приводит к переполнению на той машине, где тип char являетсязнаковым, всякий раз, когда используется лексема со значением больше 127. Если бытакой программный код компилировался и связывался без ошибки, во время выполнениямогли обнаружиться серьезные последствия.В С++ информация о количестве и типах параметров функций помещается в имяфункции – это называется безопасным связыванием (type-safe linkage).
Оно помогаетобнаружить расхождения в объявлениях функций в разных файлах. Поскольку типыпараметров unsigned char и char различны, в соответствии с принципом безопасногосвязывания функция addToken(), объявленная в файле lex.C, будет считатьсянеизвестной. Согласно стандарту определение в файле token.C задает другую функцию.Подобный механизм обеспечивает некоторую степень проверки типов при вызовефункций из разных файлов. Безопасное связывание также необходимо для поддержкиперегруженных функций. (Мы продолжим рассмотрение этой проблемы в главе 9.)Прочие типы несоответствия объявлений одного и того же объекта или функции в разныхфайлах не обнаруживаются во время компиляции или связывания.
Посколькукомпилятор обрабатывает отдельно каждый файл, он не способен сравнить типы вразных файлах. Несоответствия могут быть источником серьезных ошибок,проявляющихся, подобно приведенным ниже, только во время выполнения программы (к// в token. Cunsigned char lastTok = 0;unsigned char peekTok() { /* ... */ }// в lex.Cextern char lastTok;примеру, путем возбуждения исключения или из-за вывода неправильной информации).extern char peekTok();Избежать подобных неточностей поможет прежде всего правильное использованиезаголовочных файлов. Мы поговорим об этом в следующем подразделе.8.2.3. Несколько слов о заголовочных файлахЗаголовочный файл предоставляет место для всех extern-объявлений объектов,объявлений функций и определений встроенных функций. Это называется локализацией378С++ для начинающихобъявлений.
Те исходные файлы, где объект или функция определяется или используется,должны включать заголовочный файл.Такие файлы позволяют добиться двух целей. Во-первых, гарантируется, что всеисходные файлы содержат одно и то же объявление для глобального объекта илифункции. Во-вторых, при необходимости изменить объявление это изменение делается водном месте, что исключает возможность забыть внести правку в какой-то из исходныхфайлов.// ----- token.h ----typedef unsigned char uchar;const uchar INLINE = 128;// ...const uchar IT = ...;const uchar GT = ...;extern uchar lastTok;extern int addToken( uchar );inline bool is_relational( uchar tok ){ return (tok >= LT && tok <= GT); }// ----- lex.C ----#include "token.h"// ...// ----- token.C ----#include "token.h"Пример с addToken() имеет следующий заголовочный файл:// ...При проектировании заголовочных файлов нужно учитывать несколько моментов.
Всеобъявления такого файла должны быть логически связанными. Если он слишком великили содержит слишком много не связанных друг с другом элементов, программисты нестанут включать его, экономя на времени компиляции. Для уменьшения временныхзатратвнекоторыхреализацияхС++предусматриваетсяиспользованиепредкомпилированных заголовочных файлов. В руководстве к компилятору сказано, каксоздать такой файл из обычного. Если в вашей программе используются большиезаголовочные файлы, применение предкомпиляции может значительно сократить времяобработки.Чтобы это стало возможным, заголовочный файл не должен содержать объявленийвстроенных (inline) функций и объектов.
Любая из следующих инструкций являетсяextern int ival = 10;double fica_rate;определением и, следовательно, не может быть использована в заголовочном файле:extern void dummy () {}Хотя переменная i объявлена с ключевым словом extern, явная инициализацияпревращает ее объявление в определение. Точно так же и функция dummy(), несмотря наявное объявление как extern, определяется здесь же: пустые фигурные скобки содержатее тело. Переменная fica_rate определяется и без явной инициализации: об этом379С++ для начинающих380говорит отсутствие ключевого слова extern.
Включение такого заголовочного файла вдва или более исходных файла одной программы вызовет ошибку связывания –повторные определения объектов.В файле token.h, приведенном выше, константа INLINE и встроенная функцияis_relational() кажутся нарушающими правило. Однако это не так.Определения символических констант и встроенных функций являются специальнымивидами определений: те и другие могут появиться в программе несколько раз.При возможности компилятор заменяет имя символической константы ее значением.Этот процесс называют подстановкой константы.
Например, компилятор подставит 128вместо INLINE везде, где это имя встретится в исходном файле. Для того чтобыкомпилятор произвел такую замену, определение константы (значение, которым онаинициализирована) должно быть видимо в том месте, где она используется. Определениесимволической константы может появиться несколько раз в разных файлах, потому что врезультирующем исполняемом файле благодаря подстановке оно будет только одно.В некоторых случаях, однако, такая подстановка невозможна. Тогда лучше вынестиинициализацию константы в отдельный исходный файл.
Это делается с помощью явного// ----- заголовочный файл ----const int buf_chunk = 1024;extern char *const bufp;// ----- исходный файл -----объявления константы как extern. Например:char *const bufp = new char[buf_chunk];Хотя bufp объявлена как const, ее значение не может быть вычислено во времякомпиляции (она инициализируется с помощью оператора new, который требует вызовабиблиотечной функции). Такая конструкция в заголовочном файле означала бы, чтоконстанта определяется каждый раз, когда этот заголовочный файл включается.Символическая константа – это любой объект, объявленный со спецификатором const.Можете ли вы сказать, почему следующее объявление, помещенное в заголовочный файл,// ошибка: не должно быть в заголовочном файлевызывает ошибку связывания, если такой файл включается в два различных исходных?const char* msg = "?? oops: error: ";Проблема вызвана тем, что msg не константа.
Это неконстантный указатель, адресующийконстанту. Правильное объявление выглядит так (полное описание объявленийуказателей см. в главе 3):const char *constmsg = "?? oops: error: ";Такое определение может появиться в разных файлах.Схожая ситуация наблюдается и со встроенными функциями. Для того чтобыкомпилятор мог подставить тело функции “по месту”, он должен видеть ее определение.(Встроенные функции были представлены в разделе 7.6.)С++ для начинающихСледовательно, встроенная функция, необходимая в нескольких исходных файлах,должна быть определена в заголовочном файле.