Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
138
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

372

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

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().

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

С++ для начинающих

373

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

Из-за порядка просмотра областей видимости в процессе разрешения имен объявление из внешней области может быть скрыто объявлением того же имени во вложенной области.

Если бы в предыдущем примере переменная 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 )

{

// ...

}

в виде:

}

С++ для начинающих

374

Тем самым программисту запрещается применять управляющую переменную вне локальной области видимости цикла. Если нужно проверить 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

Найдите различные области видимости в следующем примере. Какие объявления ошибочны и почему?

С++ для начинающих

375

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, объявление функции устанавливает ее имя, а также тип возвращаемого значения и список параметров. Определение функции, помимо этой

С++ для начинающих

376

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

//объявление функции 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;

С++ для начинающих

377

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. Первый же запуск потерпел неудачу: даже самые простые тесты не проходили. Что случилось?

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

С++ для начинающих

378

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-объявлений объектов, объявлений функций и определений встроенных функций. Это называется локализацией

С++ для начинающих

379

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

Такие файлы позволяют добиться двух целей. Во-первых, гарантируется, что все

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

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

С++ для начинающих

380

говорит отсутствие ключевого слова extern. Включение такого заголовочного файла в два или более исходных файла одной программы вызовет ошибку связывания повторные определения объектов.

В файле token.h, приведенном выше, константа INLINE и встроенная функция is_relational() кажутся нарушающими правило. Однако это не так.

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

При возможности компилятор заменяет имя символической константы ее значением. Этот процесс называют подстановкой константы. Например, компилятор подставит 128 вместо INLINE везде, где это имя встретится в исходном файле. Для того чтобы компилятор произвел такую замену, определение константы (значение, которым она инициализирована) должно быть видимо в том месте, где она используется. Определение символической константы может появиться несколько раз в разных файлах, потому что в результирующем исполняемом файле благодаря подстановке оно будет только одно.

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

// ----- заголовочный файл -----

const int buf_chunk = 1024; extern char *const bufp;

// ----- исходный файл -----

объявления константы как extern. Например: char *const bufp = new char[buf_chunk];

Хотя bufp объявлена как const, ее значение не может быть вычислено во время компиляции (она инициализируется с помощью оператора new, который требует вызова библиотечной функции). Такая конструкция в заголовочном файле означала бы, что константа определяется каждый раз, когда этот заголовочный файл включается. Символическая константа это любой объект, объявленный со спецификатором const. Можете ли вы сказать, почему следующее объявление, помещенное в заголовочный файл,

// ошибка: не должно быть в заголовочном файле

вызывает ошибку связывания, если такой файл включается в два различных исходных? const char* msg = "?? oops: error: ";

Проблема вызвана тем, что msg не константа. Это неконстантный указатель, адресующий константу. Правильное объявление выглядит так (полное описание объявлений указателей см. в главе 3):

const char *const msg = "?? oops: error: ";

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

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

С++ для начинающих

381

Следовательно, встроенная функция, необходимая в нескольких исходных файлах, должна быть определена в заголовочном файле. Однако спецификация 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.Локальные объекты

Объявление переменной в локальной области видимости вводит локальный объект.

Существует три вида таких объектов: автоматические, регистровые и статические,

различающиеся временем жизни и характеристиками занимаемой памяти.