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

  • 8.2.2. Сопоставление объявлений в разных файлах

  • 8.2.3. Несколько слов о заголовочных файлах

  • 8.3.1. Автоматические объекты

  • 8.3.2. Регистровые автоматические объекты

  • 8.3.3. Статические локальные объекты

  • Язык программирования C++. Вводный курс. С для начинающих


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница35 из 93
    1   ...   31   32   33   34   35   36   37   38   ...   93
    377
    extern
    -объявление не выделяет места под объект. Оно может встретиться несколько раз в одном и том же исходном файле или в разных файлах одной программы. Однако обычно находится в общедоступном заголовочном файле, который включается в те модули, где необходимо использовать глобальный объект: int obj2;
    Объявление глобального объекта с указанием ключевого слова extern и с явной инициализацией считается определением. Под этот объект выделяется память, и другие определения не допускаются: const double pi; // ошибка: повторное определение pi
    Ключевое слово extern может быть указано и при объявлении функции – для явного обозначения его подразумеваемого смысла: “определено в другом месте”. Например: extern void putValues( int*, int );
    8.2.2.
    Сопоставление объявлений в разных файлах
    Одна из проблем, вытекающих из возможности объявлять объект или функцию в разных файлах, – вероятность несоответствия объявлений или их расхождения в связи с модификацией программы. В С++ имеются средства, помогающие обнаружить такие различия.
    Предположим, что в файле token.C функция addToken() определена как имеющая один параметр типа unsigned char. В файле lex.C, где эта функция вызывается, в ее определении указан параметр типа char. extern int addToken( char );
    Вызов addToken() в файле lex.C вызывает ошибку во время связывания программы.
    Если бы такое связывание прошло успешно, можно представить дальнейшее развитие событий: скомпилированная программа была протестирована на рабочей станции Sun
    Sparc, а затем перенесена на IBM 390. Первый же запуск потерпел неудачу: даже самые простые тесты не проходили. Что случилось?
    Вот часть объявлений набора лексем:
    // заголовочный файл extern int obj1; extern int obj2;
    // исходный файл int obj1 = 97; extern const double pi = 3.1416; // определение
    // ---- в файле token.C ---- int addToken( unsigned char tok ) { /* ... */ }
    // ---- в файле lex.C ----

    С++ для начинающих
    378
    const unsigned char VIRTUAL = 129;
    Вызов addToken() выглядит так: addToken( curTok );
    Тип char реализован как знаковый в одном случае и как беззнаковый в другом. Неверное объявление addToken() приводит к переполнению на той машине, где тип char является знаковым, всякий раз, когда используется лексема со значением больше 127. Если бы такой программный код компилировался и связывался без ошибки, во время выполнения могли обнаружиться серьезные последствия.
    В С++ информация о количестве и типах параметров функций помещается в имя функции – это называется безопасным связыванием (type-safe linkage). Оно помогает обнаружить расхождения в объявлениях функций в разных файлах. Поскольку типы параметров unsigned char и char различны, в соответствии с принципом безопасного связывания функция addToken(), объявленная в файле lex.C, будет считаться неизвестной. Согласно стандарту определение в файле token.C задает другую функцию.
    Подобный механизм обеспечивает некоторую степень проверки типов при вызове функций из разных файлов. Безопасное связывание также необходимо для поддержки перегруженных функций. (Мы продолжим рассмотрение этой проблемы в главе 9.)
    Прочие типы несоответствия объявлений одного и того же объекта или функции в разных файлах не обнаруживаются во время компиляции или связывания. Поскольку компилятор обрабатывает отдельно каждый файл, он не способен сравнить типы в разных файлах. Несоответствия могут быть источником серьезных ошибок, проявляющихся, подобно приведенным ниже, только во время выполнения программы (к примеру, путем возбуждения исключения или из-за вывода неправильной информации). extern char peekTok();
    Избежать подобных неточностей поможет прежде всего правильное использование заголовочных файлов. Мы поговорим об этом в следующем подразделе.
    8.2.3.
    Несколько слов о заголовочных файлах
    Заголовочный файл предоставляет место для всех extern-объявлений объектов, объявлений функций и определений встроенных функций. Это называется локализацией const unsigned char INLINE = 128; curTok = INLINE;
    // ...
    // в token. C unsigned char lastTok = 0; unsigned char peekTok() { /* ... */ }
    // в lex.C extern char lastTok;

    С++ для начинающих
    379
    объявлений. Те исходные файлы, где объект или функция определяется или используется, должны включать заголовочный файл.
    Такие файлы позволяют добиться двух целей. Во-первых, гарантируется, что все исходные файлы содержат одно и то же объявление для глобального объекта или функции. Во-вторых, при необходимости изменить объявление это изменение делается в одном месте, что исключает возможность забыть внести правку в какой-то из исходных файлов.
    Пример с addToken() имеет следующий заголовочный файл:
    // ...
    При проектировании заголовочных файлов нужно учитывать несколько моментов. Все объявления такого файла должны быть логически связанными. Если он слишком велик или содержит слишком много не связанных друг с другом элементов, программисты не станут включать его, экономя на времени компиляции. Для уменьшения временных затрат в некоторых реализациях
    С++ предусматривается использование
    предкомпилированных заголовочных файлов. В руководстве к компилятору сказано, как создать такой файл из обычного. Если в вашей программе используются большие заголовочные файлы, применение предкомпиляции может значительно сократить время обработки.
    Чтобы это стало возможным, заголовочный файл не должен содержать объявлений встроенных (inline) функций и объектов. Любая из следующих инструкций является определением и, следовательно, не может быть использована в заголовочном файле: extern void dummy () {}
    Хотя переменная i объявлена с ключевым словом extern, явная инициализация превращает ее объявление в определение. Точно так же и функция dummy(), несмотря на явное объявление как extern, определяется здесь же: пустые фигурные скобки содержат ее тело. Переменная fica_rate определяется и без явной инициализации: об этом
    // ----- 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" extern int ival = 10; double fica_rate;

    С++ для начинающих
    380
    говорит отсутствие ключевого слова extern. Включение такого заголовочного файла в два или более исходных файла одной программы вызовет ошибку связывания – повторные определения объектов.
    В файле token.h, приведенном выше, константа INLINE и встроенная функция is_relational()
    кажутся нарушающими правило. Однако это не так.
    Определения символических констант и встроенных функций являются специальными видами определений: те и другие могут появиться в программе несколько раз.
    При возможности компилятор заменяет имя символической константы ее значением.
    Этот процесс называют подстановкой константы. Например, компилятор подставит 128 вместо INLINE везде, где это имя встретится в исходном файле. Для того чтобы компилятор произвел такую замену, определение константы (значение, которым она инициализирована) должно быть видимо в том месте, где она используется. Определение символической константы может появиться несколько раз в разных файлах, потому что в результирующем исполняемом файле благодаря подстановке оно будет только одно.
    В некоторых случаях, однако, такая подстановка невозможна. Тогда лучше вынести инициализацию константы в отдельный исходный файл. Это делается с помощью явного объявления константы как extern. Например: char *const bufp = new char[buf_chunk];
    Хотя bufp объявлена как const, ее значение не может быть вычислено во время компиляции (она инициализируется с помощью оператора new, который требует вызова библиотечной функции). Такая конструкция в заголовочном файле означала бы, что константа определяется каждый раз, когда этот заголовочный файл включается.
    Символическая константа – это любой объект, объявленный со спецификатором const.
    Можете ли вы сказать, почему следующее объявление, помещенное в заголовочный файл, вызывает ошибку связывания, если такой файл включается в два различных исходных? const char* msg = "?? oops: error: ";
    Проблема вызвана тем, что msg не константа. Это неконстантный указатель, адресующий константу. Правильное объявление выглядит так (полное описание объявлений указателей см. в главе 3): const char *const msg = "?? oops: error: ";
    Такое определение может появиться в разных файлах.
    Схожая ситуация наблюдается и со встроенными функциями. Для того чтобы компилятор мог подставить тело функции “по месту”, он должен видеть ее определение.
    (Встроенные функции были представлены в разделе 7.6.)
    // ----- заголовочный файл ----- const int buf_chunk = 1024; extern char *const bufp;
    // ----- исходный файл -----
    // ошибка: не должно быть в заголовочном файле

    С++ для начинающих
    381
    Следовательно, встроенная функция, необходимая в нескольких исходных файлах, должна быть определена в заголовочном файле. Однако спецификация inline – только
    “совет” компилятору. Будет ли функция встроенной везде или только в данном конкретном месте, зависит от множества обстоятельств. Если компилятор пренебрегает спецификацией inline, он генерирует определение функции в исполняемом файле. Если такое определение появится в данном файле больше одного раза, это будет означать ненужную трату памяти.
    Большинство компиляторов выдают предупреждение в любом из следующих случаев
    (обычно это требует включения режима выдачи предупреждений):

    само определение функции не позволяет встроить ее. Например, она слишком сложна. В таком случае попробуйте переписать функцию или уберите спецификацию inline и поместите определение функции в исходный файл;

    конкретный вызов функции может не быть “подставлен по месту”. Например, в оригинальной реализации С++ компании AT&T (cfront) такая подстановка невозможна для второго вызова в пределах одного и того же выражения. В такой ситуации выражение следует переписать, разделив вызовы встроенных функций.
    Перед тем как употребить спецификацию inline, изучите поведение функции во время выполнения. Убедитесь, что ее действительно можно встроить. Мы не рекомендуем объявлять функции встроенными и помещать их определения в заголовочный файл, если они не могут быть таковыми по своей природе.
    Упражнение 8.3
    Установите, какие из приведенных ниже инструкций являются объявлениями, а какие – определениями, и почему:
    (e) void print( const matrix & );
    Упражнение 8.4
    Какие из приведенных ниже объявлений и определений вы поместили бы в заголовочный файл? В исходный файл? Почему?
    (e) extern int total = 255;
    8.3.
    Локальные объекты
    Объявление переменной в локальной области видимости вводит локальный объект.
    Существует три вида таких объектов: автоматические, регистровые и статические, различающиеся временем жизни и характеристиками занимаемой памяти.
    (a) extern int ix = 1024;
    (b) int iy;
    (c) extern void reset( void *p ) { /* ... */ }
    (d) extern const int *pi;
    (a) int var;
    (b) inline bool is_equal( const SmallInt &, const SmallInt & ){ }
    (c) void putValues( int *arr, int size );
    (d) const double pi = 3.1416;

    С++ для начинающих
    382
    Автоматический объект существует с момента активизации функции, в которой он определен, до выхода из нее. Регистровый объект – это автоматический объект, для которого поддерживается быстрое считывание и запись его значения. Локальный статический объект располагается в области памяти, существующей на протяжении всего времени выполнения программы. В этом разделе мы рассмотрим свойства всех этих объектов.
    8.3.1.
    Автоматические объекты
    Автоматический объект размещается в памяти во время вызова функции, в которой он определен. Память для него отводится из программного стека в записи активации функции. Говорят, что такие объекты имеют автоматическую продолжительность
    хранения, или
    автоматическую
    протяженность.
    Неинициализированный автоматический объект содержит случайное, или неопределенное, значение, оставшееся от предыдущего использования области памяти. После завершения функции ее запись активации выталкивается из программного стека, т.е. память, ассоциированная с локальным объектом, освобождается. Время жизни такого объекта заканчивается с завершением работы функции, и его значение теряется.
    Поскольку память, отведенная локальному объекту, освобождается при завершении работы функции, адрес автоматического объекта следует использовать с осторожностью.
    Например, этот адрес не может быть возвращаемым значением, так как после выполнения функции будет относиться к несуществующему объекту:
    } mainResult получает значение адреса автоматического объекта res. К несчастью, память, отведенная под res, освобождается по завершении функции trouble(). После возврата в main() mainResult указывает на область памяти, не отведенную никакому объекту. (В данном примере эта область все еще может содержать правильное значение, поскольку мы не вызывали других функций после trouble() и запись ее активации, вероятно, еще не затерта.) Подобные ошибки обнаружить весьма трудно. Дальнейшее использование mainResult в программе скорее всего даст неверные результаты.
    Передача в функцию trouble() адреса m1 автоматического объекта функции main() безопасна. Память, отведенная main(), во время вызова trouble()находится в стеке, так что m1 остается доступной внутри trouble().
    #include "Matrix.h"
    Matrix* trouble( Matrix *pm )
    {
    Matrix res;
    // какие-то действия
    // результат присвоим res return &res; // плохо!
    } int main()
    {
    Matrix m1;
    // ...
    Matrix *mainResult = trouble( &m1 );
    // ...

    С++ для начинающих
    383
    Если адрес автоматического объекта сохраняется в указателе, время жизни которого больше, чем самого объекта, такой указатель называют висячим. Работа с ним – это серьезная ошибка, поскольку содержимое адресуемой области памяти непредсказуемо.
    Если комбинация бит по этому адресу оказывается в какой-то степени допустимой (не приводит к нарушению защиты памяти), то программа будет выполняться, но результаты ее будут неправильными.
    8.3.2.
    Регистровые автоматические объекты
    Автоматические объекты, интенсивно используемые в функции, можно объявить с ключевым словом register, тогда компилятор будет их загружать в машинные регистры. Если же это невозможно, объекты останутся в основной памяти. Индексы массивов и указатели, встречающиеся в циклах, – хорошие кандидаты в регистровые объекты. for ( register int *p = array ; p < arraySize; ++p ) // ...
    Параметры также можно объявлять как регистровые переменные:
    }
    Их активное использование может заметно увеличить скорость выполнения функции.
    Указание ключевого слова register – только подсказка компилятору. Некоторые компиляторы игнорируют такой запрос, применяя специальные алгоритмы для определения наиболее подходящих кандидатов на размещение в свободных регистрах.
    Поскольку компилятор учитывает архитектуру машины, на которой будет выполняться программа, он зачастую может принять более обоснованное решение об использовании машинных регистров.
    8.3.3.
    Статические локальные объекты
    Внутри функции или составной инструкции можно объявить объект с локальной областью видимости, который, однако, будет существовать в течение всего времени выполнения программы. Если значение локального объекта должно сохраняться между вызовами функции, то обычный автоматический объект не подойдет: ведь его значение теряется каждый раз после выхода.
    В таком случае локальный объект необходимо объявить как static (со статической
    продолжительностью хранения). Хотя значение такого объекта сохраняется между вызовами функции, в которой он определен, видимость его имени ограничена локальной областью. Статический локальный объект инициализируется во время первого for ( register int ix =0; ix < sz; ++-ix ) // ... bool find( register int *pm, int Val ) { while ( *pm ) if ( *pm++ == Val ) return true; return false;

    С++ для начинающих
    384
    выполнения инструкции, где он объявлен. Вот, например, версия функции gcd()
    ,устанавливающая глубину рекурсии с его помощью:
    }
    Значение, ассоциированное со статическим локальным объектом depth, сохраняется между вызовами traceGcd(). Его инициализация выполняется только один раз – когда к этой функции обращаются впервые. В следующей программе используется traceGcd():
    }
    Результат работы программы: глубина #1 глубина #2 глубина #3 глубина #4
    НОД (15,123): 3
    Неинициализированные статические локальные объекты получают значение 0. А автоматические объекты в подобной ситуации получают случайные значения.
    Следующая программа иллюстрирует разницу инициализации по умолчанию для автоматических и статических объектов и опасность, подстерегающую программиста в случае ее отсутствия для автоматических объектов.
    #include int traceGcd( int vl, int v2 )
    { static int depth = 1; cout << "
    глубина #" << depth++ << endl; if ( v2 == 0 ) { depth = 1; return vl;
    } return traceGcd( v2, vl%v2 );
    #include extern int traceGcd(int, int); int main() { int rslt = traceCcd( 15, 123 ); cout << "
    НОД (15,123): " << rslt << endl; return 0;

    С++ для начинающих
    385
    }
    Вот результат работы программы: valuel: 0 value2: 74924 sum: 74924 valuel: 0 value2: 68748 sum: 68748 valuel: 0 value2: 68756 sum: 68756 valuel: 148620 value2: 2350 sum: 150970 valuel: 2147479844 value2: 671088640 sum: -1476398812 valuel: 0 value2: 68756 sum: 68756 value1
    и value2 – неинициализированные автоматические объекты. Их начальные значения, как можно видеть из приведенной распечатки, оказываются случайными, и потому результаты сложения непредсказуемы. Объект depth, несмотря на отсутствие явной инициализации, гарантированно получает значение 0, и функция func() рекурсивно вызывает сама себя только дважды.
    8.4.
    Динамически размещаемые объекты
    Время жизни глобальных и локальных объектов четко определено. Программист неспособен хоть как-то изменить его. Однако иногда необходимо иметь объекты, временем жизни которых можно управлять. Выделение памяти под них и ее освобождение зависят от действий выполняющейся программы. Например, можно отвести память под текст сообщения об ошибке только в том случае, если ошибка действительно имела место. Если программа выдает несколько таких сообщений, размер выделяемой строки будет разным в зависимости от длины текста, т.е. подчиняется типу ошибки, произошедшей во время исполнения программы.
    Третий вид объектов позволяет программисту полностью управлять выделением и освобождением памяти. Такие объекты называют динамически размещаемыми или, для краткости, просто динамическими. Динамический объект “живет” в пуле свободной памяти, называемой хипом. Программист создает его с помощью оператора new, а уничтожает с помощью оператора delete. Динамически размещаться может как единичный объект, так и массив объектов. Размер массива, размещаемого в хипе, разрешается задавать во время выполнения.
    #include const int iterations = 2; void func() { int value1, value2; // не инициализированы static int depth; // неявно инициализирован нулем if ( depth < iterations )
    { ++depth; func(); } else depth = 0; cout << "\nvaluel:\t" << value1; cout << "\tvalue2:\t" << value2; cout << "\tsum:\t" << value1 + value2;
    } int main() { for ( int ix = 0; ix < iterations; ++ix ) func(); return 0;

    С++ для начинающих
    1   ...   31   32   33   34   35   36   37   38   ...   93


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