8. Область видимости и время жизниВ этой главе обсуждаются два важных вопроса, касающиеся объявлений в С++. Где употребляется объявленное имя? Когда можно безопасно использовать объект или вызывать функцию, т.е. каково время жизни сущности в программе? Для ответа на первый вопрос мы введем понятие областей видимости и покажем, как они ограничивают применение имен в исходном файле программы. Мы рассмотрим разные типы таких областей: глобальную и локальную, а также более сложное понятие областей видимости пространств имен, которое появится в конце главы. Отвечая на второй вопрос, мы опишем, как объявления вводят глобальные объекты и функции (сущности, “живущие” в течение всего времени работы программы), локальные (“живущие” на определенном отрезке выполнения) и динамически размещаемые объекты (временем жизни которых управляет программист). Мы также исследуем свойства времени выполнения, характерные для этих объектов и функций. 8.1. Область видимостиКаждое имя в С++ программе должно относиться к уникальной сущности (объекту,
функции, типу или шаблону). Это не значит, что оно встречается только один раз
во всей программе: его можно повторно использовать для обозначения другой сущности,
если только есть некоторый контекст, помогающий различить разные значения
одного и того же имени. Контекстом, служащим для такого различения, служит область
видимости. В С++ поддерживается три их типа: локальная область
видимости, область видимости пространства имен и область видимости
класса. #include <iostream> #include <string> // сравниваем s1 и s2 лексикографически int lexicoCompare( const string &sl, const string &s2 ) { ... } // сравниваем длины s1 и s2 int sizeCompare( const string &sl, const string &s2 ) { ... } typedef int ( PFI)( const string &, const string & ); // сортируем массив строк void sort( string *s1, string *s2, PFI compare =lexicoCompare ) { ... } string sl[10] = { "a", "light", "drizzle", "was", "falling", "when", "they", "left", "the", "school" }; int main() { // вызов sort() со значением по умолчанию параметра compare // s1 - глобальный массив sort( s1, s1 + sizeof(s1)/sizeof(s1[0]) - 1 ); // выводим результат сортировки for ( int i = 0; i < sizeof(s1) / sizeof(s1[0]); ++i ) cout << s1[ i ].c_str() << "\n\t"; } Поскольку определения функций lexicoCompare(), sizeCompare() и sort() представляют
собой различные области видимости и все они отличны от глобальной, в каждой
из этих областей можно завести переменную с именем s1. void s1(); // ошибка: повторное объявление s1 Перегруженные функции являются исключением из правила: можно завести несколько
одноименных функций в одной области видимости, если они отличаются списком параметров.
(Перегруженные функции рассматриваются в главе 9.) 8.1.1. Локальная область видимостиЛокальная область видимости – это часть исходного текста программы, содержащаяся в определении функции (или блоке внутри тела функции). Все функции имеют свои локальные области видимости. Каждая составная инструкция (или блок) внутри функции также представляет собой отдельную локальную область. Такие области могут быть вложенными. Например, следующее определение функции содержит два их уровня (функция выполняет двоичный поиск в отсортированном векторе целых чисел): const int notFound = -1; // глобальная область видимости Первая локальная область видимости – тело функции binSearch(). В ней объявлены
параметры функции vec и val, а также переменные low и high. Цикл while внутри
функции задает вложенную локальную область, в которой определена одна переменная
mid. Параметры vec и val и переменные low и high видны во вложенной области.
Глобальная область видимости включает в себя обе локальных. В ней определена
одна целая константа notFound. int binSearch( const vector<int> &vec, int val ) { // локальная область видимости: уровень #1 int val; // ошибка: неверное переопределение val // ... Имена параметров употребляются как внутри тела функции binSearch(), так и внутри
вложенной области видимости цикла while. Параметры vec и val недоступны вне
тела функции binSearch(). int low; Для некоторых инструкций языка C++ разрешено объявлять переменные внутри управляющей части. Например, в цикле for переменную можно определить внутри инструкции инициализации: for ( int index = 0; index < vecSize; ++index ) { // переменная index видна только здесь if ( vec[ index ] == someValue ) break; } // ошибка: переменная index не видна if ( index != vecSize ) // элемент найден Подобные переменные видны только в локальной области самого цикла for и вложенных в него (это верно для стандарта С++, в предыдущих версиях языка поведение было иным). Компилятор рассматривает это объявление так же, как если бы оно было записано в виде: // представление компилятора { // невидимый блок int index = 0; for ( ; index < vecSize; ++index ) { // ... } } Тем самым программисту запрещается применять управляющую переменную вне локальной области видимости цикла. Если нужно проверить index, чтобы определить, было ли найдено значение, то данный фрагмент кода следует переписать так: int index = 0; for ( ; index < vecSize; ++index ) { // ... } // правильно: переменная index видна if ( index != vecSize ) // элемент найден Поскольку переменная, объявленная в инструкции инициализации цикла for, является локальной для цикла, то же самое имя допустимо использовать аналогичным образом и в других циклах, расположенных в данной локальной области видимости: void fooBar( int *ia, int sz ) { for (int i=0; i<sz; ++i) ... // правильно for (int i=0; i<sz; ++i) ... // правильно, другое i for (int i=0; i<sz; ++i) ... // правильно, другое i } Аналогично переменная может быть объявлена внутри условия инструкций if и switch, а также внутри условия циклов while и for. Например: if ( int *pi = getValue() ) { // pi != 0 -- *pi можно использовать здесь int result = calc(*pi); // ... } else { // здесь pi тоже видна // pi == 0 cout << "ошибка: getValue() завершилась неудачно" << endl; } Переменные, определенные в условии инструкции if, как переменная pi, видны только внутри if и соответствующей части else, а также во вложенных областях. Значением условия является значение этой переменной, которое она получает в результате инициализации. Если pi равна 0 (нулевой указатель), условие ложно и выполняется ветвь else. Если pi инициализируется любым другим значением, условие истинно и выполняется ветвь if. (Инструкции if, switch, for и while рассматривались в главе 5.) Упражнение 8.1Найдите различные области видимости в следующем примере. Какие объявления ошибочны и почему? int ix = 1024; int ix() ; void func( int ix, int iy ) { int ix = 255; if (int ix=0) { Упражнение 8.2К каким объявлениям относятся различные использования переменных ix и iy в следующем примере: int ix = 1024; void func( int ix, int iy ) { 8.2. Глобальные объекты и функцииОбъявление функции в глобальной области видимости вводит глобальную функцию,
а объявление переменной – глобальный объект. Глобальный объект существует
на протяжении всего времени выполнения программы. Время жизни глобального
объекта начинается с момента запуска программы и заканчивается с ее завершением. 8.2.1. Объявления и определенияКак было сказано в главе 7, объявление функции устанавливает ее имя, а также тип возвращаемого значения и список параметров. Определение функции, помимо этой информации, задает еще и тело – набор инструкций, заключенных в фигурные скобки. Функция должна быть объявлена перед вызовом. Например: // объявление функции calc() Определение объекта имеет две формы: type_specifier object_name; type_specifier object_name = initializer; Вот, например, определение obj1. Здесь obj1 инициализируется значением 97: int var1 = 0; int var2; Глобальный объект можно определить в программе только один раз. Поскольку он
должен быть объявлен в исходном файле перед использованием, то для программы,
состоящей из нескольких файлов, необходима возможность объявить объект, не определяя
его. Как это сделать? extern int i; Эта инструкция “обещает”, что в программе имеется определение, подобное int i; 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 ---- int addToken( unsigned char tok ) { /* ... */ } // ---- в файле lex.C ---- Вызов addToken() в файле lex.C вызывает ошибку во время связывания программы.
Если бы такое связывание прошло успешно, можно представить дальнейшее развитие
событий: скомпилированная программа была протестирована на рабочей станции Sun
Sparc, а затем перенесена на IBM 390. Первый же запуск потерпел неудачу: даже
самые простые тесты не проходили. Что случилось? const unsigned char INLINE = 128; const unsigned char VIRTUAL = 129; Вызов addToken() выглядит так: curTok = INLINE; // ... addToken( curTok ); Тип char реализован как знаковый в одном случае и как беззнаковый в другом.
Неверное объявление addToken() приводит к переполнению на той машине, где тип
char является знаковым, всякий раз, когда используется лексема со значением
больше 127. Если бы такой программный код компилировался и связывался без ошибки,
во время выполнения могли обнаружиться серьезные последствия. // в token. C unsigned char lastTok = 0; unsigned char peekTok() { /* ... */ } // в lex.C Избежать подобных неточностей поможет прежде всего правильное использование
заголовочных файлов. Мы поговорим об этом в следующем подразделе. 8.2.3. Несколько слов о заголовочных файлахЗаголовочный файл предоставляет место для всех extern-объявлений объектов,
объявлений функций и определений встроенных функций. Это называется локализацией
объявлений. Те исходные файлы, где объект или функция определяется или используется,
должны включать заголовочный файл. // ----- token.h ----- typedef unsigned char uchar; const uchar INLINE = 128; // ... const uchar IT = ...; const uchar GT = ...; extern uchar lastTok; При проектировании заголовочных файлов нужно учитывать несколько моментов.
Все объявления такого файла должны быть логически связанными. Если он слишком
велик или содержит слишком много не связанных друг с другом элементов, программисты
не станут включать его, экономя на времени компиляции. Для уменьшения временных
затрат в некоторых реализациях С++ предусматривается использование предкомпилированных
заголовочных файлов. В руководстве к компилятору сказано, как создать такой
файл из обычного. Если в вашей программе используются большие заголовочные файлы,
применение предкомпиляции может значительно сократить время обработки. extern int ival = 10; double fica_rate; extern void dummy () {} Хотя переменная i объявлена с ключевым словом extern, явная инициализация превращает
ее объявление в определение. Точно так же и функция dummy(), несмотря на явное
объявление как extern, определяется здесь же: пустые фигурные скобки содержат
ее тело. Переменная fica_rate определяется и без явной инициализации: об этом
говорит отсутствие ключевого слова extern. Включение такого заголовочного файла
в два или более исходных файла одной программы вызовет ошибку связывания – повторные
определения объектов. // ----- заголовочный файл ----- const int buf_chunk = 1024; extern char *const bufp; // ----- исходный файл ----- Хотя bufp объявлена как const, ее значение не может быть вычислено во время компиляции (она инициализируется с помощью оператора new, который требует вызова библиотечной функции). Такая конструкция в заголовочном файле означала бы, что константа определяется каждый раз, когда этот заголовочный файл включается. Символическая константа – это любой объект, объявленный со спецификатором const. Можете ли вы сказать, почему следующее объявление, помещенное в заголовочный файл, вызывает ошибку связывания, если такой файл включается в два различных исходных? // ошибка: не должно быть в заголовочном файле const char* msg = "?? oops: error: "; Проблема вызвана тем, что msg не константа. Это неконстантный указатель, адресующий константу. Правильное объявление выглядит так (полное описание объявлений указателей см. в главе 3): const char *const msg = "?? oops: error: "; Такое определение может появиться в разных файлах.
Перед тем как употребить спецификацию inline, изучите поведение функции во время выполнения. Убедитесь, что ее действительно можно встроить. Мы не рекомендуем объявлять функции встроенными и помещать их определения в заголовочный файл, если они не могут быть таковыми по своей природе. Упражнение 8.3Установите, какие из приведенных ниже инструкций являются объявлениями, а какие – определениями, и почему: (a) extern int ix = 1024; (b) int iy; (c) extern void reset( void *p ) { /* ... */ } (d) extern const int *pi; (e) void print( const matrix & ); Упражнение 8.4Какие из приведенных ниже объявлений и определений вы поместили бы в заголовочный файл? В исходный файл? Почему? (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; (e) extern int total = 255; 8.3. Локальные объектыОбъявление переменной в локальной области видимости вводит локальный объект. Существует три вида таких объектов: автоматические, регистровые и статические, различающиеся временем жизни и характеристиками занимаемой памяти. Автоматический объект существует с момента активизации функции, в которой он определен, до выхода из нее. Регистровый объект – это автоматический объект, для которого поддерживается быстрое считывание и запись его значения. Локальный статический объект располагается в области памяти, существующей на протяжении всего времени выполнения программы. В этом разделе мы рассмотрим свойства всех этих объектов. 8.3.1. Автоматические объектыАвтоматический объект размещается в памяти во время вызова функции, в которой
он определен. Память для него отводится из программного стека в записи активации
функции. Говорят, что такие объекты имеют автоматическую продолжительность
хранения, или автоматическую протяженность. Неинициализированный
автоматический объект содержит случайное, или неопределенное, значение, оставшееся
от предыдущего использования области памяти. После завершения функции ее запись
активации выталкивается из программного стека, т.е. память, ассоциированная
с локальным объектом, освобождается. Время жизни такого объекта заканчивается
с завершением работы функции, и его значение теряется. #include "Matrix.h" mainResult получает значение адреса автоматического объекта res. К несчастью,
память, отведенная под res, освобождается по завершении функции trouble(). После
возврата в main() mainResult указывает на область памяти, не отведенную никакому
объекту. (В данном примере эта область все еще может содержать правильное значение,
поскольку мы не вызывали других функций после trouble() и запись ее активации,
вероятно, еще не затерта.) Подобные ошибки обнаружить весьма трудно. Дальнейшее
использование mainResult в программе скорее всего даст неверные результаты. 8.3.2. Регистровые автоматические объектыАвтоматические объекты, интенсивно используемые в функции, можно объявить с ключевым словом register, тогда компилятор будет их загружать в машинные регистры. Если же это невозможно, объекты останутся в основной памяти. Индексы массивов и указатели, встречающиеся в циклах, – хорошие кандидаты в регистровые объекты. for ( register int ix =0; ix < sz; ++-ix ) // ... for ( register int *p = array ; p < arraySize; ++p ) // ... Параметры также можно объявлять как регистровые переменные: bool find( register int *pm, int Val ) { while ( *pm ) if ( *pm++ == Val ) return true; return false; } Их активное использование может заметно увеличить скорость выполнения функции. 8.3.3. Статические локальные объектыВнутри функции или составной инструкции можно объявить объект с локальной областью
видимости, который, однако, будет существовать в течение всего времени выполнения
программы. Если значение локального объекта должно сохраняться между вызовами
функции, то обычный автоматический объект не подойдет: ведь его значение теряется
каждый раз после выхода. #include <iostream> Значение, ассоциированное со статическим локальным объектом depth, сохраняется между вызовами traceGcd(). Его инициализация выполняется только один раз – когда к этой функции обращаются впервые. В следующей программе используется traceGcd(): #include <iostream> extern int traceGcd(int, int); int main() { Результат работы программы: глубина #1 глубина #2 глубина #3 глубина #4 НОД (15,123): 3 Неинициализированные статические локальные объекты получают значение 0. А автоматические объекты в подобной ситуации получают случайные значения. Следующая программа иллюстрирует разницу инициализации по умолчанию для автоматических и статических объектов и опасность, подстерегающую программиста в случае ее отсутствия для автоматических объектов. #include <iostream> Вот результат работы программы: valuel: 0 value2: 74924 sum: 74924 value1 и value2 – неинициализированные автоматические объекты. Их начальные
значения, как можно видеть из приведенной распечатки, оказываются случайными,
и потому результаты сложения непредсказуемы. Объект depth, несмотря на отсутствие
явной инициализации, гарантированно получает значение 0, и функция func() рекурсивно
вызывает сама себя только дважды. 8.4. Динамически размещаемые объектыВремя жизни глобальных и локальных объектов четко определено. Программист неспособен
хоть как-то изменить его. Однако иногда необходимо иметь объекты, временем жизни
которых можно управлять. Выделение памяти под них и ее освобождение зависят
от действий выполняющейся программы. Например, можно отвести память под текст
сообщения об ошибке только в том случае, если ошибка действительно имела место.
Если программа выдает несколько таких сообщений, размер выделяемой строки будет
разным в зависимости от длины текста, т.е. подчиняется типу ошибки, произошедшей
во время исполнения программы. 8.4.1. Динамическое создание и уничтожение единичных объектовОператор new состоит их ключевого слова new, за которым следует спецификатор типа. Этот спецификатор может относиться к встроенным типам или к типам классов. Например: new int; размещает в хипе один объект типа int. Аналогично в результате выполнения инструкции new iStack; там появится один объект класса iStack. int *pi = new int; Здесь оператор new создает один объект типа int, на который ссылается указатель
pi. Выделение памяти из хипа во время выполнения программы называется динамическим
выделением. Мы говорим, что память, адресуемая указателем pi, выделена динамически. if ( *pi == 0 ) вероятно, даст false, поскольку объект, на который указывает pi, содержит случайную последовательность битов. Следовательно, объекты, создаваемые с помощью оператора new, рекомендуется инициализировать. Программист может инициализировать объект типа int из предыдущего примера следующим образом: int *pi = new int( 0 ); Константа в скобках задает начальное значение для создаваемого объекта; теперь
pi ссылается на объект типа int, имеющий значение 0. Выражение в скобках называется
инициализатором. Это может быть любое выражение (не обязательно константа),
возвращающее значение, приводимое к типу int. int ival = 0; // создаем объект типа int и инициализируем его 0 int *pi = &ival; // указатель ссылается на этот объект не считая, конечно, того, что объект, адресуемый pi, создается библиотечной функцией new() и размещается в хипе. Аналогично iStack *ps = new iStack( 512 ); создает объект типа iStack на 512 элементов. В случае объекта класса значение
или значения в скобках передаются соответствующему конструктору, который вызывается
в случае успешного выделения памяти. (Динамическое создание объектов классов
более подробно рассматривается в разделе 15.8. Оставшаяся часть
данного раздела посвящена созданию объектов встроенных типов.) // необходимо ли это? if ( pi != 0 ) delete pi; Нет. Язык С++ гарантирует, что оператор delete не будет вызывать функцию delete()
в случае нулевого операнда. Следовательно, проверка на 0 необязательна. (Если
вы явно добавите такую проверку, в большинстве реализаций она фактически будет
выполнена дважды.) void f() { int i; string str = "dwarves"; int *pi = &i; short *ps = 0; double *pd = new doub1e(33); delete str; // плохо: str не является динамическим объектом Вот три основные ошибки, связанные с динамическим выделением памяти:
Эти ошибки при работе с динамически выделяемой памятью гораздо легче допустить,
нежели обнаружить и исправить. Для того чтобы помочь программисту, стандартная
библиотека С++ представляет класс auto_ptr. Мы рассмотрим его в следующем подразделе.
После этого мы покажем, как динамически размещать и уничтожать массивы, используя
вторую форму операторов new и delete. 8.4.2. Шаблон auto_ptr АВ стандартной библиотеке С++ auto_ptr является шаблоном класса, призванным
помочь программистам в манипулировании объектами, которые создаются посредством
оператора new. (К сожалению, подобного шаблона для манипулирования динамическими
массивами нет. Использовать auto_ptr для создания массивов нельзя, это приведет
к непредсказуемым результатам.) #include <memory> Определение объекта auto_ptr имеет три формы: auto_ptr< type_pointed_to > identifier( ptr_allocated_by_new ); auto_ptr< type_pointed_to > identifier( auto_ptr_of_same_type ); auto_ptr< type_pointed_to > identifier; Здесь type_pointed_to представляет собой тип нужного объекта. Рассмотрим последовательно каждое из этих определений. Как правило, мы хотим непосредственно инициализировать объект auto_ptr адресом объекта, созданного с помощью оператора new. Это можно сделать следующим образом: auto_ptr< int > pi ( new int( 1024 ) ); В результате значением pi является адрес созданного объекта, инициализированного числом 1024. С объектом, на который указывает auto_ptr, можно работать обычным способом: if ( *pi != 1024 ) // ошибка, что-то не так else *pi *= 2; Объект, на который указывает pi, будет автоматически уничтожен по окончании
времени жизни pi. Если указатель pi является локальным, то объект, который он
адресует, будет уничтожен при выходе из блока, где он определен. Если же pi
глобальный, то объект, на который он ссылается, уничтожается при выходе из программы. auto_ptr< string > pstr_auto( new string( "Brontosaurus" ) ); Предположим, что мы хотим выполнить какую-то операцию со строками. С обычной строкой мы бы поступили таким образом: string *pstr_type = new string( "Brontosaurus" ); if ( pstr_type->empty() ) // ошибка, что-то не так А как обратиться к операции empty(), используя объект auto_ptr? Точно так же: auto_ptr< string > pstr_auto( new string( "Brontosaurus" ) ); if ( pstr_type->empty() ) // ошибка, что-то не так Создатели шаблона класса auto_ptr не в последнюю очередь стремились сохранить
привычный синтаксис, употребляемый с обычными указателями, а также обеспечить
дополнительные возможности автоматического удаления объекта, на который ссылается
auto_ptr. При этом время выполнения не увеличивается. Применение встроенных
функций (которые подставляются по месту вызова) позволило сделать использование
объекта auto_ptr немногим более дорогим, чем непосредственное употребление указателя. // кто несет ответственность за уничтожение строки? auto_ptr< string > pstr_auto2( pstr_auto ); Представим, что мы непосредственно инициализировали один указатель на строку другим: string *pstr_type2( pstr_type ); Оба указателя теперь содержат адрес одной и той же строки, и мы должны быть
внимательными, чтобы не удалить строку дважды. auto_ptr< int > p1( new int( 1024 ) ); auto_ptr< int > p2( new int( 2048 ) ); Мы можем скопировать один объекта auto_ptr в другой с помощью этой операции: // пока не ссылается ни на какой объект auto_ptr< int > p_auto_int; Поскольку p_auto_int не инициализирован адресом какого-либо объекта, значение хранящегося внутри него указателя равно 0. Разыменование таких указателей приводит к непредсказуемому поведению программы: // ошибка: разыменование нулевого указателя if ( *p_auto_int != 1024 ) *p_auto_int = 1024; Обычный указатель можно проверить на равенство 0: int *pi = 0; if ( pi ! = 0 ) ...; А как проверить, адресует auto_ptr какой-либо объект или нет? Операция get() возвращает внутренний указатель, использующийся в объекте auto_ptr. Значит, мы должны применить следующую проверку: // проверяем, указывает ли p_auto_int на объект if ( p_auto_int.get() != 0 && *p_auto_int != 1024 ) *p_auto_int = 1024; Если auto_ptr ни на что не указывает, то как заставить его адресовать что-либо? Другими словами, как мы можем присвоить значение внутреннему указателю объекта auto_ptr? Это делается с помощью операции reset(). Например: else // хорошо, присвоим ему значение p_auto_int.reset( new int( 1024 ) ); Объекту auto_ptr нельзя присвоить адрес объекта, созданного с помощью оператора new: void example() { // инициализируется нулем по умолчанию auto_ptr< int > pi; { // не поддерживается pi = new int( 5 ) ; } } В этом случае надо использовать функцию reset(), которой можно передать указатель или 0, если мы хотим обнулить объект auto_ptr. Если auto_ptr указывает на объект и является его владельцем, то этот объект уничтожается перед присваиванием нового значения внутреннему указателю auto_ptr. Например: auto_ptr< string > pstr_auto( new string( "Brontosaurus" ) ); // "Brontosaurus" уничтожается перед присваиванием В последнем случае лучше, используя операцию assign(), присвоить новое значение существующей строке, чем уничтожать одну строку и создавать другую: // более эффективный способ присвоить новое значение // используем операцию assign() Одна из трудностей программирования состоит в том, что получить правильный
результат не всегда достаточно. Иногда накладываются и временные ограничения.
Такая мелочь, как удаление и создание заново строкового объекта, вместо использования
функции assign() при определенных обстоятельствах может вызвать значительное
замедление работы. Подобные детали не должны вас беспокоить при проектировании,
но при доводке программы на них следует обращать внимание.
Операция release() гарантирует, что несколько указателей не являются владельцами одного и того же объекта. release() не только возвращает адрес объекта, на который ссылается auto_ptr, но и передает владение им. Предыдущий фрагмент кода нужно переписать так: // правильно: оба указывают на один объект, // но pstr_auto больше не является его владельцем auto_ptr< string > pstr_auto2( pstr_auto.release() ); 8.4.3. Динамическое создание и уничтожение массивовОператор new может выделить из хипа память для размещения массива. В этом случае после спецификатора типа в квадратных скобках указывается размер массива. Он может быть задан сколь угодно сложным выражением. new возвращает указатель на первый элемент массива. Например: // создание единственного объекта типа int // с начальным значением 1024 int *pi = new int( 1024 ); // создание массива из 1024 элементов // элементы не инициализируются int *pia = new int[ 1024 ]; // создание двумерного массива из 4x1024 элементов int (*pia2)[ 1024 ] = new int[ 4 ][ 1024 ]; pi содержит адрес единственного элемента типа int, инициализированного значением
1024; pia – адрес первого элемента массива из 1024 элементов; pia2 – адрес начала
массива, содержащего четыре массива по 1024 элемента, т.е. pia2 адресует 4096
элементов. for (int index = 0; index < 1024; ++index ) pia[ index ] = 0; Основное преимущество динамического массива состоит в том, что количество элементов
в его первом измерении не обязано быть константой, т.е. может не быть известным
во время компиляции. Для массивов, определяемых в локальной или глобальной области
видимости, это не так: здесь размер задавать необходимо. const char *noerr = "success"; // ... const char *err189 = "Error: a function declaration must " "specify a function return type!"; Размер создаваемого с помощью оператора new массива может быть задан значением, вычисляемым во время выполнения: #include <cstring> const char *errorTxt; if (errorFound) Единица, прибавляемая к значению, которое возвращает strlen(), необходима для
учета завершающего нулевого символа в C-строке. Отсутствие этой единицы – весьма
распространенная ошибка, которую достаточно трудно обнаружить, поскольку она
проявляет себя косвенно: происходит затирание какой-либо другой области программы.
Почему? Большинство функций, которые обрабатывают массивы, представляющие собой
С-строки символов, пробегают по элементам, пока не встретят завершающий нуль. int getDim(); // создание двумерного массива Оператор delete для уничтожения массива имеет следующую форму: delete[] str1; Пустые квадратные скобки необходимы. Они говорят компилятору, что указатель
адресует массив, а не единичный элемент. Поскольку тип str1 – указатель на char,
без этих скобок компилятор не поймет, что удалять следует целый массив. 8.4.4. Динамическое создание и уничтожение константных объектовПрограммист способен создать объект в хипе и запретить изменение его значения после инициализации. Этого можно достичь, объявляя объект константным. Для этого применяется следующая форма оператора new: const int *pci = new const int(1024); Константный динамический объект имеет несколько особенностей. Во-первых, он
должен быть инициализирован, иначе компилятор сигнализирует об ошибке (кроме
случая, когда объект принадлежит к типу класса, имеющего конструктор по умолчанию;
в такой ситуации инициализатор можно опустить). delete pci; Хотя операнд оператора delete имеет тип указателя на const int, эта инструкция
является корректной и освобождает область памяти, на которую ссылается pci. const int *pci = new const int[100]; // ошибка 8.4.5. Оператор размещения new АСуществует третья форма оператора new, которая создает объект без отведения для него памяти, то есть в памяти, которая уже была выделена. Эту форму называют оператором размещения new. Программист указывает адрес области памяти, в которой размещается объект: new (place_address) type-specifier place_address должен быть указателем. Такая форма (она включается заголовочным файлом <new>) позволяет программисту предварительно выделить большую область памяти, которая впоследствии будет содержать различные объекты. Например: #include <iostream> Результат работы программы: Оператор new сработал! Для оператора размещения new нет парного оператора delete: он не нужен, поскольку эта форма не выделяет память. В предыдущем примере необходимо освободить память, адресуемую указателем buf, а не pb. Это происходит в конце программы, когда буфер больше не нужен. Поскольку buf ссылается на символьный массив, оператор delete имеет форму delete[] buf; При уничтожении buf прекращают существование все объекты, созданные в нем. В нашем примере pb больше не ссылается на существующий объект класса Foo. Упражнение 8.5Объясните, почему приведенные операторы new ошибочны: Упражнение 8.6Как бы вы уничтожили pa? Упражнение 8.7Какие из следующих операторов delete содержат потенциальные ошибки времени выполнения и почему: int globalObj; char buf[1000]; void f() { Упражнение 8.8Какие из данных объявлений auto_ptr неверны или грозят ошибками времени выполнения? Объясните каждый случай. int ix = 1024; int *pi = & ix; int *pi2 = new int ( 2048 ); (a) auto_ptr<int> p0(ix); (b) auto_ptr<int> pl(pi); (c) auto_ptr<int> p2(pi2); (d) auto_ptr<int> p3(&ix); (e) auto_ptr<int> p4(new int(2048)); (f) auto_ptr<int> p5(p2.get()); (9) auto_ptr<int> p6(p2.release()); (h) auto_ptr<int> p7(p2); Упражнение 8.9Объясните разницу между следующими инструкциями: Упражнение 8.10Пусть мы имеем: auto_ptr< string > ps( new string( "Daniel" ) ); В чем разница между этими двумя вызовами assign()?Какой их них предпочтительнее и почему? ps.get()->assign( "Danny" ); ps->assign( "Danny" ); 8.5. Определения пространства имен АПо умолчанию любой объект, функция, тип или шаблон, объявленный в глобальной
области видимости, также называемой областью видимости глобального пространства
имен, вводит глобальную сущность. Каждая такая сущность обязана
иметь уникальное имя. Например, функция и объект не могут быть одноименными,
даже если они объявлены в разных исходных файлах. class cplusplus_primer_matrix { ... }; void inverse( cplusplus_primer_matrix & ); Однако у этого решения есть недостаток. Программа, написанная на С++, может
содержать множество глобальных классов, функций и шаблонов, видимых в любой
точке кода. Работать со слишком длинными идентификаторами для программистов
утомительно. namespace cplusplus_primer { class matrix { /*...*/ }; void inverse ( matrix & ); } cplusplus_primer является пользовательским пространством имен (в отличие
от глобального пространства, которое неявно подразумевается и существует в любой
программе). cplusplus_primer::inverse(). Члены cplusplus_primer могут использоваться в программе с помощью спецификации имени: void func( cplusplus_primer::matrix &m ) { // ... cplusplus_primer::inverse(m); return m; } Если в другом пользовательском пространстве имен (скажем, DisneyFeatureAnimation) также существует класс matrix и функция inverse() и мы хотим использовать этот класс вместо объявленного в пространстве cplusplus_primer, то функцию func() нужно модифицировать следующим образом: void func( DisneyFeatureAnimation::matrix &m ) { // ... DisneyFeatureAnimation::inverse(m); return m; } Конечно, каждый раз указывать специфицированные имена типа namespace_name::member_name неудобно. Поэтому существуют механизмы, позволяющие облегчить использование
пространств имен в программах. Это псевдонимы пространств имен, using-объявления
и using-директивы. (Мы рассмотрим их в разделе 8.6.) 8.5.1. Определения пространства именОпределение пользовательского пространства имен начинается с ключевого слова
namespace, за которым следует идентификатор. Он должен быть уникальным в той
области видимости, в которой определяется данное пространство; наличие другой
сущности с тем же именем является ошибкой. Конечно, это не означает, что проблема
засорения глобального пространства решена полностью, но существенно помогает
в ее решении. namespace cplusplus_primer { class matrix { /* ... */ }; void inverse ( matrix & ); matrix operator+ ( const matrix &ml, const matrix &m2 ) {/* ... */ } const double pi = 3.1416; } Именем класса, объявленного в пространстве cplusplus_primer, будет cplusplus_primer::matrix Именем функции cplusplus_primer::inverse() Именем константы cplusplus_primer::pi Имя класса, функции или константы расширяется именем пространства, в котором
они объявлены. Такие имена называют квалифицированными. namespace cplusplus_primer { class matrix { /* ... */ }; const double pi = 3.1416; } namespace cplusplus_primer { Два приведенных примера эквивалентны: оба задают пространство имен cplusplus_primer,
содержащее класс matrix, функцию inverse(), константу pi и operator+(). Определение
пространства имен может состоять из нескольких соединенных частей. namespace namespace_name { задает новое пространство, если имя namespace_name не совпадает с одним из
ранее объявленных. В противном случае новые объявления добавляются в старое
пространство. // Эта часть пространства имен // определяет интерфейс библиотеки namespace cplusplus_primer { Первая часть пространства имен содержит объявления и определения, служащие
интерфейсом библиотеки: определения типов, констант, объявления функций. Во
второй части находятся детали реализации, то есть определения функций. // ---- primer.h ---- namespace cplusplus_primer { class matrix { /*... */ }; const double pi = 3.1416; matrix operator+ ( const matrix &m1, const matrix &m2 ); void inverse( matrix & ); } // ---- primer.C ---- Программа, использующая эту библиотеку, выглядит так: // ---- user.C ---- Подобная организация программы обеспечивает модульность библиотеки, необходимую для сокрытия реализации от пользователей, в то же время позволяя без ошибок скомпилировать и связать файлы primer.C и user.C в одну программу. 8.5.2. Оператор разрешения области видимостиИмя члена пользовательского пространства дополняется поставленным спереди именем этого пространства и оператором разрешения области видимости (::). Использование неквалифицированного члена, например matrix, является ошибкой. Компилятор не знает, к какому объявлению относится это имя: // определение интерфейса библиотеки #include "primer.h" // ошибка: нет объявления для matrix void func( matrix &m ); Объявление члена пространства имен скрыто в своем пространстве. Если мы не укажем компилятору, где именно искать объявление, он произведет поиск только в текущей области видимости и в областях, включающих текущую. Допустим, если переписать предыдущую программу так: // определение интерфейса библиотеки #include "primer.h" class matrix { /* пользовательское определение */ }; // правильно: глобальный тип matrix найден то определение класса matrix компилятор находит в глобальной области видимости
и программа компилируется без ошибок. Поскольку объявление matrix как члена
пространства имен cplusplus_primer скрыто в этом пространстве, оно не конфликтует
с классом, объявленным в глобальной области видимости. ::member_name относится к его элементу. Такой способ полезен для указания членов глобального
пространства, если их имена оказываются скрыты именами, объявленными во вложенных
локальных областях видимости. #include <iostream> const int max = 65000; const int lineLength = 12; void fibonacci( int max ) Так выглядит функция main(), вызывающая fibonacci(): #include <iostream> void fibonacci( int ); int main() { cout << "Числа Фибоначчи: 16\n"; fibonacci( 16 ); return 0; } Результат работы программы: Числа Фибоначчи: 16 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 8.5.3. Вложенные пространства именМы уже упоминали, что пользовательские пространства имен могут быть вложенными. Такие пространства применяются для дальнейшего структурирования кода нашей библиотеки. // ---- primer.h ---- Пространство имен cplusplus_primer содержит два вложенных: MatrixLib и AnimalLib. #include "primer.h" // да, это ужасно... Вложенное пространство имен является вложенной областью видимости внутри пространства, содержащего его. В процессе разрешения имен вложенные пространства ведут себя так же, как вложенные блоки. Когда некоторое имя употребляется в пространстве имен, поиск его объявление проводится во всех объемлющих пространствах. В следующем примере разрешение имени Type происходит в таком порядке: сначала ищем его в пространстве имен MatrixLib, затем в cplusplus_primer и наконец в глобальной области видимости: typedef double Type; Если некоторая сущность объявляется во вложенном пространстве имен, она скрывает
объявление одноименной сущности из объемлющего пространства. 8.5.4. Определение члена пространства именМы видели, что определение члена пространства имен может появиться внутри определения самого пространства. Например, класс matrix и константа pi появляются внутри вложенного пространства имен MatrixLib, а определения функций operator+() и inverse() приводятся где-то в другом месте текста программы: // ---- primer.h ---- Член пространства имен можно определить и вне соответствующего пространства. В таком случае имя члена должно быть квалифицировано именами пространств, к которым он принадлежит. Например, если определение функции operator+() помещено в глобальную область видимости, то оно должно выглядеть следующим образом: // ---- primer.C ---- #include "primer.h" // определение в глобальной области видимости Имя operator+() квалифицировано в данном случае именами пространств cplusplus_primer
и MatrixLib. Однако обратите внимание на тип matrix в списке параметров operator+():
употреблено неквалифицированное имя. Как такое может быть? cplusplus_primer::MatrixLib::operator+ В определении operator+() неквалифицированные имена могут встречаться в любом объявлении или выражении внутри списка параметров или тела функции. Например, локальное объявление внутри operator+() способно создать объект класса matrix: // ---- primer.C ---- #include "primer.h" cplusplus_primer::MatrixLib::matrix Хотя члены могут быть определены вне своего пространства имен, такие определения допустимы не в любом месте. Их разрешается помещать только в пространства, объемлющие данное. Например, определение operator+() может появиться в глобальной области видимости, в пространстве имен cplusplus_primer и в пространстве MatrixLib. В последнем случае это выглядит так: // ---- primer.C -- #include "primer.h" namespace cplusplus_primer { Член может определяться вне своего пространства только при условии, что ранее он был объявлен внутри. Последнее приведенное определение operator+() было бы ошибочным, если бы ему не предшествовало объявление в файле primer.h: namespace cplusplus_primer { namespace MatrixLib { class matrix { /*...*/ }; // следующее объявление не может быть пропущено matrix operator+ ( const matrix &ml, const matrix &m2 ); // ... } } 8.5.5. ПОО и члены пространства именКак уже было сказано, определение пространства имен может состоять из разрозненных частей и размещаться в разных файлах. Следовательно, член пространства разрешено объявлять во многих файлах. Например: // primer.h namespace cplusplus_primer { // ... void inverse( matrix & ); } // usel.C Объявление cplusplus::inverse() в primer.h ссылается на одну и ту же функцию
в обоих исходных файлах use1.C и use2.C.
// ---- primer.C ---- #include "primer.h" namespace cplusplus_primer { // определения функций Для объявления объекта без его определения используется ключевое слово extern, как и в случае такого объявления в глобальной области видимости. 8.5.6. Безымянные пространства именМожет возникнуть необходимость определить объект, функцию, класс или любую
другую сущность так, чтобы она была видимой только в небольшом участке программы.
Это еще один способ решения проблемы засорения глобального пространства имен.
Поскольку мы уверены, что эта сущность используется ограниченно, можно не тратить
время на выдумывание уникального имени. Если мы объявляем объект внутри функции
или блока, его имя видимо только в этом блоке. А как сделать некоторую сущность
доступной нескольким функциям, но не всей программе? // ----- SortLib.h ----- void quickSort( double *, double * ); void bubbleSort( double *, double * ); void mergeSort( double *, double * ); void heapSort( double *, double * ); Все они используют одну и ту же функцию swap() для того, чтобы менять местами элементы вектора. Однако она не должна быть видна во всей программе, поскольку нужна только четырем названным функциям. Локализуем ее в файле SortLib.C. Приведенный код не дает желаемого результата. Как вы думаете, почему? // ----- SortLib.C ----- void swap( double *dl, double *d2 ) { /* ... */ } // только эти функции используют swap() Хотя функция swap() определена в файле SortLib.C и не появляется в заголовочном
файле SortLib.h, где содержится описание интерфейса библиотеки сортировки, она
объявлена в глобальной области видимости. Следовательно, это имя является глобальным,
при этом сохраняется возможность конфликта с другими именами. // ----- SortLib.C ----- namespace { void swap( double *dl, double *d2 ) { /* ... */ } } // определения функций сортировки не изменяются Функция swap() видна только в файле SortLib.C. Если в другом файле в безымянном
пространстве имен содержится определение swap(), то это другая функция. Наличие
двух функций swap() не является ошибкой, поскольку они различны. Безымянные
пространства имен отличаются от прочих: определение такого пространства локально
для одного файла и не может размещаться в нескольких. void quickSort( double *d1, double *d2 ) { // ... double* elem = d1; // ... // ссылка на член безымянного пространства имен swap() swap( d1, elem ); // ... } Члены безымянного пространства имен относятся к сущностям программы. Поэтому
функция swap() может быть вызвана во время выполнения. Однако имена этих членов
видны только внутри одного файла. // SortLib.C // swap() невидима для других файлов программы static void swap( double *d1, double *d2 ) { /* ... */ } // определения функций сортировки такие же, как и раньше Во многих программах на С++ используются объявления с ключевым словом static. Предполагается, что они должны быть заменены безымянными пространствами имен по мере того, как все большее число компиляторов начнет поддерживать это понятие. Упражнение 8.11Зачем нужно определять собственное пространство имен в программе? Упражнение 8.12Имеется следующее объявление operator*(), члена вложенного пространства имен cplusplus_primer::MatrixLib: namespace cplusplus_primer { namespace MatrixLib { class matrix { /*...*/ }; matrix operator* ( const matrix &, const matrix & ); // ... } } Как определить эту функцию в глобальной области видимости? Напишите только прототип. Упражнение 8.13Объясните, зачем нужны безымянные пространства имен. 8.6. Использование членов пространства имен АИспользование квалифицированных имен при каждом обращении к членам пространств
может стать обременительным, особенно если имена пространств достаточно длинны.
Если бы удалось сделать их короче, то такие имена проще было бы читать и набивать.
Однако употребление коротких имен увеличивает риск их совпадения с другими,
поэтому желательно, чтобы в библиотеках применялись пространства с длинными
именами. 8.6.1. Псевдонимы пространства именПсевдоним пространства имен используется для задания короткого синонима имени
пространства. Например, длинное имя namespace IBM = International_Business_Machines; Объявление псевдонима начинается ключевым словом namespace, за которым следует
короткий псевдоним, а за ним – знак равенства и исходное полное имя пространства.
Если полное имя не соответствует никакому известному пространству, это ошибка. #include "primer.h" // трудно читать! Разрешается задать псевдоним для обозначения вложенного cplusplLis_primer::MatrixLib, сделав определение функции более удобным для восприятия: #include "primer.h" // более короткий псевдоним namespace mlib = cplusplus_primer::MatrixLib; // читать проще! Одно пространство имен может иметь несколько взаимозаменяемых псевдонимов. Например, если псевдоним Lib ссылается на cplusplus_primer, то определение функции func() может выглядеть и так: // псевдоним alias относится к пространству имен cplusplus_primer namespace alias = Lib; void func( cplusplus_primer::matrix &m ) { 8.6.2. Using-объявленияИмеется механизм, позволяющий обращаться к членам пространства имен, используя
их имена без квалификатора, т.е. без префикса namespace_name::. Для этого применяются
using-объявления. namespace cplusplus_primer { namespace MatrixLib { class matrix { /* ... */ }; // ... } } // using-объявление для члена matrix Using-объявление вводит имя в ту область видимости, в которой оно использовано.
Так, предыдущее using-объявление делает имя matrix глобально видимым. void func( matrix &m ); Оно вводит функцию func() с параметром типа cplusplus_primer:: MatrixLib::matrix.
Например: namespace blip { Using-объявления в функции manip() позволяют ссылаться на членов пространства
blib с помощью неквалифицированных имен. Такие объявления не видны вне manip(),
и неквалифицированные имена могут применяться только внутри этой функции. Вне
ее необходимо употреблять квалифицированные имена. 8.6.3. Using-директивыПространства имен появились в стандартном С++. Предыдущие версии С++ их не
поддерживали, и, следовательно, поставляемые библиотеки не помещали глобальные
объявления в пространства имен. Множество программ на С++ было написано еще
до того, как компиляторы стали поддерживать такую опцию. Заключая содержимое
библиотеки в пространство имен, мы можем испортить старое приложение, использующее
ее предыдущие версии: все имена из этой библиотеки становятся квалифицированными,
т.е. должны включать имя пространства вместе с оператором разрешения области
видимости. Те приложения, в которых эти имена употребляются в неквалифицированной
форме, перестают компилироваться. #include "primer.h" using cplusplus_primer::matrix; using cplusplus_primer::inverse; // using-объявления позволяют использовать Но если библиотека достаточно велика и приложение часто использует имена из
нее, то для подгонки имеющегося кода к новой библиотеке может потребоваться
много using-объявлений. Добавлять их все только для того, чтобы старый код скомпилировался
и заработал, утомительно и чревато ошибками. Решить эту проблему помогают using-директивы,
облегчающие переход на новую версию библиотеки, где впервые стали применяться
пространства имен. #include "pnmer.h" // using-директива: все члены cplusplus_primer Using-директива делает имена членов пространства имен видимыми за его пределами, в том месте, где она использована. Например, приведенная using-директива создает иллюзию того, что все члены cplusplus_primer объявлены в глобальной области видимости перед определением func(). При этом члены пространства имен не получают локальных псевдонимов, а как бы перемещаются в новую область видимости. Код namespace A { int i, j; } выглядит как int i, J; для фрагмента программы, содержащего в области видимости следующую using-директиву: namespace blip { Во-первых, using-директивы имеют область видимости. Такая директива в функции
manip() относится только к блоку этой функции. Для manip() члены пространства
имен blip выглядят так, как будто они объявлены в глобальной области видимости,
а следовательно, можно использовать их неквалифицированные имена. Вне этой функции
необходимо употреблять квалифицированные. namespace cplusplus_primer { class matrix { }; // прочие вещи ... } namespace DisneyFeatureAnimation { class matrix { }; // здесь тоже ... using namespace cplusplus_primer; Ошибки неоднозначности, вызываемые using-директивой, обнаруживаются только
в момент использования. В данном случае – при употреблении имени matrix. Такая
ошибка, найденная не сразу, может стать сюрпризом: заголовочные файлы не менялись
и никаких новых объявлений в программу добавлено не было. Ошибка появилась после
того, как мы решили воспользоваться новыми средствами из библиотеки. 8.6.4. Стандартное пространство имен stdВсе компоненты стандартной библиотеки С++ находятся в пространстве имен std.
Каждая функция, объект и шаблон класса, объявленные в стандартном заголовочном
файле, таком, как <vector> или <iostream>, принадлежат к этому пространству. #include <vector> #include <string> #include <iterator> int main() Правильно, этот фрагмент кода не компилируется, потому что члены пространства имен std должны использоваться с указанием их специфицированных имен. Для того чтобы исправить положение, мы можем выбрать один из следующих способов:
Членами пространства имен std в этом примере являются: шаблон класса istream_iterator,
стандартный входной поток cin, класс string и шаблон класса vector. using std::istream_iterator; using std::string; using std::cin; using std::vector; Но куда их поместить? Если программа состоит из большого количества файлов,
можно для удобства создать заголовочный файл, содержащий все эти using-объявления,
и включать его в исходные файлы вслед за заголовочными файлами стандартной библиотеки. Упражнение 8.14Поясните разницу между using-объявлениями и using-директивами. Упражнение 8.15Напишите все необходимые using-объявления для примера из раздела 6.14. Упражнение 8.16Возьмем следующий фрагмент кода: namespace Exercise { int ivar = 0; double dvar = 0; const int limit = 1000; } int ivar = 0; //1 Каковы будут значения объявлений и выражений, если поместить using-объявления для всех членов пространства имен Exercise в точку //1? В точку //2? А если вместо using-объявлений использовать using-директиву? Назад ВпередСодержание |
2012-08-28 14:27:16 Илья ch8.2.3 "...Чтобы это стало возможным, заголовочный файл не должен содержать объявлений встроенных (inline) функций и объектов...." "...Определения символических констант и встроенных функций являются специальными видами определений: те и другие могут появиться в программе несколько раз..." Возникает вопрос, верно ли написано в 1-й части? Если да, то как это пояснить? 2012-08-28 16:06:54 Илья ch8.4.5_Пример "... const int chunk = 16; ... char *buf = new char[ sizeof(Foo) * chunk ]; ... Foo *pb = new (buf) Foo; ..." 1) Для чего используем константу chunk в инструкции выделения памяти? 2) Почему для объекта типа Foo мы выделяем область памяти объекта типа char? Не будет ли в таком случае никаких конфликтов в дальнейшем? 2016-05-16 12:28:18 Иван Кто автор? Что за книга? 2024-04-01 17:12:58 Ааакк Оставить комментарий: |