С++ для начинающих - Учебное пособие

8. область видимости и время жизни

В этой главе обсуждаются два важных вопроса, касающиеся объявлений в С++. Где употребляется объявленное имя? Когда можно безопасно использовать объект или вызывать функцию, т.е. каково время жизни сущности в программе? Для ответа на первый вопрос мы введем понятие областей видимости и покажем, как они ограничивают применение имен в исходном файле программы. Мы рассмотрим разные типы таких областей: глобальную и локальную, а также более сложное понятие областей видимости пространств имен, которое появится в конце главы. Отвечая на второй вопрос, мы опишем, как объявления вводят глобальные объекты и функции (сущности, “живущие” в течение всего времени работы программы), локальные (“живущие” на определенном отрезке выполнения) и динамически размещаемые  объекты  (временем  жизни  которых  управляет  программист).  Мы также исследуем свойства времени выполнения, характерные для этих объектов и функций.

8.1. Область видимости

Каждое имя в С++ программе должно относиться к уникальной сущности (объекту, функции, типу или шаблону). Это не значит, что оно встречается только один раз во всей программе: его можно повторно использовать для обозначения другой сущности, если только есть некоторый контекст, помогающий различить разные значения одного и того же имени. Контекстом, служащим для такого различения, служит область видимости. В С++ поддерживается три их типа: локальная область видимости, область видимости пространства имен и область видимости класса.

Локальная    область –    это    часть    исходного    текста    программы,    содержащаяся    в определении функции (или в блоке). Любая функция имеет собственную такую часть, и каждая составная инструкция (или блок) внутри функции также представляет собой отдельную локальную область.

Область   видимости   пространства   имен –   часть   исходного   текста   программы,   не содержащаяся  внутри  объявления  или  определения  функции  или  определения  класса. Самая  внешняя  часть  называется  глобальной  областью  видимости  или  глобальной областью видимости пространства имен.

Объекты, функции, типы и шаблоны могут быть определены в глобальной области видимости. Программисту разрешено задать пользовательские пространства имен, заключенные внутри глобальной области с помощью определения пространства имен. Каждое такое пространство является отдельной областью видимости. Пользовательское пространство, как и глобальное, может содержать объявления и определения объектов, функций,  типов и шаблонов, а также вложенные пользовательские пространства  имен. (Они рассматриваются в разделах 8.5 и 8.6.)

Каждое определение класса представляет собой отдельную область видимости класса. (О таких областях мы расскажем в главе 13.)

Имя  может  обозначать  различные  сущности  в  зависимости  от  области  видимости.  В

следующем фрагменте программы имя s1 относится к четырем разным сущностям:

 

#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() << " ";

}

Поскольку  определения  функций  lexicoCompare(),  sizeCompare() и  sort() представляют собой различные области видимости и все они отличны от глобальной, в каждой из этих областей можно завести переменную с именем s1.

Имя,  введенное  с помощью  объявления,  можно  использовать  от точки  объявления  до конца   области  видимости   (включая   вложенные  области).   Так,  имя  s1 параметра функции lexicoCompare() разрешается употреблять до конца ее области видимости, то есть до конца ее определения.

Имя глобального массива s1 видимо с точки его объявления до конца исходного файла,

включая вложенные области, такие, как определение функции main().

В общем случае имя должно обозначать одну сущность внутри одной области видимости. Если в предыдущем примере после объявления массива s1 добавить следующую строку, компилятор выдаст сообщение об ошибке:

void s1(); // ошибка: повторное объявление s1

Перегруженные функции являются исключением из правила: можно завести несколько одноименных функций в одной области видимости, если они отличаются списком параметров. (Перегруженные функции рассматриваются в главе 9.)

В С++ имя должно быть объявлено до момента его первого использования в выражении. В противном случае компилятор выдаст сообщение об ошибке. Процесс сопоставления имени, используемого в выражении, с его объявлением называется разрешением. С помощью этого процесса имя получает конкретный смысл. Разрешение имени зависит от способа его употребления и от его области видимости. Мы рассмотрим этот процесс в различных контекстах. (В следующем подразделе описывается разрешение имен в локальной  области  видимости;  в  разделе  10.9 –  разрешение  в  шаблонах  функций;    в конце главы 13 – в области видимости классов, а в разделе 16.12 – в шаблонах классов.)

 

Области видимости и разрешение имен – понятия времени компиляции. Они применимы к отдельным частям текста программы. Компилятор интерпретирует текст программы согласно правилам областей видимости и правилам разрешения имен.

8.1.1. Локальная область видимости

Локальная область видимости – это часть исходного текста программы, содержащаяся в определении функции (или блоке внутри тела функции). Все функции имеют свои локальные области видимости. Каждая составная инструкция (или блок) внутри функции также представляет собой отдельную локальную область. Такие области могут быть вложенными.  Например,  следующее  определение  функции  содержит  два  их  уровня

const int notFound = -1; // глобальная область видимости

int binSearch( const vector<int> &vec, int val )

{ // локальная область видимости: уровень #1 int low = 0;

int high = vec.size() - 1;

while ( low <= high )

{ // локальная область видимости: уровень #2

int mid = ( low + high ) / 2;

if ( val < vec[ mid ] )

high = mid - 1;

else low = mid + 1;

}

return notFound; // локальная область видимости: уровень #1

(функция выполняет двоичный поиск в отсортированном векторе целых чисел):

}

Первая  локальная  область  видимости – тело функции  binSearch().  В ней  объявлены параметры  функции  vec и val,  а также переменные low и high. Цикл while внутри функции задает вложенную локальную область, в которой определена одна переменная mid.  Параметры  vec и  val и  переменные  low и  high видны  во вложенной  области. Глобальная область видимости включает в себя обе локальных. В ней определена одна целая константа notFound.

Имена   параметров  функции  vec и  val принадлежат  к  первой  локальной  области видимости тела функции, и в ней использовать те же имена для других сущностей нельзя.

int binSearch( const vector<int> &vec, int val )

{ // локальная область видимости: уровень #1

int val; // ошибка: неверное переопределение val

Например:

// ...

Имена параметров употребляются как внутри тела функции binSearch(), так и внутри вложенной области видимости цикла while. Параметры vec и val недоступны вне тела функции binSearch().

Разрешение имени в локальной области видимости происходит следующим образом: просматривается   та   область,   где  оно  встретилось.   Если   объявление  найдено,   имя разрешено. Если нет, просматривается область видимости, включающая текущую. Этот

 

процесс продолжается до тех пор, пока объявление не будет найдено либо не будет достигнута глобальная область видимости. Если и там имени нет, оно будет считаться ошибочным.

Из-за порядка просмотра областей видимости в процессе разрешения имен объявление из внешней области может быть скрыто объявлением того же имени во вложенной области. Если бы в предыдущем примере переменная low была объявлена в глобальной области видимости   перед   определением   функции   binSearch(),   то   использование   low в локальной  области  видимости  цикла  while все  равно  относилось  бы  к  локальному

int low;

int binSearch( const vector<int> &vec, int val )

{

// локальное объявление low

// скрывает глобальное объявление

int low = 0;

// ...

// low - локальная переменная

while ( low <= high )

{//...

}

// ...

объявлению, скрывающему глобальное:

}

Для   некоторых   инструкций   языка   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,

if ( int *pi = getValue() )

{

// pi != 0 -- *pi можно использовать здесь

int result = calc(*pi);

// ...

}

else

{

// здесь pi тоже видна

// pi == 0

cout << "ошибка: getValue() завершилась неудачно" << endl;

а также внутри условия циклов while и for. Например:

}

Переменные, определенные в условии инструкции 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) {

int ix = 79;

{

int ix = 89;

}

}

else {

int ix = 99;

}

}

Упражнение 8.2

К  каким  объявлениям  относятся  различные  использования  переменных  ix и  iy в

int ix = 1024;

void func( int ix, int iy ) {

ix = 100;

for( int iy = 0; iy < 400; iy += 100 ) {

iy += 100;

ix = 300;

}

iy = 400;

следующем примере:

}

8.2. Глобальные объекты и функции

Объявление функции в глобальной области видимости вводит глобальную функцию, а объявление переменной – глобальный объект. Глобальный объект существует на протяжении всего времени выполнения программы. Время жизни глобального объекта начинается с момента запуска программы и заканчивается с ее завершением.

Для  того  чтобы  глобальную  функцию  можно  было  вызвать  или  взять  ее  адрес,  она должна  иметь  определение.  Любой  глобальный  объект,  используемый  в  программе, должен  быть  определен,  причем  только  один  раз.  Встроенные  функции  могут определяться несколько раз, если только все определения совпадают. Такое требование единственности или точного совпадения получило название правила одного определения (ПОО). В этом разделе мы покажем, как следует вводить глобальные объекты и функции в программе, чтобы ПОО соблюдалось.

8.2.1. Объявления и определения

Как было сказано в главе 7, объявление функции устанавливает ее имя, а также тип возвращаемого  значения  и  список  параметров.  Определение  функции,  помимо  этой

 

