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

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

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

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

652

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

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

// --- primer.h ---

namespace cplusplus_primer { class List {

// ...

private:

class ListItem { public:

void check_status(); int action();

// ...

};

};

}

// --- primer.C ---

#include "primer.h"

namespace cplusplus_primer {

//правильно: check_status() определено в том же пространстве имен,

//что и List

void List::ListItem::check_status() { }

}

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

//в пространстве имен, объемлющем определение класса List

//Имя члена квалифицировано именем пространства

возможность организовать код библиотеки следующим образом: int cplusplus_primer::List::ListItem::action() { }

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

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

Как происходит разрешение имени в определении члена, которое находится в

int cplusplus_primer::List::ListItem::action() { int local = someVal;

// ...

объявленном пользователем пространстве? Например, как будет разрешено someVal:

}

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

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

653

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

 

// --- primer.h ---

 

namespace cplusplus_primer {

 

class List {

 

// ...

 

private:

 

class ListItem {

 

public:

 

int action();

 

// ...

 

};

 

};

 

const int someVal = 365;

 

}

 

// --- primer.C ---

 

#include "primer.h"

 

namespace cplusplus_primer {

 

int List::ListItem::action() {

//правильно: cplusplus_primer::someVal int local = someVal;

//ошибка: calc() еще не объявлена double result = calc( local );

//...

}

double calc(int) { }

// ...

action():

}

Определение пространства имен cplusplus_primer не является непрерывным. Определения класса List и объекта someVal размещены в первом его разделе, который находится в заголовочном файле primer.h. Определение функции calc() появляется в определении пространства имен, расположенном в файле реализации primer.C. Использование calc() внутри action() ошибочно, так как она объявлена после использования. Если calc() часть интерфейса cplusplus_primer, ее следовало бы

// --- primer.h ---

namespace cplusplus_primer { class List {

// ...

}

const int someVal = 365; double calc(int);

объявить в той части данного пространства, которая находится в заголовочном файле:

}

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

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

654

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

Довольно просто запомнить, в каком порядке просматриваются области видимости при поиске имени из определения функции, расположенного вне определения класса. Имена, которыми квалифицировано имя члена, указывают порядок рассмотрения пространств. Например, имя action() в предыдущем примере квалифицируется так:

cplusplus_primer::List::ListItem::action()

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

Класс, определенный в области видимости пространства имен, потенциально виден во всей программе. Если заголовочный файл primer.h включен в несколько исходных файлов, то имя cplusplus_primer::List везде относится к одному и тому же классу. Класс это сущность, для которой в программе может быть более одного определения. Определение класса должно присутствовать один раз в каждом исходном файле, где определяются или используются сам класс или его члены. Однако оно должно быть одинаковым во всех файлах, где встречается, поэтому его следует помещать в заголовочный файл, например primer.h. Затем такой файл можно включать в любой исходный, где определяются или используются члены класса. Это предотвратит несоответствия в случае, когда определение класса записывается более одного раза.

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

Упражнение 13.22

Используя класс iStack, определенный в упражнении 13.21, объявите классы

namespace LibException { class pushOnFull{ }; class popOnEmpty{ };

исключений pushOnFull и popOnEmpty как члены пространства имен LibException:

}

а сам iStack членом пространства имен Container. Модифицируйте соответствующим образом определение данного класса и его функций-членов, а также определение main().

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

655

13.12. Локальные классы A

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

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

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

вложенного класса в таком определении должно быть квалифицировано именем

void foo( int val )

{

class Bar { public:

int barVal;

class nested; // объявление вложенного класса обязательно

};

// определение вложенного класса class Bar::nexted {

// ...

};

объемлющего класса. Объявление вложенного класса в объемлющем нельзя опускать:

}

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

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

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

656

int a, val;

void foo( int val )

{

static int si;

enum Loc { a = 1024, b }; class Bar {

public:

Loc locVal;

// правильно

int barVal;

 

void fooBar ( Loc l = a ) { // правильно: Loc::a

barVal = val;

// ошибка: локальный объект

barVal = ::val;

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

barVal = si;

// правильно: статический локальный объект

locVal = b;

// правильно: элемент перечисления

}

 

};

 

// ...

 

}

 

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

Как всегда, если первое найденное объявление таково, что употребление имени оказывается некорректным, поиск других объявлений не производится. Несмотря на то что использование val в fooBar() выше является ошибкой, глобальная переменная val не будет найдена, если только ее имени не предшествует оператор разрешения глобальной области видимости.

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

657

14. Инициализация, присваивание и

уничтожение класса

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

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

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

14.1. Инициализация класса

class Data { public:

int ival; char *ptr;

Рассмотрим следующее определение класса:

};

Чтобы безопасно пользоваться объектом класса, необходимо правильно инициализировать его члены. Однако смысл этого действия для разных классов различен. Например, может ли ival содержать отрицательное значение или нуль? Каковы правильные начальные значения обоих членов класса? Мы не ответим на эти вопросы, не понимая абстракции, представляемой классом. Если с его помощью описываются служащие компании, то ptr, вероятно, указывает на фамилию служащего, а ival его уникальный номер. Тогда отрицательное или нулевое значения ошибочны. Если же класс представляет текущую температуру в городе, то допустимы любые значения ival. Возможно также, что класс Data представляет строку со счетчиком ссылок: в таком случае ival содержит текущее число ссылок на строку по адресу ptr. При такой абстракции ival инициализируется значением 1; как только значение становится равным 0, объект класса уничтожается.