информации, задает еще и тело – набор инструкций, заключенных в фигурные скобки.

// объявление функции calc()

// определение находится в другом файле

void calc(int);

int main()

{

int loc1 = get(); // ошибка: get() не объявлена

calc(loc1);        // правильно: calc() объявлена

// ...

Функция должна быть объявлена перед вызовом. Например:

}

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;

 

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 вызывает ошибку во время связывания программы. Если бы такое связывание прошло успешно, можно представить дальнейшее развитие событий: скомпилированная программа была протестирована на рабочей станции Sun Sparc, а затем перенесена на IBM 390. Первый же запуск потерпел неудачу: даже самые простые тесты не проходили. Что случилось?

Вот часть объявлений набора лексем:

 

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. C

unsigned char lastTok = 0;

unsigned char peekTok() { /* ... */ }

// в lex.C

extern char lastTok;

примеру, путем возбуждения исключения или из-за вывода неправильной информации).

extern char peekTok();

Избежать   подобных   неточностей   поможет   прежде  всего  правильное  использование заголовочных файлов. Мы поговорим об этом в следующем подразделе.

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 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 определяется  и  без  явной  инициализации:  об  этом

 

говорит отсутствие ключевого слова 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 *const  msg = "?? oops: error: ";

Такое определение может появиться в разных файлах.

Схожая  ситуация  наблюдается  и  со встроенными  функциями.  Для  того чтобы компилятор мог подставить тело функции “по месту”, он должен видеть ее определение. (Встроенные функции были представлены в разделе 7.6.)

 

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

Большинство  компиляторов  выдают  предупреждение  в  любом  из  следующих  случаев

(обычно это требует включения режима выдачи предупреждений):

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

конкретный вызов функции может не быть “подставлен по месту”. Например, в оригинальной реализации С++ компании AT&T (cfront) такая подстановка невозможна для второго вызова в пределах одного и того же выражения. В такой ситуации выражение следует переписать, разделив вызовы встроенных функций.

Перед тем как употребить спецификацию   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"

Matrix* trouble( Matrix *pm )

{

Matrix res;

// какие-то действия

// результат присвоим res

return &res; // плохо!

}

int main()

{

Matrix m1;

// ...

Matrix *mainResult = trouble( &m1 );

// ...

выполнения функции будет относиться к несуществующему объекту:

}

mainResult получает  значение  адреса  автоматического   объекта  res.  К  несчастью, память, отведенная под res, освобождается по завершении функции trouble(). После возврата в main() mainResult указывает на область памяти, не отведенную никакому объекту. (В данном примере эта область все еще может содержать правильное значение, поскольку мы не вызывали  других функций  после trouble() и запись  ее активации, вероятно, еще не затерта.) Подобные ошибки обнаружить весьма трудно. Дальнейшее использование mainResult в программе скорее всего даст неверные результаты.

Передача  в функцию  trouble() адреса m1 автоматического объекта  функции  main() безопасна. Память, отведенная main(), во время вызова trouble()находится в стеке, так что m1 остается доступной внутри trouble().

 

Если  адрес  автоматического  объекта  сохраняется  в  указателе,  время  жизни  которого больше,  чем  самого  объекта,  такой  указатель  называют  висячим.  Работа  с  ним –  это серьезная  ошибка,  поскольку  содержимое  адресуемой  области  памяти  непредсказуемо. Если комбинация бит по этому адресу оказывается в какой-то степени допустимой (не приводит к нарушению защиты памяти), то программа будет выполняться, но результаты ее будут неправильными.

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;

Параметры также можно объявлять как регистровые переменные:

}

Их активное использование может заметно увеличить скорость выполнения функции.

Указание   ключевого   слова   register –   только   подсказка   компилятору.   Некоторые компиляторы  игнорируют  такой  запрос,  применяя  специальные  алгоритмы  для определения наиболее подходящих кандидатов на размещение в свободных регистрах.

Поскольку компилятор учитывает архитектуру машины, на которой будет выполняться программа, он зачастую может принять более обоснованное решение об использовании машинных регистров.

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

Внутри   функции   или   составной   инструкции   можно  объявить   объект  с  локальной областью видимости, который, однако, будет существовать в течение всего времени выполнения программы. Если значение локального объекта должно сохраняться между вызовами функции, то обычный автоматический объект не подойдет: ведь его значение теряется каждый раз после выхода.

В таком  случае локальный  объект  необходимо  объявить  как static (со статической продолжительностью хранения). Хотя значение такого объекта сохраняется между вызовами функции, в которой он определен, видимость его имени ограничена локальной областью.    Статический    локальный    объект    инициализируется    во   время   первого

 

выполнения   инструкции, где    он          объявлен.       Вот,     например,      версия    функции

#include <iostream>

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 );

gcd(),устанавливающая глубину рекурсии с его помощью:

}

Значение,  ассоциированное  со  статическим  локальным  объектом  depth,  сохраняется между вызовами traceGcd(). Его инициализация выполняется только один раз – когда к

#include <iostream>

extern int traceGcd(int, int);

int main() {

int rslt = traceCcd( 15, 123 );

cout << "НОД (15,123): " << rslt << endl;

return 0;

этой функции обращаются впервые. В следующей программе используется traceGcd():

}

Результат работы программы:

глубина #1 глубина #2 глубина #3 глубина #4

НОД (15,123): 3

Неинициализированные статические локальные объекты получают значение 0. А автоматические  объекты  в  подобной  ситуации  получают  случайные  значения. Следующая программа иллюстрирует разницу инициализации по умолчанию для автоматических и статических объектов и опасность, подстерегающую программиста в случае ее отсутствия для автоматических объектов.

 

#include <iostream>

const int iterations = 2;

void func() {

int value1, value2; // не инициализированы

static int depth;           // неявно инициализирован нулем

if ( depth < iterations )

{ ++depth; func(); }

else depth = 0;

cout << " valuel: " << value1;

cout << " value2: " << value2;

cout << " sum: " << value1 + value2;

}

int main() {

for ( int ix = 0; ix < iterations; ++ix ) func();

return 0;

}

Вот результат работы программы:

 

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.   Динамически   размещаться   может   как единичный объект, так и массив объектов. Размер массива, размещаемого в хипе, разрешается задавать во время выполнения.

 

В этом разделе, посвященном динамическим объектам, мы рассмотрим три формы оператора new: для размещения единичного объекта, для размещения массива и третью форму, называемую оператором размещения new (placement new expression). Когда хип исчерпан, этот оператор возбуждает исключение. (Разговор об исключениях будет продолжен   в   главе   11.   В  главе  15  мы  расскажем   об  операторах   new и  delete применительно к классам.)

8.4.1. Динамическое создание и уничтожение единичных объектов

Оператор new состоит их ключевого слова new, за которым следует спецификатор типа. Этот  спецификатор   может  относиться   к  встроенным   типам  или  к  типам  классов. Например:

new int;

размещает   в   хипе   один   объект   типа   int.   Аналогично   в   результате   выполнения инструкции

new iStack;

там появится один объект класса iStack.

Сам  по себе  оператор  new не слишком  полезен.  Как  можно  реально  воспользоваться созданным объектом? Одним из аспектов работы с памятью из хипа является то, что размещаемые в ней объекты не имеют имени. Оператор new возвращает не сам объект, а указатель на него. Все манипуляции с этим объектом производятся косвенно через указатели:

int *pi = new int;

Здесь оператор new создает один объект типа int, на который ссылается указатель pi. Выделение памяти из хипа во время выполнения программы называется динамическим выделением. Мы говорим, что память, адресуемая указателем pi, выделена динамически.

Второй аспект, относящийся к использованию хипа, состоит в том, что эта память не инициализируется. Она содержит “мусор”, оставшийся после предыдущей работы. Проверка условия:

if ( *pi == 0 )

вероятно, даст false, поскольку объект, на который указывает pi, содержит случайную последовательность  битов. Следовательно,  объекты, создаваемые с помощью оператора new,  рекомендуется  инициализировать.  Программист  может  инициализировать  объект типа int из предыдущего примера следующим образом:

int *pi = new int( 0 );

Константа в скобках задает начальное значение для создаваемого объекта; теперь pi ссылается на объект типа int, имеющий значение 0. Выражение в скобках называется инициализатором. Это может быть любое выражение (не обязательно константа), возвращающее значение, приводимое к типу int.

 

Оператор  new выполняет  следующую  последовательность  действий:  выделяет  из  хипа память для объекта, затем инициализирует его значением, стоящим в скобках. Для выделения  памяти  вызывается  библиотечная  функция  new().  Предыдущий  оператор

int ival = 0;      // создаем объект типа int и инициализируем его 0

приблизительно эквивалентен следующей последовательности инструкций:

int *pi = &ival; // указатель ссылается на этот объект

не считая, конечно, того, что объект, адресуемый pi, создается библиотечной функцией

new() и размещается в хипе. Аналогично

iStack *ps = new iStack( 512 );

создает объект типа  iStack на 512 элементов.  В случае объекта  класса  значение или значения в скобках передаются соответствующему конструктору, который вызывается в случае успешного выделения памяти. (Динамическое создание объектов классов более подробно рассматривается в разделе 15.8. Оставшаяся часть данного раздела посвящена созданию объектов встроенных типов.)

Описанные операторы new могут вызывать одну проблему: хип, к сожалению, является конечным ресурсом, и в некоторой точке выполнения программы мы можем исчерпать его.  Если  функция  new() не может  выделить  затребованного  количества  памяти,  она возбуждает исключение bad_alloc. (Обработка исключений рассматривается в главе 11.)

Время  жизни  объекта,  на  который  указывает  pi,  заканчивается  при  освобождении памяти,  где  этот  объект  размещен.  Это  происходит,  когда  pi передается  оператору delete. Например,

delete pi;

освобождает память, на которую ссылается pi, завершая время жизни объекта типа int. Программист   управляет  окончанием   жизни  объекта,  используя  оператор  delete в нужном месте программы. Этот оператор вызывает библиотечную функцию delete(), которая возвращает выделенную память в хип. Поскольку хип конечен, очень важно возвращать ее своевременно.

Глядя на предыдущий пример, вы можете спросить: а что случится, если значение  pi по

// необходимо ли это?

if ( pi != 0 )

какой-либо причине было нулевым? Не следует ли переписать этот код таким образом:

delete pi;

Нет. Язык С++ гарантирует, что оператор delete не будет вызывать функцию delete() в случае нулевого операнда. Следовательно, проверка на 0 необязательна. (Если вы явно добавите такую проверку, в большинстве реализаций она фактически будет выполнена дважды.)

Важно понимать  разницу между временем жизни указателя  pi и объекта, который он адресует.   Сам  объект   pi является  глобальным   и  объявлен   в   глобальной   области

 

видимости. Следовательно, память под него выделяется до выполнения программы и сохраняется   за  ним  до  ее  завершения.   Совсем  не  так  определяется   время  жизни адресуемого  указателем  pi объекта,  который  создается  с  помощью  оператора  new во время выполнения. Область памяти, на которую указывает pi, выделена динамически, следовательно,  pi является указателем на динамически  размещенный  объект типа int. Когда в программе встретится оператор delete, эта память  будет освобождена. Однако память,   отведенная   самому  указателю   pi,  не  освобождается,   а   ее  содержимое  не изменяется. После выполнения delete объект pi становится висячим указателем, то есть ссылается на область памяти, не принадлежащую программе. Такой указатель служит источником трудно обнаруживаемых ошибок, поэтому сразу после уничтожения объекта ему полезно присвоить 0, обозначив таким образом, что указатель больше ни на что не ссылается.

Оператор  delete может  использоваться  только  по  отношению  к  указателю,  который содержит адрес области памяти, выделенной в результате выполнения оператора new. Попытка применить delete к указателю, не ссылающемуся на такую память,  приведет к непредсказуемому поведению программы. Однако, как было сказано выше, этот оператор можно применять к нулевому указателю.

void f() {

int i;

string str = "dwarves";

int *pi = &i;

short *ps = 0;

double *pd = new doub1e(33);

delete str; // плохо: str не является динамическим объектом

delete pi; // плохо: pi ссылается на локальный объект

delete ps; // безопасно

delete pd; // безопасно

Ниже приведены примеры опасных и безопасных операторов delete:

}

Вот три основные ошибки, связанные с динамическим выделением памяти:

не освободить выделенную память. В таком случае память не возвращается в хип. Эта ошибка получила название утечки памяти;

дважды применить оператор delete к одной и той же области памяти. Такое бывает, когда два указателя получают адрес одного и того же динамически размещенного объекта. В результате подобной ошибки мы вполне можем удалить нужный объект. Действительно, память, освобожденная с помощью одного из адресующих ее указателей, возвращается в хип и затем выделяется под другой объект. Затем оператор delete применяется ко второму указателю, адресовавшему старый объект, а удаляется при этом новый;

изменять   объект   после   его   удаления.   Такое   часто   случается,   поскольку указатель, к которому применяется оператор delete, не обнуляется.

Эти ошибки при работе с динамически выделяемой памятью гораздо легче допустить, нежели обнаружить и исправить. Для того чтобы помочь программисту, стандартная библиотека С++ представляет класс auto_ptr. Мы рассмотрим его в следующем подразделе.   После   этого  мы  покажем,   как  динамически   размещать   и  уничтожать массивы, используя вторую форму операторов new и delete.

 

8.4.2. Шаблон auto_ptr  А

В  стандартной  библиотеке  С++  auto_ptr является  шаблоном  класса,  призванным помочь программистам в манипулировании объектами, которые создаются посредством оператора new. (К сожалению, подобного шаблона для манипулирования динамическими массивами  нет. Использовать  auto_ptr для создания массивов  нельзя, это приведет к непредсказуемым результатам.)

Объект  auto_ptr инициализируется   адресом   динамического   объекта,  созданного  с помощью оператора new. Такой объект автоматически уничтожается, когда заканчивается время жизни auto_ptr. В этом подразделе мы расскажем, как ассоциировать auto_ptr с динамически размещаемыми объектами.

Для использования шаблона класса auto_ptr необходимо включить заголовочный файл:

#include <memory>

auto_ptr< type_pointed_to > identifier( ptr_allocated_by_new );

auto_ptr< type_pointed_to > identifier( auto_ptr_of_same_type );

Определение объекта  auto_ptr имеет три формы:

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 адресом   объекта   класса,   скажем,

auto_ptr< string >

стандартного класса string? Например:

 

pstr_auto( new string( "Brontosaurus" ) );

Предположим,  что мы  хотим  выполнить  какую-то операцию  со строками.  С обычной

string *pstr_type = new string( "Brontosaurus" );

if ( pstr_type->empty() )

строкой мы бы поступили таким образом:

// ошибка, что-то не так

auto_ptr< string > pstr_auto( new      string( "Brontosaurus" ) );

if ( pstr_type->empty() )

А как обратиться к операции empty(), используя объект auto_ptr? Точно так же:

// ошибка, что-то не так

Создатели  шаблона  класса  auto_ptr не  в  последнюю  очередь  стремились  сохранить привычный синтаксис, употребляемый с обычными указателями, а также обеспечить дополнительные возможности автоматического удаления объекта, на который ссылается auto_ptr.  При  этом  время  выполнения  не  увеличивается.  Применение  встроенных функций (которые подставляются по месту вызова) позволило сделать использование объекта   auto_ptr немногим   более   дорогим,   чем   непосредственное   употребление указателя.

Что   произойдет,   если   мы   проинициализируем   pstr_auto2 значением   pstr_auto,

// кто несет ответственность за уничтожение строки?

который является объектом auto_ptr, указывающим на строку?

auto_ptr< string > pstr_auto2( pstr_auto );

Представим,  что  мы  непосредственно  инициализировали   один  указатель  на  строку другим:

string *pstr_type2( pstr_type );

Оба указателя теперь содержат адрес одной и той же строки, и мы должны быть внимательными, чтобы не удалить строку дважды.

В противоположность  этому шаблон класса auto_ptr поддерживает понятие владения. Когда мы определили pstr_auto, он стал владельцем строки, адресом которой был инициализирован, и принял на себя ответственность за ее уничтожение.

Вопрос в том, кто станет владельцем строки, когда мы инициализируем pstr_auto2 адресом,  указывающим  на тот же объект, что и pstr_auto? Нежелательно,  чтобы оба объекта  владели  одной  и  той  же  строкой:  это  вернет  нас  к  проблемам  повторного удаления, от которых мы стремились уйти с помощью шаблона класса auto_ptr.

Когда  один  объект  auto_ptr инициализируется  другим  или  получает  его значение в результате присваивания, одновременно он получает и право владения адресуемым объектом. Объект  auto_ptr, стоящий справа от оператора присваивания, передает право

 

владения  и  ответственность  auto_ptr,  стоящему  слева.  В  нашем  примере ответственность за уничтожение строки несет pstr_auto2, а не pstr_auto. pstr_auto больше не может употребляться для ссылки на эту строку.

auto_ptr< int > p1( new int( 1024 ) );

Аналогично ведет себя и операция присваивания. Пусть у нас есть два объекта auto_ptr:

auto_ptr< int > p2( new int( 2048 ) );

Мы можем скопировать один объекта auto_ptr в другой с помощью этой операции:

p1 = p2;

Перед присваиванием объект, на который ссылался p1, удаляется.

После присваивания  p1 владеет объектом  типа  int со значением  2048. p2 больше не может использоваться как ссылка на этот объект.

Третья   форма   определения   объекта   auto_ptr создает   его,   но  не  инициализирует

// пока не ссылается ни на какой объект

значением указателя на область памяти из хипа. Например:

auto_ptr< int > p_auto_int;

Поскольку  p_auto_int не  инициализирован  адресом  какого-либо  объекта,  значение хранящегося внутри него указателя равно 0. Разыменование таких указателей приводит к