Мнемонические имена класса и обоих его членов сделали бы, конечно, его назначение более понятным для читателя программы, но не дали бы никакой дополнительной

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

658

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

приведенных ниже инструкций представляет корректную инициализацию объекта класса

Data dat01( "Venus and the Graces", 107925 );

Data dat02( "about" );

Data dat03( 107925 );

Data:

Data dat04;

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

Обязан ли наш класс Data иметь конструктор? Нет, поскольку все его члены открыты. Унаследованный из языка C механизм поддерживает явную инициализацию,

int main()

{

//local1.ival = 0; local1.ptr = 0 Data local1 = { 0, 0 };

//local2.ival = 1024;

//local3.ptr = "Anna Livia Plurabelle" Data.local2 - { 1024, "Anna Livia Plurabelle" };

//...

аналогичную используемой при инициализации массивов:

}

Значения присваиваются позиционно, на основе порядка, в котором объявляются данные- члены. Следующий пример приводит к ошибке компиляции, так как ival объявлен перед

//ошибка: ival = "Anna Livia Plurabelle";

//ptr = 1024

ptr:

Data.local2 - { "Anna Livia Plurabelle", 1024 };

Явная инициализация имеет два основных недостатка. Во-первых, она может быть применена лишь для объектов классов, все члены которых открыты (т.е. эта инициализация не поддерживает инкапсуляции данных и абстрактных типов их не было в языке C, откуда она заимствована). А во-вторых, такая форма требует

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

659

вмешательства программиста, что увеличивает вероятность появления ошибок (забыл

включить список инициализации или перепутал порядок следования инициализаторов в нем).

Так нужно ли применять явную инициализацию вместо конструкторов? Да. Для

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

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

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

Примечание [O.A.4]: Нумера ция сносок сбита.

14.2. Конструктор класса

Среди других функций-членов конструктор выделяется тем, что его имя совпадает с

class Account { public:

//конструктор по умолчанию ...

Account();

//...

private:

char *_name;

unsigned int _acct_nmbr; double _balance;

именем класса. Для объявления конструктора по умолчанию мы пишем2:

};

Единственное синтаксическое ограничение, налагаемое на конструктор, состоит в том, что он не должен иметь тип возвращаемого значения, даже void. Поэтому следующие

// ошибки: у конструктора не может быть типа возвращаемого значения void Account::Account() { ... }

объявления ошибочны:

Account* Account::Account( const char *pc ) { ... }

1 Более подробное обсуждение этой темы с примерами и приблизительными оценками производительности см. в [LIPPMAN96a].

2 В реальной программе мы объявили бы член _name как имеющий тип string. Здесь он объявлен как C-строка, чтобы отложить рассмотрение вопроса об инициализации членов класса до раздела 14.4.

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

660

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

Откуда мы знаем, сколько и каких конструкторов определить? Как минимум, необходимо присвоить начальное значение каждому члену, который в этом нуждается. Например, номер счета либо задается явно, либо генерируется автоматически таким образом, чтобы гарантировать его уникальность. Предположим, что он будет создаваться автоматически. Тогда мы должны разрешить инициализировать оставшиеся два члена _name и _balance:

Account( const char *name, double open_balance );

Объект класса Account, инициализируемый конструктором, можно объявить следующим образом:

Account newAcct( "Mikey Matz", 0 );

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

Account( const char *name );

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

Account( const char *name, double open_balance = 0.0 );

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

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

предоставляет полный интерфейс для указания начальных значений тех членов класса

class Account { public:

//конструктор по умолчанию ...

Account();

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

Account( const char*, double=0.0 );

const char* name() { return name; }

//...

private:

//...

Account, которые могут быть инициализированы пользователем:

};

Ниже приведены два примера правильного определения объекта класса Account, где конструктору передается один или два аргумента:

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

661

int main()

{

//правильно: в обоих случаях вызывается конструктор

//с двумя параметрами

Account acct( "Ethan Stern" );

Account *pact = new Account( "Michael Lieberman", 5000 );

if ( strcmp( acct.name(), pact->name() ))

// ...

}

C++ требует, чтобы конструктор применялся к определенному объекту до его первого использования. Это означает, что как для acct, так и для объекта, на который указывает pact, конструктор будет вызван перед проверкой в инструкции if.

Компилятор перестраивает нашу программу, вставляя вызовы конструкторов. Вот как, по

//псевдокод на C++,

//иллюстрирующий внутреннюю вставку конструктора int main()

{

Account acct; acct.Account::Account("Ethan Stern", 0.0);

// ...

всей вероятности, будет модифицировано определение acct внутри main():

}

Конечно, если конструктор определен как встроенный, то он подставляется в точке вызова.

Обработка оператора new несколько сложнее. Конструктор вызывается только тогда, когда он успешно выделил память. Модификация определения pact в несколько

//псевдокод на C++,

//иллюстрирующий внутреннюю вставку конструктора при обработке new int main()

{

// ...

Account *pact; try {

pact = _new( sizeof( Account )); pact->Acct.Account::Account(

"Michael Liebarman", 5000.0);

}

catch( std::bad_alloc ) {

//оператор new закончился неудачей:

//конструктор не вызывается

}

// ...

упрощенном виде выглядит так:

}