// ошибка: разыменование нулевого указателя

if ( *p_auto_int != 1024 )

непредсказуемому поведению программы:

*p_auto_int = 1024;

int *pi = 0;

Обычный указатель можно проверить на равенство 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 ни  на  что  не  указывает,  то  как  заставить  его  адресовать  что-либо?

Другими  словами,  как мы  можем  присвоить  значение внутреннему указателю объекта

else

// хорошо, присвоим ему значение

auto_ptr? Это делается с помощью операции reset(). Например:

p_auto_int.reset( new int( 1024 ) );

Объекту auto_ptr нельзя  присвоить  адрес  объекта,  созданного  с помощью  оператора

void example() {

// инициализируется нулем по умолчанию

auto_ptr< int > pi;

{

 

 

}

new:

}

// не поддерживается

pi = new int( 5 ) ;

 

 

В этом случае надо использовать функцию reset(), которой можно передать указатель или 0, если мы хотим обнулить объект auto_ptr. Если auto_ptr указывает на объект и является  его  владельцем,  то  этот  объект  уничтожается  перед  присваиванием  нового

auto_ptr< string >

pstr_auto( new string( "Brontosaurus" ) );

// "Brontosaurus" уничтожается перед присваиванием

значения внутреннему указателю auto_ptr. Например:

pstr_auto.reset( new string( "Long-neck" ) );

В последнем  случае лучше,  используя  операцию  assign(),  присвоить  новое значение

// более эффективный способ присвоить новое значение

// используем операцию assign()

существующей строке, чем уничтожать одну строку и создавать другую:

pstr_auto->assign( "Long-neck" );

Одна   из  трудностей   программирования   состоит  в  том,   что  получить   правильный результат не всегда достаточно. Иногда накладываются и временные ограничения. Такая мелочь,  как  удаление  и  создание  заново  строкового  объекта,  вместо  использования функции  assign() при  определенных  обстоятельствах  может  вызвать  значительное замедление работы. Подобные детали не должны вас беспокоить при проектировании, но при доводке программы на них следует обращать внимание.

 

Шаблон  класса  auto_ptr обеспечивает  значительные  удобства  и  безопасность использования динамически выделяемой памяти. Однако все равно надо не терять бдительности, чтобы не навлечь на себя неприятности:

нельзя   инициализировать   объект   auto_ptr указателем,   полученным   не  с помощью оператора new, или присвоить ему такое значение. В противном случае после  применения  к  этому  объекту  оператора  delete поведение  программы непредсказуемо;

два объекта auto_ptr не должны получать во владение один и тот же объект.

Очевидный  способ  допустить  такую  ошибку –  присвоить  одно  значение  двум

auto_ptr< string >

pstr_auto( new string( "Brontosaurus" ) );

// ошибка: теперь оба указывают на один объект

// и оба являются его владельцами

объектам. Менее очевидный – с помощью операции get(). Вот пример:

auto_ptr< string > pstr_auto2( pstr_auto.get() );

Операция   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

элементов.

В  общем  случае  массив,  размещаемый  в  хипе,  не  может  быть  инициализирован.  (В разделе  15.8  мы  покажем,  как  с  помощью  конструктора  по  умолчанию  присвоить начальное значение динамическому массиву объектов типа класса.) Задавать инициализатор  при  выделении  оператором  new памяти  под  массив  не  разрешается. Массиву   элементов   встроенного   типа,   размещенному   в  хипе,   начальные  значения

for (int index = 0; index < 1024; ++index )

присваиваются с помощью цикла for:

pia[ index ] = 0;

Основное преимущество динамического массива состоит в том, что количество элементов в его первом измерении не обязано быть константой, т.е. может не быть известным во время компиляции. Для массивов, определяемых в локальной или глобальной области видимости, это не так: здесь размер задавать необходимо.

Например, если указатель в ходе выполнения программы ссылается на разные C-строки, то область памяти под текущую строку обычно выделяется динамически и ее размер определяется в зависимости от длины строки. Как правило, это более эффективно, чем создавать массив фиксированного размера, способный вместить самую длинную строку: ведь все остальные строки могут быть значительно короче. Более того, программа может аварийно завершиться, если длина хотя бы одной из строк превысит отведенный лимит.

Оператор   new допустимо  использовать   для  задания  первого  измерения   массива   с помощью  значения,  вычисляемого  во  время  выполнения.  Предположим,  у  нас  есть

const char *noerr = "success";

// ...

const char *err189 = "Error: a function declaration must "

 

 

следующие C-строки:

"specify a function return type!";

 

 

Размер  создаваемого  с помощью оператора new массива  может быть задан значением,

#include <cstring>

const char *errorTxt;

if (errorFound)

errorTxt = errl89;

else

errorTxt = noerr;

int dimension = strlen( errorTxt ) + 1;

char *strl = new char[ dimension ];

// копируем текст ошибки в strl

вычисляемым во время выполнения:

strcpy( strl, errorTxt );

 

// обычная для С++ идиома,

// иногда удивляющая начинающих программистов

dimension разрешается заменить выражением:

char *strl = new char[ str1en( errorTxt ) + 1 ];

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

Если  в  конце строки  нуля  нет,  то возможно  чтение или  запись  в  случайную  область памяти. Избежать подобных проблем позволяет класс string из стандартной библиотеки С++.

Отметим, что только первое измерение массива, создаваемого с помощью оператора new,

может быть задано значением, вычисляемым во время выполнения. Остальные измерения

int getDim();

// создание двумерного массива

int (*pia3)[ 1024 ] = new int[ getDim() ][ 1024 ]; // правильно

// ошибка: второе измерение задано не константой

должны задаваться константами, известными во время компиляции. Например:

int **pia4 = new int[ 4 ][ getDim() ];

Оператор delete для уничтожения массива имеет следующую форму:

delete[] str1;

Пустые   квадратные  скобки   необходимы.   Они   говорят   компилятору,   что  указатель адресует массив, а не единичный элемент. Поскольку тип str1 – указатель на char, без этих скобок компилятор не поймет, что удалять следует целый массив.

Отсутствие скобок не является синтаксической ошибкой, но правильность выполнения программы не гарантируется (это особенно справедливо для массивов, которые содержат объекты классов, имеющих деструкторы, как это будет показано в разделе 14.4).

Чтобы избежать проблем, связанных с управлением динамически выделяемой памятью для массивов, рекомендуется пользоваться контейнерными типами из стандартной библиотеки,   такими,   как   vector,   list или   string.   Они   управляют   памятью автоматически. (Тип string был представлен в разделе 3.4, тип vector – в разделе 3.10. Подробное описание контейнерных типов см. в главе 6.)

 

8.4.4. Динамическое создание и уничтожение константных объектов

Программист способен создать объект в хипе и запретить изменение его значения после инициализации. Этого можно достичь, объявляя объект константным. Для этого применяется следующая форма оператора new:

const int *pci = new const int(1024);

Константный динамический объект имеет несколько особенностей. Во-первых, он должен быть инициализирован, иначе компилятор сигнализирует об ошибке (кроме случая, когда объект  принадлежит  к  типу  класса,  имеющего  конструктор  по  умолчанию;  в  такой ситуации инициализатор можно опустить).

Во-вторых, указатель, возвращаемый выражением  new, должен адресовать константу. В

предыдущем примере pci служит указателем на const int.

Константность  динамически  созданного  объекта      подразумевает,  что  значение, полученное при инициализации, в дальнейшем не может быть изменено. Но поскольку объект динамический, временем его жизни управляет оператор delete. Например:

delete pci;

Хотя  операнд  оператора  delete имеет  тип  указателя  на  const int,  эта  инструкция является корректной и освобождает область памяти, на которую ссылается pci.

Невозможно создать динамический массив константных элементов встроенного типа потому,  что,  как  мы  отмечали  выше,  элементы  такого  массива  нельзя проинициализировать в операторе new. Следующая инструкция приводит к ошибке компиляции:

const int *pci = new const int[100]; // ошибка

8.4.5. Оператор размещения new  А

Существует третья форма оператора new, которая создает объект без отведения для него памяти, то есть в памяти, которая уже была выделена. Эту форму называют оператором размещения new. Программист указывает адрес области памяти, в которой размещается объект:

new (place_address) type-specifier

place_address должен  быть указателем.  Такая форма  (она  включается  заголовочным файлом <new>) позволяет программисту предварительно выделить большую область памяти, которая впоследствии будет содержать различные объекты. Например:

 

#include <iostream>

#include <new>

const int chunk = 16;

class Foo {

public:

int val() { return _val; } FooQ(){ _val = 0; }

private:

int _val;

};

// выделяем память, но не создаем объектов Foo char *buf = new char[ sizeof(Foo) * chunk ];

int main() {

// создаем объект Foo в buf

Foo *pb = new (buf) Foo;

// проверим, что объект помещен в buf if ( pb.val() == 0 )

cout << "Оператор new сработал!" << endl;

// здесь нельзя использовать pb delete[] buf;

return 0;

}

Результат работы программы:

Оператор new сработал!

Для оператора размещения new нет парного оператора delete: он не нужен, поскольку эта форма не выделяет память. В предыдущем примере необходимо освободить память, адресуемую указателем buf, а не pb. Это происходит в конце программы, когда буфер больше  не нужен.  Поскольку  buf ссылается  на  символьный  массив,  оператор  delete имеет форму

delete[] buf;

При  уничтожении  buf прекращают  существование  все  объекты,  созданные  в  нем.  В

нашем примере pb больше не ссылается на существующий объект класса Foo.

Упражнение 8.5

(a) const float *pf = new const float[100]; (b) double *pd = new doub1e[10] [getDim()]; (c) int (*pia2)[ 1024 ] = new int[ ][ 1024 ];

Объясните, почему приведенные операторы new ошибочны:

(d) const int *pci = new const int;

Упражнение 8.6

Как бы вы уничтожили pa?

 

typedef int arr[10];

int *pa = new arr;

Упражнение 8.7

Какие  из  следующих  операторов  delete содержат  потенциальные  ошибки  времени

int globalObj;

char buf[1000];

void f() {

int *pi = &global0bj;

double *pd = 0;

float *pf = new float(O);

int *pa = new(buf)int[20];

delete pi;  // (a) delete pd;  // (b) delete pf;  // (c) de1ete[] pa; // (d)

выполнения и почему:

}

Упражнение 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

int *pi0 = p2.get();

Объясните разницу между следующими инструкциями:

int *pi1 = p2.release() ;

Для каких случаев более приемлем тот или иной вызов?

Упражнение 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 является  пользовательским   пространством  имен  (в  отличие  от глобального пространства, которое неявно подразумевается и существует в любой программе).

Каждое  такое  пространство  представляет  собой  отдельную  область  видимости.  Оно может содержать вложенные определения пространств имен, а также объявления или определения функций, объектов, шаблонов и типов. Все сущности, объявленные внутри некоторого пространства имен, называются его членами. Каждое имя в пользовательском пространстве,   как   и   в   глобальном,   должно   быть   уникальным   в   пределах   этого пространства.

Однако  в  разных  пользовательских  пространствах  могут  встречаться  члены  с одинаковыми именами.

Имя члена пространства имен автоматически дополняется, или квалифицируется, именем этого пространства. Например, имя класса matrix, объявленное в пространстве cplusplus_primer, становится cplusplus_primer::matrix, а имя функции inverse() превращается в 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 {

void inverse ( matrix & );

matrix operator+ ( const matrix &ml, const matrix &m2 )

{/* ... */ }

}

Два приведенных примера эквивалентны: оба задают пространство имен cplusplus_primer,  содержащее  класс  matrix,  функцию  inverse(),  константу  pi и operator+().  Определение  пространства  имен  может  состоять  из  нескольких соединенных частей.

Последовательность

namespace namespace_name {

задает  новое  пространство,  если имя namespace_name не совпадает  с одним из ранее объявленных. В противном случае новые объявления добавляются в старое пространство.

Возможность разбить пространство имен на несколько частей помогает при организации библиотеки.  Ее исходный  код  легко  разделить  на интерфейсную  часть и  реализацию.

// Эта часть пространства имен

// определяет интерфейс библиотеки

namespace cplusplus_primer { class matrix { /* ... */ }; const double pi = 3.1416;

matrix operator+ ( const matrix &ml, const matrix &m2       );

void inverse ( matrix & );

}

// Эта часть пространства имен

// определяет реализацию библиотеки

namespace cplusplus_primer {

void inverse ( matrix &m )

{ /* ... */ }

matrix operator+ ( const matrix &ml,             const matrix &m2 )

{ /* ... */ }

Например:

}

Первая часть пространства имен содержит объявления и определения, служащие интерфейсом библиотеки: определения типов, констант, объявления функций. Во второй части находятся детали реализации, то есть определения функций.

Еще более полезной для организации исходного кода библиотеки является возможность разделить определение одного пространства имен на несколько файлов: эти определения также объединяются. Наша библиотека может быть устроена следующим образом:

 

// ---- 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 ----

#include "primer.h"

namespace cplusplus_primer {

void inverse( matrix &m )

{ /* ... */ }

matrix operator+ ( const matrix &m1, const matrix &m2 )

{ /* ... */ }

}

// ---- user.C ----

// определение интерфейса библиотеки

#include "primer.h"

void func( cplusplus_primer::matrix &m        )

{

//...

cplusplus_primer: :inverse( m );

return m;

Программа, использующая эту библиотеку, выглядит так:

}

Подобная организация программы обеспечивает модульность библиотеки, необходимую для сокрытия реализации от пользователей, в то же время позволяя без ошибок скомпилировать и связать файлы primer.C и user.C в одну программу.

8.5.2. Оператор разрешения области видимости

Имя члена пользовательского пространства дополняется поставленным спереди именем этого пространства и оператором разрешения области видимости (::). Использование неквалифицированного  члена,  например  matrix,  является  ошибкой.  Компилятор  не

// определение интерфейса библиотеки

#include "primer.h"

// ошибка: нет объявления для matrix

знает, к какому объявлению относится это имя:

void func( matrix &m );

Объявление члена пространства имен скрыто в своем пространстве. Если мы не укажем компилятору,  где  именно  искать  объявление,  он  произведет  поиск  только  в  текущей области видимости и в областях, включающих текущую. Допустим, если переписать предыдущую программу так:

 

// определение интерфейса библиотеки

#include "primer.h"

class matrix { /* пользовательское определение */ };

// правильно: глобальный тип matrix найден

void func( matrix &m );

то определение класса matrix компилятор  находит в глобальной области видимости и программа   компилируется   без   ошибок.   Поскольку   объявление   matrix как   члена пространства имен cplusplus_primer скрыто в этом пространстве, оно не конфликтует с классом, объявленным в глобальной области видимости.

Именно поэтому мы говорим, что пространства имен решают проблему засорения глобального пространства: имена их членов невидимы, если имя пространства не указано явно, с помощью оператора разрешения области видимости. Существуют и другие механизмы, позволяющие сделать объявление члена пространства имен видимым вне его. Это using-объявления и using-директивы. Мы рассмотрим их в следующем разделе.

Отметим, что оператор области видимости может быть использован и для того, чтобы сослаться  на  элемент  глобального  пространства  имен.  Поскольку  это пространство  не имеет имени, запись

::member_name

относится к его элементу. Такой способ полезен для указания членов глобального пространства,  если  их  имена  оказываются  скрыты  именами,  объявленными  во вложенных локальных областях видимости.

Следующий пример демонстрирует использование оператора области видимости для обращения к скрытому члену глобального пространства имен. Функция вычисляет последовательность чисел Фибоначчи. В программе два определения переменной max. Глобальная переменная указывает максимальное значение элемента последовательности, при  превышении  которого  вычисление  прекращается,  а  локальная –  желаемую  длину последовательности при данном вызове функции. (Напоминаем, что параметры функции относятся к ее локальной области видимости.) Внутри функции должны быть доступны обе   переменных.   Однако   неквалифицированное   имя   max ссылается   на   локальное объявление этой переменной. Чтобы получить глобальную переменную, нужно использовать оператор разрешения области видимости ::max. Вот текст программы:

 

#include <iostream> const int max = 65000; const int lineLength = 12;

void fibonacci( int max )

{

if ( max < 2 ) return;

cout << "0 1 ";

int v1 = 0, v2 = 1, cur;

for ( int ix = 3; ix <= max; ++ix ) {

cur = v1 + v2;

if ( cur > ::max ) break;

cout << cur << " ";

vl = v2;

v2 = cur;

if (ix \% "lineLength == 0) cout << end"!;

}

}

#include <iostream> void fibonacci( int ); int main() {

cout << "Числа Фибоначчи: 16 ";

fibonacci( 16 );

return 0;

Так выглядит функция main(), вызывающая fibonacci():

}

Результат работы программы:

Числа Фибоначчи: 16

0 1 1 2 3 5 8 13 21 34 55 89

144 233 377 610

8.5.3. Вложенные пространства имен

Мы уже упоминали, что пользовательские пространства имен могут быть вложенными. Такие пространства применяются для дальнейшего структурирования кода нашей библиотеки. Например:

 

// ---- primer.h ---- namespace cplusplus_primer {

// первое вложенное пространство имен:

// матричная часть библиотеки

namespace MatrixLib {

class matrix { /* ... */ };

const double pi = 3.1416;

matrix operators+ ( const matrix &ml, const matrix &m2 );

void inverse( matrix & );

// ...

}

// второе вложенное пространство имен:

// зоологическая часть библиотеки

namespace AnimalLib {

class ZooAnimal { /* ... */ };

class Bear : public ZooAnimal { /* ... */ };

class Raccoon : public Bear { /* ... */ };

// ...

}

}

Пространство   имен   cplusplus_primer содержит   два   вложенных:   MatrixLib и

AnimalLib.

cplusplus_primer предотвращает  конфликт  между  именами  из  нашей  библиотеки  и именами из глобального пространства вызывающей программы. Вложенность позволяет делить  библиотеку  на  части,  в  которых  сгруппированы   связанные  друг  с  другом объявления и определения. MatrixLib содержит сущности, имеющие отношение к классу matrix, а AnimalLib – к классу ZooAnimal.

Объявление  члена  вложенного  пространства  скрыто  в  этом пространстве.  Имя  такого члена автоматически дополняется поставленными спереди именами самого внешнего и вложенного пространств.

Например, класс, объявленный во вложенном пространстве MatrixLib, имеет имя

cplusplus_primer::MatrixLib::matrix

а функция

cplusplus_primer::MatrixLib::inverse

Программа,    использующая           члены             вложенного   пространства

#include "primer.h"

// да, это ужасно...

// скоро мы рассмотрим механизмы, облегчающие

// использование членов пространств имен!

void func( cplusplus_primer::MatrixLib::matrix &m )

{

// ... cplusplus_primer::MatrixLib::inverse( m ); return m;

cplusplus_primer::MatrixLib, выглядит так:

 

}

Вложенное пространство имен является вложенной областью видимости внутри пространства, содержащего его. В процессе разрешения имен вложенные пространства ведут   себя   так  же,   как  вложенные   блоки.   Когда   некоторое  имя  употребляется   в пространстве  имен,  поиск  его  объявление  проводится  во  всех  объемлющих пространствах.  В  следующем  примере  разрешение  имени  Type происходит  в  таком порядке: сначала ищем его в пространстве имен MatrixLib, затем в cplusplus_primer и

typedef double Type;

namespace cplusplus_primer {

typedef int Type; // скрывает ::Type

namespace MatrixLib {

int val;

// Type: объявление найдено в cplusplus_primer int func(Type t) {

double val; // скрывает MatrixLib::val val = ...;

}

// ...

}

наконец в глобальной области видимости:

}

Если некоторая сущность объявляется во вложенном пространстве имен, она скрывает объявление одноименной сущности из объемлющего пространства.

В предыдущем примере имя Type из глобальной области видимости скрыто объявлением Type в пространстве cplusplus_primer. При разрешении имени Type, упоминаемого в MatrixLib,   оно  будет  найдено   в  cplusplus_primer,   поэтому   у  функции   func() параметр имеет тип int.

Аналогично сущность, объявленная в пространстве имен, скрывается одноименной сущностью из вложенной локальной области видимости. В предыдущем примере имя val из  MatrixLib скрыто  новым  объявлением  val.  При  разрешении  имени  val внутри func() будет  найдено  его  объявление  в  локальной   области  видимости,  и  потому присваивание в func() относится именно к локальной переменной.

8.5.4. Определение члена пространства имен

Мы   видели,   что   определение   члена   пространства   имен   может   появиться   внутри определения самого пространства. Например, класс matrix и константа pi появляются внутри вложенного пространства имен MatrixLib, а определения функций operator+() и inverse() приводятся где-то в другом месте текста программы:

 

// ---- primer.h ---- namespace cplusplus_primer {

// первое вложенное пространство имен:

// матричная часть библиотеки

namespace MatrixLib {

class matrix { /* ... */ };

const double pi = 3.1416;

matrix operators+ ( const matrix &ml, const matrix &m2 );

void inverse( matrix & );

// ...

}

}

Член  пространства  имен  можно  определить  и  вне соответствующего  пространства.  В таком случае имя члена должно быть квалифицировано именами пространств, к которым он  принадлежит.   Например,   если   определение  функции   operator+() помещено  в

// ---- primer.C ----

#include "primer.h"

// определение в глобальной области видимости

cplusplus_primer::MatrixLib::matrix

cplusplus_primer::MatrixLib::operator+

( const matrix& ml, const matrix &m2 )

глобальную область видимости, то оно должно выглядеть следующим образом:

{ /* ... */ }

Имя  operator+() квалифицировано  в  данном  случае  именами  пространств cplusplus_primer и   MatrixLib.  Однако обратите внимание на тип matrix в списке параметров  operator+():  употреблено  неквалифицированное  имя.  Как  такое  может быть?

В определении функции operator+() можно использовать неквалифицированные имена для членов своего пространства, поскольку определение принадлежит к его области видимости.  При  разрешении  имен  внутри  функции  operator+() используется MatrixLib. Заметим, однако, что в типе возвращаемого значения все же нужно указывать квалифицированное имя, поскольку он расположен вне области видимости, заданной определением функции:

cplusplus_primer::MatrixLib::operator+

В определении  operator+() неквалифицированные  имена могут встречаться в любом объявлении или выражении внутри списка параметров или тела функции. Например, локальное объявление внутри operator+() способно создать объект класса matrix:

 

// ---- primer.C ----

#include "primer.h"

cplusplus_primer::MatrixLib::matrix cplusplus_primer::MatrixLib::operator+

( const matrix &ml, const matrix &m2 )

{

// объявление локальной переменной типа

// cplusplus_primer::MatrixLib::matrix

matrix res;

// вычислим сумму двух объектов matrix return res;

}

Хотя члены могут быть определены вне своего пространства имен, такие определения допустимы не в любом месте. Их разрешается помещать только в пространства, объемлющие  данное.  Например,  определение  operator+() может  появиться  в глобальной    области    видимости,    в    пространстве    имен    cplusplus_primer и    в

// ---- primer.C --

#include "primer.h"

namespace cplusplus_primer { MatrixLib::matrix MatrixLib::operator+

( const matrix &ml, const matrix &m2 ) { /* ... */ }

пространстве  MatrixLib. В последнем случае это выглядит так:

}

Член может определяться вне своего пространства только при условии, что ранее он был объявлен    внутри.    Последнее    приведенное    определение    operator+() было    бы

namespace cplusplus_primer {

namespace MatrixLib {

class matrix { /*...*/ };

// следующее объявление не может быть пропущено

matrix operator+ ( const matrix &ml, const matrix &m2 );

// ...

}

ошибочным, если бы ему не предшествовало объявление в файле primer.h:

}

8.5.5. ПОО и члены пространства имен

Как уже было сказано, определение пространства имен может состоять из разрозненных частей и размещаться в разных файлах. Следовательно, член пространства разрешено объявлять во многих файлах. Например:

 

// primer.h

namespace cplusplus_primer {

// ...

void inverse( matrix & );

}

// usel.C

#include "primer.h"

// объявление cplusplus_primer::inverse() в use1.C

// use2.C

#include "primer.h"

// объявление cplusplus_primer::inverse() в use2.C

Объявление cplusplus::inverse() в primer.h ссылается на одну и ту же функцию в обоих исходных файлах use1.C и use2.C.

Член  пространства   имен  является  глобальной   сущностью,   хотя  его  имя квалифицировано. Требование ПОО (правило одного определения, см. раздел 8.2) распространяется  и  на  него.  Чтобы  удовлетворить  этому  требованию,  программы,  в которых используются пространства имен, обычно организуют следующим образом:

1.   Объявления    функций   и   объектов,    являющихся    членами   пространства   имен,

помещают в заголовочный файл, который включается в каждый исходный файл, где

// ---- primer.h ---- namespace cplusplus_primer {

class matrix { /* ... */ };

// объявления функций

extern matrix operator+ ( const matrix &m1, const matrix &m2 );

extern void inverse( matrix & );

// объявления объектов

extern bool error_state;

они используются.

}

2.   Определения этих членов помещают в исходный файл, содержащий реализацию:

 

// ---- primer.C ----

#include "primer.h"

namespace cplusplus_primer {

// определения функций

void inverse( matrix & )

{ /* ... */ }

matrix operator+ ( const matrix &ml, const    matrix &m2     )

{ /" ... */ }

// определения объектов

bool error_state = false;

}

Для объявления объекта без его определения используется ключевое слово extern, как и в случае такого объявления в глобальной области видимости.

8.5.6. Безымянные пространства имен

Может  возникнуть  необходимость  определить  объект,  функцию,  класс  или  любую другую сущность так, чтобы она была видимой только в небольшом участке программы. Это еще один способ решения проблемы засорения глобального пространства имен. Поскольку мы уверены, что эта сущность используется ограниченно, можно не тратить время на выдумывание уникального имени. Если мы объявляем объект внутри функции или блока, его имя видимо только в этом блоке. А как сделать некоторую сущность доступной нескольким функциям, но не всей программе?

Предположим,  мы  хотим  реализовать  набор  функций  для  сортировки  вектора  типа

// ----- SortLib.h -----

void quickSort( double *, double * );

void bubbleSort( double *, double * );

void mergeSort( double *, double * );

double:

void heapSort( double *, double * );

Все они  используют  одну и  ту же функцию  swap() для  того,  чтобы  менять  местами элементы  вектора.  Однако  она  не должна  быть  видна  во  всей  программе,  поскольку нужна  только   четырем   названным   функциям.   Локализуем   ее  в  файле  SortLib.C.

// ----- SortLib.C -----

void swap( double *dl, double *d2 ) { /* ... */ }

// только эти функции используют swap()

void quickSort( double *d1, double *d2 ) { /* ... */ } void bubbleSort( double *d1, double *d2 ) { /* ... */ } void mergeSort( double *d1, double *d2 ) { /* ... */ }

Приведенный код не дает желаемого результата. Как вы думаете, почему?

void heapSort( double *d1, double *d2 ) { /* ... */ }

 

Хотя функция  swap() определена в файле SortLib.C и не появляется в заголовочном файле SortLib.h, где содержится описание интерфейса библиотеки сортировки, она объявлена  в  глобальной  области  видимости.  Следовательно,  это  имя  является глобальным, при этом сохраняется возможность конфликта с другими именами.

Язык С++ предоставляет  возможность  использования безымянного  пространства имен для объявления сущности, локальной по отношению к файлу. Определение такого пространства начинается ключевым словом namespace. Очевидно, что никакого имени за этим  словом  нет,  а  сразу  же идет  блок  в  фигурных  скобках,  содержащий  различные

// ----- SortLib.C ----- namespace {

void swap( double *dl, double *d2 ) { /* ... */ }

}

объявления. Например:

// определения функций сортировки не изменяются

Функция swap() видна только в файле SortLib.C. Если в другом файле в безымянном пространстве  имен  содержится  определение  swap(),  то  это  другая  функция.  Наличие двух  функций  swap() не  является  ошибкой,  поскольку  они  различны.  Безымянные пространства имен отличаются от прочих: определение такого пространства локально для одного файла и не может размещаться в нескольких.

Имя  swap() может  употребляться  в неквалифицированной  форме в файле SortLib.C

после определения безымянного пространства. Оператор разрешения области видимости

void quickSort( double *d1, double *d2 ) {

// ...

double* elem = d1;

// ...

// ссылка на член безымянного пространства имен swap()

swap( d1, elem );

// ...

для ссылки на его члены не нужен.

}

Члены безымянного пространства имен относятся к сущностям программы. Поэтому функция swap() может быть вызвана во время выполнения. Однако имена этих членов видны только внутри одного файла.

До того как в стандарте С++ появилось понятие пространства имен, наиболее удачным решением проблемы локализации было использование ключевого слова static, унаследованного из С. Член безымянного пространства имеет свойства, аналогичные глобальной сущности, объявленной как static. В языке С такая сущность невидима вне файла,  в котором  объявлена.  Например,  текст  из SortLib.C можно переписать  на  С,

// SortLib.C

// swap() невидима для других файлов программы

static void swap( double *d1, double *d2 ) { /* ... */ }

сохранив свойства swap():

 

// определения функций сортировки такие же, как и раньше

Во многих программах на С++ используются объявления с ключевым словом  static. Предполагается, что они должны быть заменены безымянными пространствами имен по мере того, как все большее число компиляторов начнет поддерживать это понятие.

Упражнение 8.11

Зачем нужно определять собственное пространство имен в программе?

Упражнение 8.12

Имеется  следующее  объявление  operator*(),  члена  вложенного  пространства  имен

namespace cplusplus_primer {

namespace MatrixLib {

class matrix { /*...*/ };

matrix operator* ( const matrix &, const matrix & );

// ...

}

cplusplus_primer::MatrixLib:

}

Как  определить  эту  функцию  в  глобальной  области  видимости?  Напишите  только прототип.

Упражнение 8.13

Объясните, зачем нужны безымянные пространства имен.

8.6. Использование членов пространства имен А

Использование квалифицированных имен при каждом обращении к членам пространств может  стать  обременительным,  особенно  если  имена  пространств  достаточно  длинны. Если бы удалось сделать их короче, то такие имена проще было бы читать и набивать. Однако употребление коротких имен увеличивает риск их совпадения с другими, поэтому желательно, чтобы в библиотеках применялись пространства с длинными именами.

К  счастью,  существуют  механизмы,  облегчающие  использование  членов  пространств имен   в   программах.   Псевдонимы   пространства   имен,   using-объявления   и   using- директивы помогают преодолеть неудобства работы с очень длинными именами.

8.6.1. Псевдонимы пространства имен

Псевдоним  пространства  имен  используется  для  задания  короткого  синонима  имени

namespace International_Business_Machines

пространства. Например, длинное имя

{ /* ... */ }

может быть ассоциировано с более коротким синонимом:

 

namespace IBM = International_Business_Machines;

Объявление псевдонима начинается ключевым словом namespace, за которым следует короткий  псевдоним,  а за ним – знак равенства  и исходное полное имя пространства. Если полное имя не соответствует никакому известному пространству, это ошибка.

Псевдоним  может относиться и к вложенному пространству имен. Вспомним слишком

#include "primer.h"

// трудно читать!

void func( cplusplus_primer::MatrixLib::matrix &m )

{

// ... cplusplLis_primer::MatrixLib::inverse( m ); return m;

длинное определение функции func() выше:

}

Разрешается   задать псевдоним     для      обозначения   вложенного

cplusplLis_primer::MatrixLib,   сделав   определение   функции   более  удобным   для

#include "primer.h"

// более короткий псевдоним

namespace mlib = cplusplus_primer::MatrixLib;

// читать проще!

void func( mlib::matrix &m )

{

// ... mlib::inverse( m ); return m;

восприятия:

}

Одно   пространство   имен   может   иметь   несколько   взаимозаменяемых   псевдонимов.

Например,   если   псевдоним   Lib ссылается   на   cplusplus_primer,   то  определение

// псевдоним alias относится к пространству имен cplusplus_primer namespace alias = Lib;

void func( cplusplus_primer::matrix &m ) {

// ...

alias::inverse( m );

return m;

функции func() может выглядеть и так:

}

 

8.6.2. Using-объявления

Имеется механизм, позволяющий обращаться к членам пространства имен, используя их имена без квалификатора, т.е. без префикса namespace_name::. Для этого применяются using-объявления.

Using-объявление     начинается     ключевым     словом    using,    за    которым    следует

namespace cplusplus_primer {

namespace MatrixLib {

class matrix { /* ... */ };

// ...

}

}

// using-объявление для члена matrix

квалифицированное имя члена пространства. Например:

using cplusplus_primer::MatrixLib::matrix;

Using-объявление вводит имя в ту область видимости, в которой оно использовано. Так,

предыдущее using-объявление делает имя matrix глобально видимым.

После того как это объявление встретилось в программе, использование имени matrix в глобальной  области  видимости  или  во вложенных  в  нее областях  относится  к  этому члену пространства имен. Пусть далее идет следующее объявление:

void func( matrix &m );

Оно  вводит  функцию  func() с  параметром  типа  cplusplus_primer:: MatrixLib::matrix.

Using-объявление ведет себя подобно любому другому объявлению: оно имеет область видимости, и имя, введенное им, можно употреблять начиная с места объявления и до конца области видимости. Using-объявление может использоваться в глобальной области видимости,  равно как и в области видимости любого пространства имен. Оно употребляется и в локальной области. Имя, вводимое using-объявлением, как и любым другим, имеет следующие характеристики:

оно должно быть уникальным в своей области видимости;

оно скрывает одноименную сущность во внешней области;

оно скрывается объявлением одноименной сущности во вложенной области.

Например:

 

namespace blip {

int bi = 16, bj = 15, bk = 23;

// прочие объявления

}

int bj = 0;

void manip() {

using blip::bi; // bi в функции manip() ссылается на blip::bi

++bi;    // blip::bi == 17

using blip::bj; // скрывает глобальную bj

// bj в функции manip()ссылается на blip::bj

++bj;    // blip::bj == 16

int bk;  // объявление локальной bk

using blip::bk; // ошибка: повторное определение bk в manip()

}

int wrongInit = bk; // ошибка: bk невидима

// надо использовать blip::bk

Using-объявления в функции manip() позволяют ссылаться на членов пространства blib с помощью неквалифицированных имен. Такие объявления не видны вне manip(), и неквалифицированные имена могут применяться только внутри этой функции. Вне ее необходимо употреблять квалифицированные имена.

Using-объявление упрощает использование членов пространства имен. Оно вводит только одно имя. Using-объявление может находиться в определенной области видимости, и, значит, мы способны точно указать, в каком месте программы те или иные члены разрешается употреблять без дополнительной квалификации.

В следующем подразделе мы расскажем, как ввести в определенную область видимости все члены некоторого пространства имен.

8.6.3. Using-директивы

Пространства имен появились в стандартном С++. Предыдущие версии С++ их не поддерживали, и, следовательно, поставляемые библиотеки не помещали глобальные объявления в пространства имен. Множество программ на С++ было написано еще до того, как компиляторы стали поддерживать такую опцию. Заключая содержимое библиотеки  в  пространство  имен,  мы  можем  испортить  старое  приложение, использующее ее предыдущие версии: все имена из этой библиотеки становятся квалифицированными, т.е. должны включать имя пространства вместе с оператором разрешения области видимости. Те приложения, в которых эти имена употребляются в неквалифицированной форме, перестают компилироваться.

Сделать видимыми имена из библиотеки, используемой в нашей программе, можно с помощью  using-объявления.  Предположим,  что  файл  primer.h содержит  интерфейс новой версии библиотеки, в котором глобальные объявления помещены в пространство имен  cplusplus_primer.  Нужно  заставить  нашу  программу  работать  с  новой библиотекой. Два using-объявления сделают видимыми имена класса matrix и функции inverse() из пространства cplusplus_primer:

 

#include "primer.h"

using cplusplus_primer::matrix;

using cplusplus_primer::inverse;

// using-объявления позволяют использовать

// имена matrix и inverse без спецификации

void func( matrix &m ) {

// ... inverse( m ); return m;

}

Но если библиотека достаточно велика и приложение часто использует имена из нее, то для подгонки имеющегося кода к новой библиотеке может потребоваться много using- объявлений. Добавлять их все только для того, чтобы старый код скомпилировался и заработал, утомительно и чревато ошибками. Решить эту проблему помогают using- директивы, облегчающие переход на новую версию библиотеки, где впервые стали применяться пространства имен.

Using-директива начинается ключевым словом using, за которым следует ключевое слово namespace, а затем имя некоторого пространства имен. Это имя должно ссылаться на определенное ранее пространство, иначе компилятор выдаст ошибку. Using-директива позволяет сделать все имена из этого пространства видимыми в неквалифицированной форме.

#include "pnmer.h"

// using-директива: все члены cplusplus_primer

// становятся видимыми

using namespace cplusplus_primer;

// имена matrix и inverse можно использовать без спецификации

void func( matrix &m ) {

// ...

inverse( m );

return m;

Например, предыдущий фрагмент кода может быть переписан так:

}

Using-директива делает имена членов пространства имен видимыми за его пределами, в том   месте,   где  она   использована.   Например,   приведенная   using-директива   создает иллюзию  того,  что  все  члены  cplusplus_primer объявлены  в  глобальной  области видимости перед определением func(). При этом члены пространства имен не получают

namespace A {

int i, j;

локальных псевдонимов, а как бы перемещаются в новую область видимости. Код

}

выглядит как

 

int i, J;

для   фрагмента   программы,   содержащего   в   области   видимости   следующую   using-

директиву:

using namespace A;

Рассмотрим пример, позволяющий  подчеркнуть разницу между using-объявлением (которое сохраняет пространство имен, но создает ассоциированные с его членами локальные   синонимы)   и   using-директивой   (которая   полностью   удаляет   границы

namespace blip {

int bi = 16, bj = 15, bk = 23;

// прочие объявления

}

int bj = 0;

void manip() {

using namespace blip; // using-директива -

// коллизия имен ::bj and blip::bj

// обнаруживается только при

// использовании bj

++bi;    // blip::bi == 17

++bj;    // ошибка: неоднозначность

// глобальная bj или blip::bj?

++::bj;  // правильно: глобальная bj == 1

++blip::bj;        // правильно: blip::bj == 16

int bk = 97;      // локальная bk скрывает blip::bk

++bk;   // локальная bk == 98

пространства имен).

}

Во-первых, using-директивы имеют область видимости. Такая директива в функции manip() относится только к блоку этой функции. Для manip() члены пространства имен blip выглядят  так,  как  будто  они  объявлены  в  глобальной  области  видимости,  а следовательно, можно использовать их неквалифицированные имена. Вне этой функции необходимо употреблять квалифицированные.

Во-вторых, ошибки неоднозначности, вызванные применением using-директивы, обнаруживают себя при реальном обращении к такому имени, а не при встрече в тексте самой этой директивы. Например, переменная bj, член пространства blib, выглядит для manip() как  объявленная   в   глобальной   области   видимости,   вне  blip.   Однако  в глобальной области уже есть такая переменная. Возникает неоднозначность имени bj в функции manip(): оно относится и к глобальной переменной, и к члену пространства blip. Ошибка проявляется только при упоминании bj в функции manip(). Если бы это имя вообще не использовалось в manip(), коллизия не проявилась бы.

В-третьих,   using-директива   не  затрагивает   употребление   квалифицированных   имен. Когда в manip() упоминается ::bj, имеется в виду переменная из глобальной области видимости, а blip::bj обозначает переменную из пространства имен blip.

И наконец члены пространства blip выглядят для функции manip() так, как будто они объявлены в глобальной области видимости. Это означает, что локальные объявления внутри     manip() могут   скрывать   имена   членов   пространства   blip.   Локальная

 

переменная   bk скрывает   blip::bk.   Ссылка   на   bk внутри   manip() не   является неоднозначной – речь идет о локальной переменной.

Using-директивы использовать очень просто: стоит написать одну такую директиву, и все члены пространства имен сразу становятся видимыми. Однако чрезмерное увлечение ими

namespace cplusplus_primer {

class matrix { };

// прочие вещи ...

}

namespace DisneyFeatureAnimation {

class matrix { };

// здесь тоже ...

using namespace cplusplus_primer;

using namespace DisneyFeatureAnimation;

matrix m; //ошибка, неоднозначность:

возвращает нас к старой проблеме засорения глобального пространства имен:

// cplusplus_primer::matrix или DisneyFeatureAnimation::matrix?

Ошибки  неоднозначности,   вызываемые  using-директивой,   обнаруживаются  только  в момент  использования.  В  данном  случае –  при  употреблении  имени  matrix.  Такая ошибка, найденная не сразу, может стать сюрпризом: заголовочные файлы не менялись и никаких новых объявлений  в программу добавлено не было. Ошибка появилась после того, как мы решили воспользоваться новыми средствами из библиотеки.

Using-директивы очень полезны при переводе приложений на новые версии библиотек, использующие пространства имен. Однако употребление большого числа using-директив возвращает  нас  к  проблеме  засорения  глобального  пространства  имен.  Эту  проблему можно свести к минимуму, если заменить using-директивы более селективными using- объявлениями. Ошибки неоднозначности, вызываемые ими, обнаруживаются в момент объявления. Мы рекомендуем пользоваться using-объявлениями, а не using-директивами, чтобы избежать засорения глобального пространства имен в своей программе.

8.6.4. Стандартное пространство имен std

Все  компоненты  стандартной  библиотеки  С++  находятся  в  пространстве  имен  std. Каждая  функция,  объект  и  шаблон  класса,  объявленные  в  стандартном  заголовочном файле, таком, как <vector> или <iostream>, принадлежат к этому пространству.

Если все компоненты библиотеки объявлены в std, то какая ошибка допущена в данном примере:

 

#include <vector>

#include <string>

#include <iterator>

int main()

{

// привязка istream_iterator к стандартному вводу

istream_iterator<string> infile( cin );

// istream_iterator, отмечающий end-of-stream istream_iterator<string> eos;

// инициализация svec элементами, считываемыми из cin vector<string> svec( infile, eos );

// ...

}

Правильно, этот фрагмент кода не компилируется, потому что члены пространства имен std должны использоваться с указанием их специфицированных имен. Для того чтобы исправить положение, мы можем выбрать один из следующих способов:

заменить имена членов пространства  std в этом примере соответствующими специфицированными именами;

применить  using-объявления,  чтобы  сделать  видимыми  используемые  члены пространства std;

употребить using-директиву, сделав видимыми все члены пространства std.

Членами  пространства  имен  std в  этом  примере  являются:  шаблон  класса istream_iterator,  стандартный  входной  поток  cin,  класс  string и  шаблон  класса vector.

Простейшее      решение –      добавить      using-директиву      после      директивы препроцессора #include:

using namespace std;

В  данном  примере  using-директива  делает  все  члены  пространства  std видимыми. Однако  не  все  они  нам  нужны.  Предпочтительнее  пользоваться  using-объявлениями, чтобы уменьшить вероятность коллизии имен при последующем добавлении в программу глобальных объявлений.

using std::istream_iterator;

using std::string;

using std::cin;

Using-объявления, необходимые для компиляции этого примера, таковы:

using std::vector;

Но куда их поместить? Если программа состоит из большого количества файлов, можно для  удобства  создать  заголовочный  файл,  содержащий  все  эти  using-объявления,  и включать   его   в   исходные   файлы   вслед   за   заголовочными   файлами   стандартной библиотеки.

 

В нашей книге мы не употребляли using-объявлений. Это сделано, во-первых, для того, чтобы сократить размер кода, а во-вторых, потому, что большинство примеров компилировались в реализации С++, не поддерживающей пространства имен. Подразумевается, что using-объявления указаны для всех членов пространства имен std, используемых в примерах.

Упражнение 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

void manip() {

//2

double dvar = 3.1416;

int iobj = limit + 1;

++ivar;

++::ivar;

Возьмем следующий фрагмент кода:

}

Каковы будут значения объявлений и выражений, если поместить using-объявления для всех членов пространства имен Exercise в точку //1? В точку //2? А если вместо using- объявлений использовать using-директиву?

 

 

9