Механизм классов в C++ позволяет пользователям определять собственные типы данных. По этой причине их часто называют пользовательскими типами. Класс может наделять дополнительной функциональностью уже существующий тип. Так, например, IntArray, введенный в главе 2, предоставляет больше возможностей, чем тип "массив int". С помощью классов можно создавать абсолютно новые типы, например Screen (экран) или Account (расчетный счет). Как правило, классы используются для абстракций, не отражаемых встроенными типами адекватно.
В этой главе мы узнаем, как определять типы и использовать объекты классов; увидим, что определение класса вводит как данные-члены, описывающие его, так и функции-члены, составляющие набор операций, применимых к объектам класса. Мы покажем, как можно обеспечить сокрытие информации, объявив внутреннее представление и реализацию закрытыми, но открыв операции над объектами. Говорят, что закрытое внутреннее представление инкапсулировано, а открытую часть класса называют его интерфейсом.
Далее в этой главе мы познакомимся с особым видом членов класса – статическими членами. Мы расскажем также, как можно использовать указатели на члены и функции-члены класса, и рассмотрим объединения, представляющие собой специализированный вид класса для хранения объектов разных типов в одной области памяти. Завершается глава обсуждением области видимости класса и описанием правил разрешения имен в этой области; затрагиваются такие понятия, как вложенные классы, классы-члены пространства имен и локальные классы.
Определение класса состоит из двух частей: заголовка, включающего ключевое слово class, за которым следует имя класса, и тела, заключенного в фигурные скобки. После такого определения должны стоять точка с запятой или список объявлений:
class Screen { /* ... */ };
class Screen { /* ... */ } myScreen, yourScreen;
Внутри тела объявляются данные-члены и функции-члены и указываются уровни доступа к ним. Таким образом, тело класса определяет список его членов.
Каждое определение вводит новый тип данных. Даже если два класса имеют одинаковые списки членов, они все равно считаются разными типами:
class First {
int memi;
double memd;
};
class Second {
int memi;
double memd;
};
class First obj1;
Second obj2 = obj1; // ошибка: obj1 и obj2 имеют разные типы
Тело класса определяет отдельную область видимости. Объявление членов внутри тела помещает их имена в область видимости класса. Наличие в двух разных классах членов с одинаковыми именами – не ошибка, эти имена относятся к разным объектам. (Подробнее об областях видимости классов мы поговорим в разделе 13.9.)
* После того как тип класса определен, на него можно ссылаться двумя способами: написать ключевое слово class, а после него – имя класса. В предыдущем примере объект obj1 класса First объявлен именно таким образом;
* указать только имя класса. Так объявлен объект obj2 класса Second из приведенного примера.
Оба способа сослаться на тип класса эквивалентны. Первый заимствован из языка C и остается корректным методом задания типа класса; второй способ введен в C++ для упрощения объявлений.
Данные-члены класса объявляются так же, как переменные. Например, у класса Screen могут быть следующие данные-члены:
#include
class Screen {
string _screen; // string( _height * _width )
string::size_type _cursor; // текущее положение на экране
short _height; // число строк
short _width; // число колонок
};
Поскольку мы решили использовать строки для внутреннего представления объекта класса Screen, то член _screen имеет тип string. Член _cursor – это смещение в строке, он применяется для указания текущей позиции на экране. Для него использован переносимый тип string::size_type. (Тип size_type рассматривался в разделе 6.8.)
Необязательно объявлять два члена типа short по отдельности. Вот объявление класса Screen, эквивалентное приведенному выше:
class Screen {
/*
* _ screen адресует строку размером _height * _width
* _cursor указывает текущую позицию на экране
* _height и _width - соответственно число строк и колонок
*/
string _screen;
string::size_type _cursor;
short _height, _width;
};
Член класса может иметь любой тип:
class StackScreen {
int topStack;
void (*handler)(); // указатель на функцию
vector stack; // вектор классов
};
Описанные данные-члены называются нестатическими. Класс может иметь также и статические данные-члены. (У них есть особые свойства, которые мы рассмотрим в разделе 13.5.)
Объявления данных-членов очень похожи на объявления переменных в области видимости блока или пространства имен. Однако их, за исключением статических членов, нельзя явно инициализировать в теле класса:
class First {
int memi = 0; // ошибка
double memd = 0.0; // ошибка
};
Данные-члены класса инициализируются с помощью конструктора класса. (Мы рассказывали о конструкторах в разделе 2.3; более подробно они рассматриваются в главе 14.)
Пользователям, по-видимому, понадобится широкий набор операций над объектами типа Screen: возможность перемещать курсор, проверять и устанавливать области экрана и рассчитывать его реальные размеры во время выполнения, а также копировать один объект в другой. Все эти операции можно реализовать с помощью функций-членов.
Функции-члены класса объявляются в его теле. Это объявление выглядит точно так же, как объявление функции в области видимости пространства имен. (Напомним, что глобальная область видимости – это тоже область видимости пространства имен. Глобальные функции рассматривались в разделе 8.2, а пространства имен – в разделе 8.5.) Например:
class Screen {
public:
void home();
void move( int, int );
char get();
char get( int, int );
void checkRange( int, int );
// ...
};
Определение функции-члена также можно поместить внутрь тела класса:
class Screen {
public:
// определения функций home() и get()
void home() { _cursor = 0; }
char get() { return _screen[_cursor]; }
// ...
};
home() перемещает курсор в левый верхний угол экрана; get() возвращает символ, находящийся в текущей позиции курсора.
* Функции-члены отличаются от обычных функций следующим: функция-член объявлена в области видимости своего класса, следовательно, ее имя не видно за пределами этой области. К функции-члену можно обратиться с помощью одного из операторов доступа к членам – точки (.) или стрелки (-):
ptrScreen-home();
myScreen.home();
(в разделе 13.9 область видимости класса обсуждается более детально);
* функции-члены имеют право доступа как к открытым, так и к закрытым членам класса, тогда как обычным функциям доступны лишь открытые. Конечно, функции-члены одного класса, как правило, не имеют доступа к данным-членам другого класса.
Функция-член может быть перегруженной (перегруженные функции рассматриваются в главе 9). Однако она способна перегружать лишь другую функцию-член своего класса. По отношению к функциям, объявленным в других классах или пространствах имен, функция-член находится в отдельной области видимости и, следовательно, не может перегружать их. Например, объявление get(int, int) перегружает лишь get() из того же класса Screen:
class Screen {
public:
// объявления перегруженных функций-членов get()
char get() { return _screen[_cursor]; }
char get( int, int );
// ...
};
(Подробнее мы остановимся на функциях-членах класса в разделе 13.3.)
Часто бывает так, что внутреннее представление типа класса изменяется в последующих версиях программы. Допустим, опрос пользователей нашего класса Screen показал, что для его объектов всегда задается размер экрана 80 ? 24. В таком случае было бы желательно заменить внутреннее представление экрана менее гибким, но более эффективным:
class Screen {
public:
// функции-члены
private:
// инициализация статических членов (см. 13.5)
static const int _height = 24;
static const int _width = 80;
string _screen;
string::size_type _cursor;
};
Прежняя реализация функций-членов (то, как они манипулируют данными-членами класса) больше не годится, ее нужно переписать. Но это не означает, что должен измениться и интерфейс функций-членов (список формальных параметров и тип возвращаемого значения).
Если бы данные-члены класса Screen были открыты и доступны любой функции внутри программы, как отразилось бы на пользователях изменение внутреннего представления этого класса?
* все функции, которые напрямую обращались к данным-членам старого представления, перестали бы работать. Следовательно, пришлось бы отыскивать и изменять соответствующие части кода;
* так как интерфейс не изменился, то коды, манипулировавшие объектами класса Screen только через функции-члены, не пришлось бы модифицировать. Но поскольку сами функции-члены все же изменились, программу пришлось бы откомпилировать заново.
* Сокрытие информации – это формальный механизм, предотвращающий прямой доступ к внутреннему представлению типа класса из функций программы. Ограничение доступа к членам задается с помощью секций тела класса, помеченных ключевыми словами public, private и protected – спецификаторами доступа. Члены, объявленные в секции public, называются открытыми, а объявленные в секциях private и protected соответственно закрытыми или защищенными. открытый член доступен из любого места программы. Класс, скрывающий информацию, оставляет открытыми только функции-члены, определяющие операции, с помощью которых внешняя программа может манипулировать его объектами;
* закрытый член доступен только функциям-членам и друзьям класса. Класс, который хочет скрыть информацию, объявляет свои данные-члены закрытыми;
* защищенный член ведет себя как открытый по отношению к производному классу и как закрытый по отношению к остальной части программы. (В главе 2 мы видели пример использования защищенных членов в классе IntArray. Детально они рассматриваются в главе 17, где вводится понятие наследования.)
В следующем определении класса Screen указаны секции public и private:
class Screen {
public:
void home() { _cursor = 0; }
char get() { return _screen[_cursor]; }
char get( int, int );
void move( int, int );
// ...
private:
string _screen;
string::size_type _cursor;
short _height, _width;
};
Согласно принятому соглашению, сначала объявляются открытые члены класса. (Обсуждение того, почему в старых программах C++ сначала шли закрытые члены и почему этот стиль еще кое-где сохранился, см. в книге [LIPPMAN96a].) В теле класса может быть несколько секций public, protected и private. Каждая секция продолжается либо до метки следующей секции, либо до закрывающей фигурной скобки. Если спецификатор доступа не указан, то секция, непосредственно следующая за открывающей скобкой, по умолчанию считается private.
Иногда удобно разрешить некоторым функциям доступ к закрытым членам класса. Механизм друзей позволяет классу разрешать доступ к своим неоткрытым членам.
Объявление друга начинается с ключевого слова friend и может встречаться только внутри определения класса. Так как друзья не являются членами класса, то не имеет значения, в какой секции они объявлены. В примере ниже мы сгруппировали все подобные объявления сразу после заголовка класса:
class Screen {
friend istream&
operator( istream&, Screen& );
friend ostream&
operator( ostream&, const Screen& );
public:
// ... оставшаяся часть класса Screen
};
Операторы ввода и вывода теперь могут напрямую обращаться к закрытым членам класса Screen. Простая реализация оператора вывода выглядит следующим образом:
#include
ostream& operator
Другом может быть функция из пространства имен, функция-член другого класса или даже целый класс. В последнем случае всем его функциям-членам предоставляется доступ к неоткрытым членам класса, объявляющего дружественные отношения. (В разделе 15.2 друзья обсуждаются более подробно.)
О классе говорят, что он определен, как только встретилась скобка, закрывающая его тело. После этого становятся известными все члены класса, а следовательно, и его размер.
Можно объявить класс, не определяя его. Например:
class Screen; // объявление класса Screen
Это объявление вводит в программу имя Screen и указывает, что оно относится к типу класса.
Тип объявленного, но еще не определенного класса допустимо использовать весьма ограниченно. Нельзя определять объект типа класса, если сам класс еще не определен, поскольку размер класса в этом момент неизвестен и компилятор не знает, сколько памяти отвести под объект.
Однако указатель или ссылку на объект такого класса объявлять можно, так как они имеют фиксированный размер, не зависящий от типа. Но, поскольку размеры класса и его членов неизвестны, применять оператор разыменования (*) к такому указателю, а также использовать указатель или ссылку для обращения к члену не разрешается, пока класс не будет полностью определен.
Член некоторого класса можно объявить принадлежащим к типу какого-либо класса только тогда, когда компилятор уже видел определение этого класса. До этого объявляются лишь члены, являющиеся указателями или ссылками на такой тип. Ниже приведено определение StackScreen, один из членов которого служит указателем на Screen, который объявлен, но еще не определен:
class Screen; // объявление
class StackScreen {
int topStack;
// правильно: указатель на объект Screen
Screen *stack;
void (*handler)();
};
Поскольку класс не считается определенным, пока не закончилось его тело, то в нем не может быть данных-членов его собственного типа. Однако класс считается объявленным, как только распознан его заголовок, поэтому в нем допустимы члены, являющиеся ссылками или указателями на его тип. Например:
class LinkScreen {
Screen window;
LinkScreen *next;
LinkScreen *prev;
};
Упражнение 13.1
Пусть дан класс Person со следующими двумя членами:
string _name;
string _address;
и такие функции-члены:
Person( const string &n, const string &s )
: _name( n ), _address( a ) { }
string name() { return _name; }
string address() { return _address; }
Какие члены вы объявили бы в секции public, а какие – в секции private? Поясните свой выбор.
Упражнение 13.2
Объясните разницу между объявлением и определением класса. Когда вы стали бы использовать объявление класса? А определение?
Определение класса, например Screen, не приводит к выделению памяти. Память выделяется только тогда, когда определяется объект типа класса. Так, если имеется следующая реализация Screen:
class Screen {
public:
// функции-члены
private:
string _screen;
string:size_type _cursor;
short _height;
short _width;
};
то определение
Screen myScreen;
выделяет область памяти, достаточную для хранения четырех членов Screen. Имя myScreen относится к этой области. У каждого объекта класса есть собственная копия данных-членов. Изменение членов myScreen не отражается на значениях членов любого другого объекта типа Screen.
Область видимости объекта класса зависит от его положения в тексте программы. Он определяется в иной области, нежели сам тип класса:
class Screen {
// список членов
};
int main()
{
Screen mainScreen;
}
Тип Screen объявлен в глобальной области видимости, тогда как объект mainScreen – в локальной области функции main().
Объект класса также имеет время жизни. В зависимости от того, где (в области видимости пространства имен или в локальной области) и как (статическим или нестатическим) он объявлен, он может существовать в течение всего времени выполнения программы или только во время вызова некоторой функции. Область видимости объекта класса и его время жизни ведут себя очень похоже. (Понятия области видимости и времени жизни введены в главе 8.)
Объекты одного и того же класса можно инициализировать и присваивать друг другу. По умолчанию копирование объекта класса эквивалентно копированию всех его членов. Например:
Screen bufScreen = myScreen;
// bufScreen._height = myScreen._height;
// bufScreen._width = myScreen._width;
// bufScreen._cursor = myScreen._cursor;
// bufScreen._screen = myScreen._screen;
Указатели и ссылки на объекты класса также можно объявлять. Указатель на тип класса разрешается инициализировать адресом объекта того же класса или присвоить ему такой адрес. Аналогично ссылка инициализируется l-значением объекта того же класса. (В объектно-ориентированном программировании указатель или ссылка на объект базового класса могут относиться и к объекту производного от него класса.)
int main()
{
Screen myScreen, bufScreen[10];
Screen *ptr = new Screen;
myScreen = *ptr;
delete ptr;
ptr = bufScreen;
Screen &ref = *ptr;
Screen &ref2 = bufScreen[6];
}
По умолчанию объект класса передается по значению, если он выступает в роли аргумента функции или ее возвращаемого значения. Можно объявить формальный параметр функции или возвращаемое ею значение как указатель или ссылку на тип класса. (В разделе 7.3 были представлены параметры, являющиеся указателями или ссылками на типы классов, и объяснялось, когда их следует использовать. В разделе 7.4 с этой точки зрения рассматривались типы возвращаемых значений.)
Для доступа к данным или функциям-членам объекта класса следует пользоваться соответствующими операторами. Оператор "точка" (.) применяется, когда операндом является сам объект или ссылка на него; а "стрелка"(-) – когда операндом служит указатель на объект:
#include "Screen.h"
bool isEqual( Screen& s1, Screen *s2 )
{ // возвращает false, если объекты не равны, и true - если равны
if (s1.height() != s2-height() ||
s2.width() != s2-width() )
return false;
for ( int ix = 0; ix s1.height(); ++ix )
for ( int jy = 0; jy s2-width(); ++jy )
if ( s1.get( ix, jy ) != s2-get( ix, jy ) )
return false;
return true; // попали сюда? значит, объекты равны
}
isEqual() – это не являющаяся членом функция, которая сравнивает два объекта Screen. У нее нет права доступа к закрытым членам Screen, поэтому напрямую обращаться к ним она не может. Сравнение проводится с помощью открытых функций-членов данного класса.
Для получения высоты и ширины экрана isEqual() должна пользоваться функциями-членами height() и width() для чтения закрытых членов класса. Их реализация тривиальна:
class Screen {
public:
int height() { return _height; }
int width() { return _width; }
// ...
private:
short _heigh, _width;
// ...
};
Применение оператора доступа к указателю на объект класса эквивалентно последовательному выполнению двух операций: применению оператора разыменования (*) к указателю, чтобы получить адресуемый объект, и последующему применению оператора "точка" для доступа к нужному члену класса. Например, выражение
s2-height()
можно переписать так:
(*s2).height()
Результат будет одним и тем же.
Функции-члены реализуют набор операций, применимых к объектам класса. Например, для Screen такой набор состоит из следующих объявленных в нем функций-членов:
class Screen {
public:
void home() { _cursor = 0; }
char get() { return _screen[_cursor]; }
char get( int, int );
void move( int, int );
bool checkRange( int, int );
int height() { return _height; }
int width() { return _width; }
// ...
};
Хотя у любого объекта класса есть собственная копия всех данных-членов, каждая функция-член существует в единственном экземпляре:
Screen myScreen, groupScreen;
myScreen.home();
groupScreen.home();
При вызове функции home() для объекта myScreen происходит обращение к его члену _cursor. Когда же эта функция вызывается для объекта groupScreen, то она обращается к члену _cursor именно этого объекта, причем сама функция home() одна и та же. Как же может одна функция-член обращаться к данным-членам разных объектов? Для этого применяется указатель this, рассматриваемый в следующем разделе.
Обратите внимание, что определения функций home(), get(), height() и width() приведены прямо в теле класса. Такие функции называются встроенными. (Мы говорили об этом в разделе 7.6.)
Функции-члены можно объявить в теле класса встроенными и явно, поместив перед типом возвращаемого значения ключевое слово inline:
class Screen {
public:
// использование ключевого слова inline
// для объявления встроенных функций-членов
inline void home() { _cursor = 0; }
inline char get() { return _screen[_cursor]; }
// ...
};
Определения home() и get() в приведенных примерах эквивалентны. Поскольку ключевое слово inline избыточно, мы в этой книге не пишем его явно для функций-членов, определенных в теле класса.
Функции-члены, состоящие из двух или более строк, лучше определять вне тела. Для идентификации функции как члена некоторого класса требуется специальный синтаксис объявления: имя функции должно быть квалифицировано именем ее класса. Вот как выглядит определение функции checkRange(), квалифицированное именем Screen:
#include iostream
#include "screen.h"
// имя функции-члена квалифицировано именем Screen::
bool Screen::checkRange( int row, int col )
{ // проверить корректность координат
if ( row 1 || row _height ||
col 1 || col _width ) {
cerr "Screen coordinates ( "
row ", " col
" ) out of bounds.\n";
return false;
}
return true;
}
Прежде чем определять функцию-член вне тела класса, необходимо объявить ее внутри тела, обеспечив ее видимость. Например, если бы перед определением функции checkRange() не был включен заголовочный файл Screen.h, то компилятор выдал бы сообщение об ошибке. Тело класса определяет полный список его членов. Этот список не может быть расширен после закрытия тела.
Обычно функции-члены, определенные вне тела класса, не делают встроенными. Но объявить такую функцию встроенной можно, если явно добавить слово inline в объявление функции внутри тела класса или в ее определение вне тела, либо сделав то и другое одновременно. В следующем примере move() определена как встроенная функция-член класса Screen:
inline void Screen::move( int r, int c )
{ // переместить курсор в абсолютную позицию
if ( checkRange( r, c ) ) // позиция на экране задана корректно?
{
int row = (r-1) * _width; // смещение начала строки
_cursor = row + c - 1;
}
}
Функция get(int, int) объявляется встроенной с помощью слова inline:
class Screen {
public:
inline char get( int, int );
// объявления других функций-членов не изменяются
};
Определение функции следует после объявления класса. При этом слово inline можно опустить:
char Screen::get( int r, int c )
{
move( r, c ); // устанавливаем _cursor
return get(); // вызываем другую функцию-член get()
}
Так как встроенные функции-члены должны быть определены в каждом исходном файле, где они вызываются, то встроенную функцию, не определенную в теле класса, следует поместить в тот же заголовочный файл, в котором определен ее класс. Например, представленные ранее определения move() и get() должны находиться в заголовочном файле Screen.h после определения класса Screen.
Говорят, что определение функции-члена принадлежит области видимости класса независимо от того, находится ли оно вне или внутри его тела. Отсюда следуют два вывода:
* в определении функции-члена могут быть обращения к любым членам класса, открытым или закрытым, и это не нарушает ограничений доступа;
* когда функция-член обращается к членам класса, операторы доступа "точка" и "стрелка" не необходимы.
Например:
#include string
void Screen::copy( const Screen &sobj )
{
// если этот объект и объект sobj - одно и то же,
// копирование излишне
// мы анализируем указатель this (см. раздел 13.4)
if ( this != &sobj )
{
_height = sobj._height;
_width = sobj._width;
_cursor = 0;
// создаем новую строку;
// ее содержимое такое же, как sobj._screen
_screen = sobj._screen;
}
}
Хотя _screen, _height, _width и _cursor являются закрытыми членами класса Screen, функция-член copy() работает с ними напрямую. Если при обращении к члену отсутствует оператор доступа, то считается, что речь идет о члене того класса, для которого функция-член вызвана. Если вызвать copy() следующим образом:
#include quot;Screen.hquot;
int main()
{
Screen s1;
// Установить s1
Screen s2;
s2.copy(s1);
// ...
}
то параметр sobj внутри определения copy() соотносится с объектом s1 из функции main(). Функция-член copy() вызвана для объекта s2, стоящего перед оператором “точка”. Для такого вызова члены _screen, _height, _width и _cursor, при обращении к которым внутри определения этой функции нет оператора доступа, – это члены объекта s2. В следующем разделе мы рассмотрим доступ к членам класса внутри определения функции-члена более подробно и, в частности, покажем, как для поддержки такого доступа применяется указатель this.
Функцию-член можно объявить в любой из секций public, private или protected тела класса. Где именно это следует делать? Открытая функция-член задает операцию, которая может понадобиться пользователю. Множество открытых функций-членов составляет интерфейс класса. Например, функции-члены home(), move() и get() класса Screen определяют операции, с помощью которых программа манипулирует объектами этого типа.,/p
Поскольку мы прячем от пользователей внутреннее представление класса, объявляя его члены закрытыми, то для манипуляции объектами типа Screen необходимо предоставить открытые функции-члены. Такой прием – сокрытие информации – защищает написанный пользователем код от изменений во внутреннем представлении.
Внутреннее состояние объекта класса также защищено от случайных изменений. Все модификации объекта производятся с помощью небольшого набора функций, что существенно облегчает сопровождение и доказательство правильности программы.
До сих пор мы встречались лишь с функциями, поддерживающими доступ к закрытым членам только для чтения. Ниже приведены две функции set(), позволяющие пользователю модифицировать объект Screen. Добавим их объявления в тело класса:
class Screen {
public:
void set( const string &s );
void set( char ch );
// объявления других функций-членов не изменяются
};
Далее следуют определения функций:
void Screen::set( const string &s )
{ // писать в строку, начиная с текущей позиции курсора
int space = remainingSpace();
int len = s.size();
if ( space len ) {
cerr "Screen: warning: truncation:"
"space: " space
"string length: " len endl;
len = space;
}
_screen.replace( _cursor, len, s );
_cursor += len - 1;
}
void Screen::set( char ch )
{
if ( ch == '\0' )
cerr "Screen: warning: "
"null character (ignored).\n";
else _screen[_cursor] = ch;
}
В реализации класса Screen мы предполагаем, что объект Screen не содержит двоичных нулей. По этой причине set() не позволяет записать на экран нуль.
Представленные до сих пор функции-члены были открытыми, их можно вызывать из любого места программы, а закрытые вызываются только из других функций-членов (или друзей) класса, но не из программы, обеспечивая поддержку другим операциям в реализации абстракции класса. Примером может служить функция-член remainingSpace класса Screen(), использованная в set(const string&).
class Screen {
public:
// объявления других функций-членов не изменяются
private:
inline int remainingSpace();
};
remainingSpace() сообщает, сколько места осталось на экране:
inline int Screen::remainingSpace()
{
int sz = _width * _height;
return ( sz - _cursor );
}
(Детально защищенные функции-члены будут рассмотрены в главе 17.)
Следующая программа предназначена для тестирования описанных к настоящему моменту функций-членов:
#include "Screen.h"
#include iostream
int main() {
Screen sobj(3,3); // конструктор определен в разделе 13.3.4
string init("abcdefghi");
cout "Screen Object ( "
sobj.height() ", "
sobj.width() " )\n\n";
// Задать содержимое экрана
string::size_type initpos = 0;
for ( int ix = 1; ix = sobj.width(); ++ix )
for ( int iy = 1; iy = sobj.height(); ++iy )
{
sobj.move( ix, iy );
sobj.set( init[ initpos++ ] );
}
// Напечатать содержимое экрана
for ( int ix = 1; ix = sobj.width(); ++ix )
{
for ( int iy = 1; iy = sobj.height(); ++iy )
cout sobj.get( ix, iy );
cout "\n";
}
return 0;
}
Откомпилировав и запустив эту программу, мы получим следующее:
Screen Object ( 3, 3 )
abc
def
ghi
Существует специальная категория функций-членов, отвечающих за такие действия с объектами, как инициализация, присваивание, управление памятью, преобразование типов и уничтожение. Такие функции называются конструкторами. Они вызываются компилятором неявно каждый раз, когда объект класса определяется или создается оператором new. В объявлении конструктора его имя совпадает с именем класса. Вот, например, объявление конструктора класса Screen, в котором заданы значения по умолчанию для параметров hi, wid и bkground:
class Screen {
public:
Screen( int hi = 8, int wid = 40, char bkground = '#');
// объявления других функций-членов не изменяются
};
Определение конструктора класса Screen выглядит так:
Screen::Screen( int hi, int wid, char bk ) :
_height( hi ), // инициализировать _height значением hi
_width( wid ), // инициализировать _width значением wid
_cursor ( 0 ), // инициализировать _cursor нулем
_screen( hi * wid, bk ) // размер экрана равен hi * wid
// все позиции инициализируются
// символом '#'
{ // вся работа проделана в списке инициализации членов
// этот список обсуждается в разделе 14.5
}
Каждый объявленный объект класса Screen автоматически инициализируется конструктором:
Screen s1; // Screen(8,40,'#')
Screen *ps = new Screen( 20 ); // Screen(20,40,'#')
int main() {
Screen s(24,80,'*'); // Screen(24,80,'*')
// ...
}
(В главе 14 конструкторы, деструкторы и операторы присваивания рассматриваются более подробно. В главе 15 обсуждаются конвертеры и функции управления памятью.)
Любая попытка модифицировать константный объект из программы обычно помечается компилятором как ошибка. Например:
const char blank = ' ';
blank = '\n'; // ошибка
Однако объект класса, как правило, не модифицируется программой напрямую. Вместо этого вызывается та или иная открытая функция-член. Чтобы не было "покушений" на константность объекта, компилятор должен различать безопасные (те, которые не изменяют объект) и небезопасные (те, которые пытаются это сделать) функции-члены:
const Screen blankScreen;
blankScreen.display(); // читает объект класса
blankScreen.set( '*' ); // ошибка: модифицирует объект класса
Проектировщик класса может указать, какие функции-члены не модифицируют объект, объявив их константными с помощью спецификатора const:
class Screen {
public:
char get() const { return _screen[_cursor]; }
// ...
};
Для класса, объявленного как const, могут быть вызваны только те функции-члены, которые также объявлены со спецификатором const. Ключевое слово const помещается между списком параметров и телом функции-члена. Для константной функции-члена, определенной вне тела класса, это слово должно присутствовать как в объявлении, так и в определении:
class Screen {
public:
bool isEqual( char ch ) const;
// ...
private:
string::size_type _cursor;
string _screen;
// ...
};
bool Screen::isEqual( char ch ) const
{
return ch == _screen[_cursor];
}
Запрещено объявлять константную функцию-член, которая модифицирует члены класса. Например, в следующем упрощенном определении:
class Screen {
public:
int ok() const { return _cursor; }
void error( int ival ) const { _cursor = ival; }
// ...
private:
string::size_type _cursor;
// ...
};
определение функции-члена ok() корректно, так как она не изменяет значения _cursor. В определении же error() значение _cursor изменяется, поэтому такая функция-член не может быть объявлена константной и компилятор выдает сообщение об ошибке:
error: cannot modify a data member within a const member function
ошибка: не могу модифицировать данные-члены внутри константной функции-члена
Если класс будет интенсивно использоваться, лучше объявить его функции-члены, не модифицирующие данных, константными. Однако наличие спецификатора const в объявлении функции-члена не предотвращает все возможные изменения. Такое объявление гарантирует лишь, что функции-члены не смогут изменять данные-члены, но если класс содержит указатели, то адресуемые ими объекты могут быть модифицированы константной функцией, не вызывая ошибки компиляции. Это часто приводит в недоумение начинающих программистов. Например:
#include cstring
class Text {
public:
void bad( const string &parm ) const;
private:
char *_text;
};
void Text::bad( const string &parm ) const
{
_text = parm.c_str(); // ошибка: нельзя модифицировать _text
for ( int ix = 0; ix parm.size(); ++ix )
_text[ix] = parm[ix]; // плохой стиль, но не ошибка
}
Модифицировать _text нельзя, но это объект типа char*, и символы, на которые он указывает, можно изменить внутри константной функции-члена класса Text. Функция-член bad() демонстрирует плохой стиль программирования. Константность функции-члена не гарантирует, что объекты внутри класса останутся неизменными после ее вызова, причем компилятор не поможет обнаружить такую ситуацию.
Константную функцию-член можно перегружать неконстантной функцией с тем же списком параметров:
class Screen {
public:
char get(int x, int y);
char get(int x, int y) const;
// ...
};
В этом случае наличие спецификатора const у объекта класса определяет, какая из двух функций будет вызвана:
int main() {
const Screen cs;
Screen s;
char ch = cs.get(0,0); // вызывает константную функцию-член
ch = s.get(0,0); // вызывает неконстантную функцию-член
}
Хотя конструкторы и деструкторы не являются константными функциями-членами, они все же могут вызываться для константных объектов. Объект становится константным после того, как конструктор проинициализирует его, и перестает быть таковым, как только вызывается деструктор. Таким образом, объект со спецификатором const трактуется как константный с момента завершения работы конструктора и до вызова деструктора.
Функцию-член можно также объявить со спецификатором volatile (он был введен в разделе 3.13). Объект класса объявляется как volatile, если его значение изменяется способом, который не обнаруживается компилятором (например, если это структура данных, представляющая порт ввода/вывода). Для таких объектов вызываются только функции-члены с тем же спецификатором, конструкторы и деструкторы:
class Screen {
public:
char poll() volatile;
// ...
};
char Screen::poll() volatile { ... }
При объявлении объекта класса Screen константным возникают некоторые проблемы. Предполагается, что после инициализации объекта Screen, его содержимое уже нельзя изменять. Но это не должно мешать нам читать содержимое экрана. Рассмотрим следующий константный объект класса Screen:
const Screen cs ( 5, 5 );
Если мы хотим прочитать символ, находящийся в позиции (3,4), то попробуем сделать так:
// прочитать содержимое экрана в позиции (3,4)
// Увы! Это не работает
cs.move( 3, 4 );
char ch = cs.get();
Но такая конструкция не работает: move() – это не константная функция-член, и сделать ее таковой непросто. Определение move() выглядит следующим образом:
inline void Screen::move( int r, int c )
{
if ( checkRange( r, c ) )
{
int row = (r-1) * _width;
_cursor = row + c - 1; // модифицирует _cursor
}
}
Обратите внимание, что move()изменяет член класса _cursor, следовательно, не может быть объявлена константной.
Но почему нельзя модифицировать _cursor для константного объекта класса Screen? Ведь _cursor – это просто индекс. Изменяя его, мы не модифицируем содержимое экрана, а лишь пытаемся установить позицию внутри него. Модификация _cursor должна быть разрешена несмотря на то, что у класса Screen есть спецификатор const.
Чтобы разрешить модификацию члена класса, принадлежащего константному объекту, объявим его изменчивым (mutable). Член с таким спецификатором не бывает константным, даже если он член константного объекта. Его можно обновлять, в том числе функцией-членом со спецификатором const. Объявлению изменчивого члена класса должно предшествовать ключевое слово mutable:
class Screen {
public:
// функции-члены
private:
string _screen;
mutable string::size_type _cursor; // изменчивый член
short _height;
short _width;
};
Теперь любая константная функция способна модифицировать _cursor, и move() может быть объявлена константной. Хотя move() изменяет данный член, компилятор не считает это ошибкой.
// move() - константная функция-член
inline void Screen::move( int r, int c ) const
{
// ...
// правильно: константная функция-член может модифицировать члены
// со спецификатором mutable
_cursor = row + c - 1;
// ...
}
Показанные в начале этого подраздела операции позиционирования внутри экрана теперь можно выполнить без сообщения об ошибке.
Отметим, что изменчивым объявлен только член _cursor, тогда как _screen, _height и _width не имеют спецификатора mutable, поскольку их значения в константном объекте класса Screen изменять нельзя.
Упражнение 13.3
Объясните, как будет вести себя copy() при следующих вызовах:
Screen myScreen;
myScreen.copy( myScreen );
Упражнение 13.4
К дополнительным перемещениям курсора можно отнести его передвижение вперед и назад на один символ. Из правого нижнего угла экрана курсор должен попасть в левый верхний угол. Реализуйте функции forward() и backward().
Упражнение 13.5
Еще одной полезной возможностью является перемещение курсора вниз и вверх на одну строку. По достижении верхней или нижней строки экрана курсор не перепрыгивает на противоположный край; вместо этого подается звуковой сигнал, и курсор остается на месте. Реализуйте функции up() и down(). Для подачи сигнала следует вывести на стандартный вывод cout символ с кодом '007'.
Упражнение 13.6
Пересмотрите описанные функции-члены класса Screen и объявите те, которые сочтете нужными, константными. Объясните свое решение.
У каждого объекта класса есть собственная копия данных-членов. Например:
int main() {
Screen myScreen( 3, 3 ), bufScreen;
myScreen.clear();
myScreen.move( 2, 2 );
myScreen.set( '*' );
myScreen.display();
bufScreen.resize( 5, 5 );
bufScreen.display();
}
У объекта myScreen есть свои члены _width, _height, _cursor и _screen, а у объекта bufScreen – свои. Однако каждая функция-член класса существует в единственном экземпляре. Их и вызывают myScreen и bufScreen.
В предыдущем разделе мы видели, что функция-член может обращаться к членам своего класса, не используя операторы доступа. Так, определение функции move() выглядит следующим образом:
inline void Screen::move( int r, int c )
{
if ( checkRange( r, c ) ) // позиция на экране задана корректно?
{
int row = (r-1) * _width; // смещение строки
_cursor = row + c - 1;
}
}
Если функция move() вызывается для объекта myScreen, то члены _width и _height, к которым внутри нее имеются обращения, – это члены объекта myScreen. Если же она вызывается для объекта bufScreen, то и обращения производятся к членам данного объекта. Каким же образом _cursor, которым манипулирует move(), оказывается членом то myScreen, то bufScreen? Дело в указателе this.
Каждой функции-члену передается указатель на объект, для которого она вызвана, – this. В неконстантной функции-члене это указатель на тип класса, в константной – константный указатель на тот же тип, а в функции со спецификатором volatile указатель с тем же спецификатором. Например, внутри функции-члена move() класса Screen указатель this имеет тип Screen*, а в неконстантной функции-члене List – тип List*.
Поскольку this адресует объект, для которого вызвана функция-член, то при вызове move() для myScreen он указывает на объект myScreen, а при вызове для bufScreen – на объект bufScreen. Таким образом, член _cursor, с которым работает функция move(), в первом случае принадлежит объекту myScreen, а во втором – bufScreen.
Понять все это можно, если представить себе, как компилятор реализует объект this. Для его поддержки необходимо две трансформации:
* Изменить определение функции-члена класса, добавив дополнительный параметр:
// псевдокод, показывающий, как происходит расширение
// определения функции-члена
// ЭТО НЕ КОРРЕКТНЫЙ КОД C++
inline void Screen::move( Screen *this, int r, int c )
{
if ( checkRange( r, c ) )
{
int row = (r-1) * this-_width;
this-_cursor = row + c - 1;
}
}
В этом определении использование указателя this для доступа к членам _width и _cursor сделано явным.
* Изменение каждого вызова функции-члена класса с целью передачи одного дополнительного аргумента – адреса объекта, для которого она вызвана:
myScreen.move( 2, 2 );
транслируется в
move( &myScreen, 2, 2 );
Программист может явно обращаться к указателю this внутри функции. Так, вполне корректно, хотя и излишне, определить функцию-член home() следующим образом:
inline void Screen::home()
{
this-_cursor = 0;
}
Однако бывают случаи, когда без такого обращения не обойтись, как мы видели на примере функции-члена copy() класса Screen. В следующем подразделе мы рассмотрим и другие примеры.
Наша функция main() вызывает функции-члены класса Screen для объектов myScreen и bufScreen таким образом, что каждое действие – это отдельная инструкция. У нас есть возможность определить функции-члены так, чтобы конкатенировать их вызовы при обращении к одному и тому же объекту. Например, все вызовы внутри main() будут выглядеть так:
int main() {
// ...
myScreen.clear().move( 2, 2 ), set( '*' ). display();
bufScreen.reSize( 5, 5 ).display();
}
Именно так интуитивно представляется последовательность операций с экраном: очистить экран myScreen, переместить курсор в позицию (2,2), записать в эту позицию символ '*' и вывести результат.
Операторы доступа "точка" и "стрелка" левоассоциативны, т.е. их последовательность выполняется слева направо. Например, сначала вызывается myScreen.clear(), затем myScreen.move() и т.д. Чтобы myScreen.move() можно было вызвать после myScreen.clear(), функция clear() должна возвращать объект myScreen, для которого она была вызвана. Мы уже видели, что доступ к объекту внутри функции-члена класса производится в помощью указателя this. Вот реализация clear():
// объявление clear() находится в теле класса
// в нем задан аргумент по умолчанию bkground = '#'
Screen& Screen::clear( char bkground )
{ // установить курсор в левый верхний угол и очистить экран
_cursor = 0;
_screen.assign( // записать в строку
_screen.size(), // size() символов
bkground // со значением bkground
);
// вернуть объект, для которого была вызвана функция
return *this;
}
Обратите внимание, что возвращаемый тип этой функции-члена – Screen& – ссылка на объект ее же класса. Чтобы конкатенировать вызовы, необходимо также пересмотреть реализацию move() и set(). Возвращаемый тип следует изменить с void на Screen&, а в определении возвращать *this.
Аналогично функцию-член display() можно написать так:
Screen& Screen::display()
{
typedef string::size_type idx_type;
for ( idx_type ix = 0; ix _height; ++ix )
{ // для каждой строки
idx_type offset = _width * ix; // смещение строки
for ( idx_type iy = 0; iy _width; ++iy )
// для каждой колонки вывести элемент
cout _screen[ offset + iy ];
cout endl;
}
return *this;
}
А вот реализация reSize():
// объявление reSize() находится в теле класса
// в нем задан аргумент по умолчанию bkground = '#'
Screen& Screen::reSize( int h, int w, char bkground )
{ // сделать высоту экрана равной h, а ширину - равной w
// запомнить содержимое экрана
string local(_screen);
// заменить строку _screen
_screen.assign( // записать в строку
h * w, // h * w символов
bkground // со значением bkground
);
typedef string::size_type idx_type;
idx_type local_pos = 0;
// скопировать содержимое старого экрана в новый
for ( idx_type ix = 0; ix _height; ++ix )
{ // для каждой строки
idx_type offset = w * ix; // смещение строки
for ( idx_type iy = 0; iy _width; ++iy )
// для каждой колонки присвоить новое значение
_screen[ offset + iy ] = local[ local_pos++ ];
}
_height = h;
_width = w;
// _cursor не меняется
return *this;
}
Работа указателя this не исчерпывается возвратом объекта, к которому была применена функция-член. При рассмотрении copy() в разделе 13.3 мы видели и другой способ его использования:
void Screen::copy( const Screen& sobj )
{
// если этот объект Screen и sobj - одно и то же,
// копирование излишне
if ( this != sobj )
{
// скопировать значение sobj в this
}
}
Указатель this хранит адрес объекта, для которого была вызвана функция-член. Если адрес, на который ссылается sobj, совпадает со значением this, то sobj и this относятся к одному и тому же объекту, так что операция копирования не нужна. (Мы еще встретимся с этой конструкцией, когда будем рассматривать копирующий оператор присваивания в разделе 14.7.)
Упражнение 13.7
Указатель this можно использовать для модификации адресуемого объекта, а также для его замены другим объектом того же типа. Например, функция-член assign() класса classType выглядит так. Можете ли вы объяснить, что она делает?
classType& classType::assign( const classType &source )
{
if ( this != &source )
{
this-~classType();
new (this) classType( source );
}
return *this;
}
Напомним, что ~classType – это имя деструктора. Оператор new выглядит несколько причудливо, но мы уже встречались с подобным в разделе 8.4.
Как вы относитесь к такому стилю программирования? Безопасна ли эта операция? Почему?
Иногда нужно, чтобы все объекты некоторого класса имели доступ к единственному глобальному объекту. Допустим, необходимо подсчитать, сколько их было создано; глобальным может быть указатель на процедуру обработки ошибок для класса или, скажем, указатель на свободную память для его объектов. В подобных случаях более эффективно иметь один глобальный объект, используемый всеми объектами класса, чем отдельные члены в каждом объекте. Хотя такой объект является глобальным, он существует лишь для поддержки реализации абстракции класса.
В этой ситуации приемлемым решением является статический член класса, который ведет себя как глобальный объект, принадлежащий своему классу. В отличие от других членов, которые присутствуют в каждом объекте как отдельные элементы данных, статический член существует в единственном экземпляре и связан с самим типом, а не с конкретным его объектом. Это разделяемая сущность, доступная всем объектам одного класса.
По сравнению с глобальным объектом у статического члена есть следующие преимущества:
* статический член не находится в глобальном пространстве имен программы, следовательно, уменьшается вероятность случайного конфликта имен с другими глобальными объектами;
* остается возможность сокрытия информации, так как статический член может быть закрытым, а глобальный объект – никогда.
Чтобы сделать член статическим, надо поместить в начале его объявления в теле класса ключевое слово static. К ним применимы все правила доступа к открытым, закрытым и защищенным членам. Например, для определенного ниже класса Account член _interestRate объявлен как закрытый и статический типа double:
class Account { // расчетный счет
Account( double amount, const string &owner );
string owner() { return _owner; }
private:
static double _interestRate; // процентная ставка
double _amount; // сумма на счету
string _owner; // владелец
};
Почему _interestRate сделан статическим, а _amount и _owner нет? Потому что у всех счетов разные владельцы и суммы, но процентная ставка одинакова. Следовательно, объявление члена _interestRate статическим уменьшает объем памяти, необходимый для хранения объекта Account.
Хотя текущее значение _interestRate для всех счетов одинаково, но со временем оно может изменяться. Поэтому мы решили не объявлять этот член как const. Достаточно модифицировать его лишь один раз, и с этого момента все объекты Account будут видеть новое значение. Если бы у каждого объекта была собственная копия, то пришлось бы обновить их все, что неэффективно и является потенциальным источником ошибок.
В общем случае статический член инициализируется вне определения класса. Его имя во внешнем определении должно быть специфицировано именем класса. Вот так можно инициализировать _interestRate:
// явная инициализация статического члена класса
#include "account.h"
double Account::_interestRate = 0.0589;
В программе может быть только одно определение статического члена. Это означает, что инициализацию таких членов следует помещать не в заголовочные файлы, а туда, где находятся определения невстроенных функций-членов класса.
В объявлении статического члена можно указать любой тип. Это могут быть константные объекты, массивы, объекты классов и т.д. Например:
#include string
class Account {
// ...
private:
static const string name;
};
const string Account::name( "Savings Account" );
Константный статический член целого типа инициализируется константой внутри тела класса: это особый случай. Если бы для хранения названия счета мы решили использовать массив символов вместо строки, то его размер можно было бы задать с помощью константного члена типа int:
// заголовочный файл
class Account {
//...
private:
static const int nameSize = 16;
static const string name[nameSize];
};
// исходный файл
const string Account::nameSize; // необходимо определение члена
const string Account::name[nameSize] = "Savings Account";
Отметим, что константный статический член целого типа, инициализированный константой, – это константное выражение. Проектировщик может объявить такой статический член, если внутри тела класса возникает необходимость в именованной константе. Например, поскольку константный статический член nameSize является константным выражением, проектировщик использует его для задания размера члена-массива с именем name.
Даже если такой член инициализируется в теле класса, его все равно необходимо задать вне определения класса. Однако поскольку начальное значение уже задано в объявлении, то при определении оно не указывается.
Так как name – это массив (и не целого типа), его нельзя инициализировать в теле класса. Попытка поступить таким образом приведет к ошибке компиляции:
class Account {
//...
private:
static const int nameSize = 16; // правильно: целый тип
static const string name[nameSize] = "Savings Account"; // ошибка
};
Член name должен быть инициализирован вне определения класса.
Обратите внимание, что член nameSize задает размер массива name в определении, находящемся вне тела класса:
const string Account::name[nameSize] = "Savings Account";
nameSize не квалифицирован именем класса Account. И хотя это закрытый член, определение name не приводит к ошибке. Как такое может быть? Определение статического члена аналогично определению функции-члена класса, которое может ссылаться на закрытые члены. Определение статического члена name находится в области видимости класса и может ссылаться на закрытые члены, после того как распознано квалифицированное имя Account::name. (Подробнее об области видимости класса мы поговорим в разделе 13.9.)
Статический член класса доступен функции-члену того же класса и без использования соответствующих операторов:
inline double Account::dailyReturn()
{
return( _interestRate / 365 * _amount );
}
Что же касается функций, не являющихся членами класса, то они могут обращаться к статическому члену двумя способами. Во-первых, посредством операторов доступа:
class Account {
// ...
private:
friend int compareRevenue( Account&, Account* );
// остальное без изменения
};
// мы используем ссылочный и указательный параметры,
// чтобы проиллюстрировать оба оператора доступа
int compareRevenue( Account &ac1, Account *ac2 );
{
double ret1, ret2;
ret1 = ac1._interestRate * ac1._amount;
ret2 = ac2-_interestRate * ac2-_amount;
// ...
}
Как ac1._interestRate, так и ac2-_interestRate относятся к статическому члену Account::_interestRate.
Поскольку есть лишь одна копия статического члена класса, до нее необязательно добираться через объект или указатель. Другой способ заключается в том, чтобы обратиться к статическому члену напрямую, квалифицировав его имя именем класса:
// доступ к статическому члену с указанием квалифицированного имени
if ( Account::_interestRate 0.05 )
Если обращение к статическому члену производится без помощи оператора доступа, то его имя следует квалифицировать именем класса, за которым следует оператор разрешения области видимости:
Account::
Это необходимо, поскольку такой член не является глобальным объектом, а значит, в глобальной области видимости отсутствует. Следующее определение дружественной функции compareRevenue эквивалентно приведенному выше:
int compareRevenue( Account &ac1, Account *ac2 );
{
double ret1, ret2;
ret1 = Account::_interestRate * ac1._amount;
ret2 = Account::_interestRate * ac2-_amount;
// ...
}
Уникальная особенность статического члена – то, что он существует независимо от объектов класса, – позволяет использовать его такими способами, которые для нестатических членов недопустимы.
* статический член может принадлежать к типу того же класса, членом которого он является. Нестатические объявляются лишь как указатели или ссылки на объект своего класса:
class Bar {
public:
// ...
private:
static Bar mem1; // правильно
Bar *mem2; // правильно
Bar mem3; // ошибка
};
* статический член может выступать в роли аргумента по умолчанию для функции-члена класса, а для нестатического это запрещено:
extern int var;
class Foo {
private:
int var;
static int stcvar;
public:
// ошибка: трактуется как Foo::var,
// но ассоциированного объекта класса не существует
int mem1( int = var );
// правильно: трактуется как static Foo::stcvar,
// ассоциированный объект и не нужен
int mem2( int = stcvar );
// правильно: трактуется как глобальная переменная var
int mem3( int = :: var );
};
Функции-члены raiseInterest() и interest() обращаются к глобальному статическому члену _interestRate:
class Account {
public:
void raiseInterest( double incr );
double interest() { return _interestRate; }
private:
static double _interestRate;
};
inline void Account::raiseInterest( double incr )
{
_interestRate += incr;
}
Проблема в том, что любая функция-член должна вызываться с помощью оператора доступа к конкретному объекту класса. Поскольку приведенные выше функции обращаются только к статическому _interestRate, то совершенно безразлично, для какого объекта они вызываются. Нестатические члены при вызове этих функций не читаются и не модифицируются.
Поэтому лучше объявить такие функции-члены как статические. Это можно сделать следующим образом:
class Account {
public:
static void raiseInterest( double incr );
static double interest() { return _interestRate; }
private:
static double _interestRate;
};
inline void Account::raiseInterest( double incr )
{
_interestRate += incr;
}
Объявление статической функции-члена почти такое же, как и нестатической: в теле класса ему предшествует ключевое слово static, а спецификаторы const или volatile запрещены. В ее определении, находящемся вне тела класса, слова static быть не должно.
Такой функции-члену указатель this не передается, поэтому явное или неявное обращение к нему внутри ее тела вызывает ошибку компиляции. В частности, попытка обращения к нестатическому члену класса неявно требует наличия указателя this и, следовательно, запрещена. Например, представленную ранее функцию-член dailyReturn() нельзя объявить статической, поскольку она обращается к нестатическому члену _amount.
Статическую функцию-член можно вызвать для объекта класса, пользуясь одним из операторов доступа. Ее также можно вызвать непосредственно, квалифицировав ее имя, даже если никаких объектов класса не объявлено. Вот небольшая программа, иллюстрирующая их применение:
#include iostream
#include "account.h"
bool limitTest( double limit )
{
// пока еще ни одного объекта класса Account не объявлено
// правильно: вызов статической функции-члена
return limit = Account::interest() ;
}
int main() {
double limit = 0.05;
if ( limitTest( limit ) )
{
// указатель на статическую функцию-член
// объявлен как обычный указатель
void (*psf)(double) = &Account::raiseInterest;
psf( 0.0025 );
}
Account ac1( 5000, "Asterix" );
Account ac2( 10000, "Obelix" );
if ( compareRevenue( ac1, &ac2 ) & 0 )
cout ac1.owner()
"is richer than "
ac2.owner() "\n";
else
cout ac1.owner()
" is poorer than"
ac2.owner() "\n";
return 0;
}
Упражнение 13.8
Пусть дан класс Y с двумя статическими данными-членами и двумя статическими функциями-членами:
class X {
public:
X( int i ) { _val = i; }
int val() { return _val; }
private:
int _val;
};
class Y {
public:
Y( int i );
static X xval();
static int callsXval();
private:
static X _xval;
static int _callsXval;
};
Инициализируйте _xval значением 20, а _callsXval значением 0.
Упражнение 13.9
Используя классы из упражнения 13.8, реализуйте обе статические функции-члена для класса Y. callsXval() должна подсчитывать, сколько раз вызывалась xval().
Упражнение 13.10
Какие из следующих объявлений и определений статических членов ошибочны? Почему?
// example.h
class Example {
public:
static double rate = 6.5;
static const int vecSize = 20;
static vectordouble vec(vecSize);
};
// example.c
#include "example.h "
double Example::rate;
vectordouble Example::vec;
Предположим, что в нашем классе Screen определены четыре новых функции-члена: forward(), back(), up() и down(), которые перемещают курсор соответственно вправо, влево, вверх и вниз. Сначала мы должны объявить их в теле класса:
class Screen {
public:
inline Screen& forward();
inline Screen& back();
inline Screen& end();
inline Screen& up();
inline Screen& down();
// другие функции-члены не изменяются
private:
inline int row();
// другие функции-члены не изменяются
};
Функции-члены forward() и back() перемещают курсор на один символ. По достижении правого нижнего или левого верхнего угла экрана курсор переходит в противоположный угол.
inline Screen& Screen::forward()
{ // переместить _cursor вперед на одну экранную позицию
++_cursor;
// если достигли конца экрана, перепрыгнуть в противоположный угол
if ( _cursor == _screen.size() )
home();
return *this;
}
inline Screen& Screen::back()
{ // переместить _cursor назад на одну экранную позицию
// если достигли начала экрана, перепрыгнуть в противоположный угол
if ( _cursor == 0 )
end();
else
--_cursor;
return *this;
}
end() перемещает курсор в правый нижний угол экрана и является парной по отношению к функции-члену home():
inline Screen& Screen::end()
{
_cursor = _width * _height - 1;
return *this;
}
Функции up() и down() перемещают курсор вверх и вниз на одну строку. По достижении верхней или нижней строки курсор остается на месте и подается звуковой сигнал:
const char BELL = '\007';
inline Screen& Screen::up()
{ // переместить _cursor на одну строку вверх
// если уже наверху, остаться на месте и подать сигнал
if ( row() == 1 ) // наверху?
cout BELL endl;
else
_cursor -= _width;
return *this;
}
inline Screen& Screen::down()
{
if ( row() == _height ) //внизу?
cout BELL endl;
else
_cursor += _width;
return *this;
}
row() – это закрытая функция-член, которая используется в функциях up() и down(), возвращая номер строки, где находится курсор:
inline int Screen::row()
{ // вернуть текущую строку
return ( _cursor + _width ) / height;
}
Пользователи класса Screen попросили нас добавить функцию repeat(), которая повторяет указанное действие n раз. Ее реализация могла бы выглядеть так:
Screen &repeat( char op, int times )
{
switch( op ) {
case DOWN: // n раз вызвать Screen::down()
break;
case DOWN: // n раз вызвать Screen::up()
break;
// ...
}
}
Такая реализация имеет ряд недостатков. В частности, предполагается, что функции-члены класса Screen останутся неизменными, поэтому при добавлении или удалении функции-члена repeat() необходимо модифицировать. Вторая проблема – размер функции. Поскольку приходится проверять все возможные функции-члены, то исходный текст становится громоздким и неоправданно сложным.
В более общей реализации параметр op заменяется параметром типа указателя на функцию-член класса Screen. Теперь repeat() не должна сама устанавливать, какую операцию следует выполнить, и всю инструкцию switch можно удалить. Определение и использование указателей на члены класса – тема последующих подразделов.
Указателю на функцию нельзя присвоить адрес функции-члена, даже если типы возвращаемых значений и списки параметров полностью совпадают. Например, переменная pfi – это указатель на функцию без параметров, которая возвращает значение типа int:
int (*pfi)();
Если имеются глобальные функции HeightIs() и WidthIs() вида:
int HeightIs();
int WidthIs();
то допустимо присваивание pfi адреса любой из этих переменных:
pfi = HeightIs;
pfi = WidthIs;
В классе Screen также определены две функции доступа, height() и width(), не имеющие параметров и возвращающие значение типа int:
inline int Screen::height() { return _height; }
inline int Screen::width() { return _width; }
Однако попытка присвоить их переменной pfi является нарушением типизации и влечет ошибку компиляции:
// неверное присваивание: нарушение типизации
pfi = &Screen::height;
В чем нарушение? У функций-членов есть дополнительный атрибут типа, отсутствующий у функций, не являющихся членами, – класс. Указатель на функцию-член должен соответствовать типу присваиваемой ему функции не в двух, а в трех отношениях: по типу и количеству формальных параметров; типу возвращаемого значения; типу класса, членом которого является функция.
Несоответствие типов между двумя указателями – на функцию-член и на обычную функцию – обусловлено их разницей в представлении. В указателе на обычную функцию хранится ее адрес, который можно использовать для непосредственного вызова. (Указатели на функции рассматривались в разделе 7.9.) Указатель же на функцию-член должен быть сначала привязан к объекту или указателю на объект, чтобы получить this, и только после этого он применяется для вызова функции-члена. (В следующем подразделе мы покажем, как осуществить такую привязку.) Хотя для указателя на обычную функцию и для указателя на функцию-член используется один и тот же термин, их природа различна.
Синтаксис объявления указателя на функцию-член должен принимать во внимание тип класса. То же верно и в отношении указателей на данные-члены. Рассмотрим член _height класса Screen. Его полный тип таков: член класса Screen типа short. Следовательно, полный тип указателя на _height – это указатель на член класса Screen типа short:
short Screen::*
Определение указателя на член класса Screen типа short выглядит следующим образом:
short Screen::*ps_Screen;
Переменную ps_Screen можно инициализировать адресом _height:
short Screen::*ps_Screen = &Screen::_height;
или присвоить ей адрес _width:
short Screen::*ps_Screen = &Screen::_width;
Переменной ps_Screen разрешается присваивать указатель на _width или _height, так как они являются членами класса Screen типа short.
Несоответствие типов указателя на данные-члены и обычного указателя также связано с различием в их представлении. Обычный указатель содержит всю информацию, необходимую для обращения к объекту. Указатель на данные-члены следует сначала привязать к объекту или указателю на него, а лишь затем использовать для доступа к члену этого объекта. (В книге “Inside the C++ Object Model” ([LIPPMAN96a]) также описывается представление указателей на члены.)
Указатель на функцию-член определяется путем задания типа возвращаемого функцией значения, списка ее параметров и класса. Например, следующий указатель, с помощью которого можно вызвать функции height() и width(), имеет тип указателя на функцию-член класса Screen без параметров, которая возвращает значение типа int:
int (Screen::*)()
Указатели на функции-члены можно объявлять, инициализировать и присваивать:
// всем указателям на функции-члены класса можно присвоить значение 0
int (Screen::*pmf1)() = 0;
int (Screen::*pmf2)() = &Screen::height;
pmf1 = pmf2;
pmf2 = &Screen::width;
Использование typedef может облегчить чтение объявлений указателей на члены. Например, для типа “указатель на функцию-член класса Screen без параметров, которая возвращает ссылку на объект Screen”, т.е.
Screen& (Screen::*)()
Следующий typedef определяет Action как альтернативное имя:
typedef Screen& (Screen::*Action)();
Action default = &Screen::home;
Action next = &Screen::forward;
Тип "указатель на функцию-член" можно использовать для объявления формальных параметров и типа возвращаемого значения функции. Для параметра того же типа можно также указать значение аргумента по умолчанию:
Screen& action( Screen&, Action)();
action() объявлена как принимающая два параметра: ссылку на объект класса Screen и указатель на функцию-член Screen без параметров, которая возвращает ссылку на его объект. Вызвать action() можно любым из следующих способов:
Screen meScreen;
typedef Screen& (Screen::*Action)();
Action default = &Screen::home;
extern Screen& action( Screen&, Sction = &Screen::display );
void ff()
{
action( myScreen );
action( myScreen, default );
action( myScreen, &Screen::end );
}
В следующем подразделе обсуждается вызов функции-члена посредством указателя.
К указателям на члены класса можно обращаться только с помощью конкретного объекта или указателя на объект типа класса. Для этого применяется любой из двух операторов доступа (.* для объектов класса и ссылок на них или -* для указателей). Например, так вызывается функция-член через указатель на нее:
int (Screen::*pmfi)() = &Screen::height;
Screen& (Screen::*pmfS)( const Screen& ) = &Screen::copy;
Screen myScreen, *bufScreen;
// прямой вызов функции-члена
if ( myScreen.height() == bufScreen-height() )
bufScreen-copy( myScreen );
// эквивалентный вызов по указателю
if ( (myScreen.*pmfi)() == (bufScreen-*pmfi)() )
(bufScreen-*pmfS)( myScreen );
Вызовы
(myScreen.*pmfi)()
(bufScreen-*pmfi)();
требуют скобок, поскольку приоритет оператора вызова () выше, чем приоритет взятия указателя на функцию-член. Без скобок
myScreen.*pmfi()
интерпретируется как
myScreen.*(pmfi())
Это означает вызов функции pmfi() и привязку возвращенного ей значения к оператору (.*). Разумеется, тип pmfi не поддерживает такого использования, так что компилятор выдаст сообщение об ошибке.
Указатели на данные-члены используются аналогично:
typedef short Screen::*ps_Screen;
Screen myScreen, *tmpScreen = new Screen( 10, 10 );
ps_Screen pH = &Screen::_height;
ps_Screen pW = &Screen::_width;
tmpScreen-*pH = myScreen.*pH;
tmpScreen-*pW = myScreen.*pW;
Приведем реализацию функции-члена repeat(), которую мы обсуждали в начале этого раздела. Теперь она будет принимать указатель на функцию-член:
typedef Screen& (Screen::Action)();
Screen& Screen::repeat( Action op, int times )
{
for ( int i = 0; i
Параметр op – это указатель на функцию-член, которая должна вызываться times раз.
Если бы нужно было задать значения аргументов по умолчанию, то объявление repeat() выглядело бы следующим образом:
class Screen {
public:
Screen &repeat( Action = &Screen::forward, int = 1 );
// ...
};
А ее вызовы так:
Screen myScreen;
myScreen.repeat(); // repeat( &Screen::forward, 1 );
myScreen.repeat( &Screen::down, 20 );
Определим таблицу указателей. В следующем примере Menu – это таблица указателей на функции-члены класса Screen, которые реализуют перемещение курсора. CursorMovements – перечисление, элементами которого являются номера в таблице Menu.
Action::Menu() = {
&Screen::home,
&Screen::forward,
&Screen::back,
&Screen::up,
&Screen::down,
&Screen::end
};
enum CursorMovements {
HOME, FORWARD, BACK, UP, DOWN, END
};
Можно определить перегруженную функцию-член move(), которая принимает параметр CursorMovements и использует таблицу Menu для вызова указанной функции-члена. Вот ее реализация:
Screen& Screen::move( CursorMovements cm )
{
( this-*Menu[ cm ] )();
return *this;
}
У оператора взятия индекса ([]) приоритет выше, чем у оператора указателя на функцию-член (-*). Первая инструкция в move() сначала по индексу выбирает из таблицы Menu нужную функцию-член, которая и вызывается с помощью указателя this и оператора указателя на функцию-член. move() можно применять в интерактивной программе, где пользователь выбирает вид перемещения курсора из отображаемого на экране меню.
Между указателями на статические и нестатические члены класса есть разница. Синтаксис указателя на член класса не используется для обращения к статическому члену. Статические члены – это глобальные объекты и функции, принадлежащие классу. Указатели на них – это обычные указатели. (Напомним, что статической функции-члену не передается указатель this.)
Объявление указателя на статический член класса выглядит так же, как и для указателя на объект, не являющийся членом класса. Для разыменования указателя никакой объект не требуется. Рассмотрим класс Account:
class Account {
public:
static void raiseInterest( double incr );
static double interest() { return _interestRate ; }
double amount() { return _amount; }
private:
static double _interestRate;
double _amount;
string _owner;
};
inline void Account::raiseInterest( double incr )
{
_interestRate += incr;
}
Тип &_interestRate – это double*:
// это неправильный тип для &_interestRate
double Account::*
Определение указателя на &_interestRate имеет вид:
// правильно: double*, а не double Account::*
double *pd = &Account::_interestRate;
Этот указатель разыменовывается так же, как и обычный, объект класса для этого не требуется:
Account unit;
// используется обычный оператор разыменования
double daily = *pd / 365 * unit._amount;
Однако, поскольку _interestRate и _amount – закрытые члены, необходимо иметь статическую функцию-член interest() и нестатическую amount().
Указатель на interest() – это обычный указатель на функцию:
// правильно
double (*)()
а не на функцию-член класса Account:
// неправильно
double (Account::*)()
Определение указателя и косвенный вызов interest() реализуются так же, как и для обычных указателей:
// правильно: double(*pf)(), а не double(Account::*pf)()
double(*pf)() = &Account::interest;
double daily = pf() / 365 * unit.amount();
Упражнение 13.11
К какому типу принадлежат члены _screen и _cursor класса Screen?
Упражнение 13.12
Определите указатель на член и инициализируйте его значением Screen::_screen; присвойте ему значение Screen::_cursor.
Упражнение 13.13
Определите typedef для каждой из функций-членов класса Screen.
Упражнение 13.14
Указатели на члены можно также объявлять как данные-члены класса. Модифицируйте определение класса Screen так, чтобы оно содержало указатель на его функцию-член того же типа, что home() и end().
Упражнение 13.15
Модифицируйте имеющийся конструктор класса Screen (или напишите новый) так, чтобы он принимал параметр типа указателя на функцию-член класса Screen, для которой список формальных параметров и тип возвращаемого значения такие же, как у home() и end(). Реализуйте для этого параметра значение по умолчанию и используйте параметр для инициализации члена класса, описанного в упражнении 13.14. Напишите функцию-член Screen, позволяющую пользователю задать ее значение.
Упражнение 13.16
Определите перегруженный вариант repeat(), который принимает параметр типа cursorMovements.
Объединение – это специальный вид класса. Данные-члены хранятся в нем таким образом, что перекрывают друг друга. Все члены размещаются, начиная с одного и того же адреса. Для объединения отводится столько памяти, сколько необходимо для хранения самого большого его члена. В любой момент времени можно присвоить значение лишь одному такому члену.
Рассмотрим пример, иллюстрирующий использование объединения. Лексический анализатор, входящий в состав компилятора, разбивает программу на последовательность лексем. Так, инструкция
int i = 0;
преобразуется в последовательность из пяти лексем:
* Ключевое слово int.
* Идентификатор i.
* Оператор =
* Константа 0 типа int.
* Точка с запятой.
Лексический анализатор передает эти лексемы синтаксическому анализатору, парсеру, который идентифицирует полученную последовательность. Полученная информация должна дать парсеру возможность распознать эту последовательность лексем как объявление. Для этого с каждой лексемой ассоциируется информация, позволяющая парсеру увидеть следующее:
Type ID Assign Constant Semicolon
(Тип ИД Присваивание Константа Точка с запятой)
Далее парсер анализирует значения каждой лексемы. В данном случае он видит:
Type == int
ID == i
Constant == 0
Для Assign и Semicolon дополнительной информации не нужно, так как у них может быть только одно значение: соответственно := и ;
Таким образом, в представлении лексемы могло бы быть два члена – token и value. token – это уникальный код, показывающий, что лексема имеет тип Type, ID, Assign, Constant или Semicolon, например 85 для ID и 72 для Semicolon.value содержит конкретное значение лексемы. Так, для лексемы ID в предыдущем объявлении value будет содержать строку "i", а для лексемы Type – некоторое представление типа int.
Представление члена value несколько проблематично. Хотя для любой отдельной лексемы в нем хранится всего одно значение, их типы для разных лексем могут различаться. Для лексемы ID в value хранится строка символов, а для Constant – целое число.
Конечно, для хранения данных нескольких типов можно использовать класс. Разработчик компилятора может объявить, что value принадлежит к типу класса, в котором для каждого типа данных есть отдельный член.
Применение класса решает проблему представления value. Однако для любой данной лексемы value имеет лишь один из множества возможных типов и, следовательно, будет задействован только один член класса, хотя памяти выделяется столько, сколько нужно для хранения всех членов. Чтобы память резервировалась только для нужного в данный момент члена, применяется объединение. Вот как оно определяется:
union TokenValue {
char _cval;
int _ival;
char *_sval;
double _dval;
};
Если самым большим типом среди всех членов TokenValue является dval, то размер TokenValue будет равен размеру объекта типа double. По умолчанию члены объединения открыты. Имя объединения можно использовать в программе всюду, где допустимо имя класса:
// объект типа TokenValue
TokenValue last_token;
// указатель на объект типа TokenValue
TokenValue *pt = new TokenValue;
Обращение к членам объединения, как и к членам класса, производится с помощью операторов доступа:
last_token._ival = 97;
char ch = pt-_cval;
Члены объединения можно объявлять открытыми, закрытыми или защищенными:
union TokenValue {
public:
char _cval;
// ...
private:
int priv;
}
int main() {
TokenValue tp;
tp._cval = '\n'; // правильно
// ошибка: main() не может обращаться к закрытому члену
// TokenValue::priv
tp.priv = 1024;
}
У объединения не бывает статических членов или членов, являющихся ссылками. Его членом не может быть класс, имеющий конструктор, деструктор или копирующий оператор присваивания. Например:
union illegal_members {
Screen s; // ошибка: есть конструктор
Screen *ps; // правильно
static int is; // ошибка: статический член
int // ошибка: член-ссылка
};
Для объединения разрешается определять функции-члены, включая конструкторы и деструкторы:
union TokenValue {
public:
TokenValue(int ix) : _ival(ix) { }
TokenValue(char ch) : _cval(ch) { }
// ...
int ival() { return _ival; }
char cval() { return _cval; }
private:
int _ival;
char _cval;
// ...
};
int main() {
TokenValue tp(10);
int ix = tp.ival();
//...
}
Вот пример работы объединения TokenValue:
enum TokenKind ( ID, Constant /* и другие типы лексем */ }
class Token {
public:
TokenKind tok;
TokenValue val;
};
Объект типа Token можно использовать так:
int lex() {
Token curToken;
char *curString;
int curIval;
// ...
case ID: // идентификатор
curToken.tok = ID;
curToken.val._sval = curString;
break;
case Constant: // целая константа
curToken.tok = Constant;
curToken.val._ival = curIval;
break;
// ... и т.д.
}
Опасность, связанная с применением объединения, заключается в том, что можно случайно извлечь хранящееся в нем значение, пользуясь не тем членом. Например, если в последний раз значение присваивалось _ival, то вряд ли понадобится значение, оказавшееся в _sval. Это, по всей вероятности, приведет к ошибке в программе.
Чтобы защититься от подобного рода ошибок, следует создать дополнительный объект, дискриминант объединения, определяющий тип значения, которое в данный момент хранится в объединении. В классе Token роль такого объекта играет член tok:
char *idVal;
// проверить значение дискриминанта перед тем, как обращаться к sval
if ( curToken.tok == ID )
idVal = curToken.val._sval;
При работе с объединением, являющимся членом класса, полезно иметь набор функций для каждого хранящегося в объединении типа данных:
#include
// функции доступа к члену объединения sval
string Token::sval() {
assert( tok==ID );
return val._sval;
}
Имя в определении объединения задавать необязательно. Если оно не используется в программе как имя типа для объявления других объектов, его можно опустить. Например, следующее определение объединения Token эквивалентно приведенному выше, но без указания имени:
class Token {
public:
TokenKind tok;
// имя типа объединения опущено
union {
char _cval;
int _ival;
char *_sval;
double _dval;
} val;
};
Существует анонимное объединение – объединение без имени, за которым не следует определение объекта. Вот, например, определение класса Token, содержащее анонимное объединение:
class Token {
public:
TokenKind tok;
// анонимное объединение
union {
char _cval;
int _ival;
char *_sval;
double _dval;
};
};
К данным-членам анонимного объединения можно напрямую обращаться в той области видимости, в которой оно определено. Перепишем функцию lex(), используя предыдущее определение:
int lex() {
Token curToken;
char *curString;
int curIval;
// ... выяснить, что находится в лексеме
// ... затем установить curToken
case ID:
curToken.tok = ID;
curToken._sval = curString;
break;
case Constant: // целая константа
curToken.tok = Constant;
curToken._ival = curIval;
break;
// ... и т.д.
}
Анонимное объединение позволяет убрать один уровень доступа, поскольку обращение к его членам идет как к членам класса Token. У него не может быть закрытых или защищенных членов, а также функций-членов. Такое объединение, определенное в глобальной области видимости, должно быть объявлено в безымянном пространстве имен или иметь модификатор static.
Для хранения заданного числа битов можно объявить член класса специального вида, называемый битовым полем. Он должен иметь целый тип данных, со знаком или без знака:
class File {
// ...
unsigned int modified : 1; // битовое поле
};
После идентификатора битового поля следует двоеточие, а за ним – константное выражение, задающее число битов. К примеру, modified – это поле из одного бита.
Битовые поля, определенные в теле класса подряд, по возможности упаковываются в соседние биты одного целого числа, делая хранение объекта более компактным. Так, в следующем объявлении пять битовых полей будут содержаться в одном числе типа unsigned int, ассоциированном с первым полем mode:
typedef unsigned int Bit;
class File {
public:
Bit mode: 2;
Bit modified: 1;
Bit prot_owner: 3;
Bit prot_group: 3;
Bit prot_world: 3;
// ...
};
Доступ к битовому полю осуществляется так же, как к прочим членам класса. Скажем, к битовому полю, являющемуся закрытым членом класса, можно обратиться лишь из функций-членов и друзей этого класса:
void File::write()
{
modified = 1;
// ...
}
void File::close()
{
if ( modified )
// ... сохранить содержимое
}
Вот простой пример использования битового поля длиной больше 1 (примененные здесь побитовые операции рассматривались в разделе 4.11):
enum { READ = 01, WRITE = 02 }; // режимы открытия файла
int main() {
File myFile;
myFile.mode |= READ;
if ( myFile.mode & READ )
cout "myFile.mode is set to READ\n";
}
Обычно для проверки значения битового поля-члена определяются встроенные функции-члены. Допустим, в классе File можно ввести члены isRead() и isWrite():
inline int File::isRead() { return mode & READ; }
inline int File::isWrite() { return mode & WRITE; }
if ( myFile.isRead() ) /* ... */
С помощью таких функций-членов битовые поля можно сделать закрытыми членами класса File.
К битовому полю нельзя применять оператор взятия адреса (&), поэтому не может быть и указателя на подобные поля-члены. Кроме того, полю запрещено быть статическим членом.
В стандартной библиотеке C++ имеется шаблон класса bitset, который облегчает манипуляции с битовыми множествами. Мы рекомендуем использовать его вместо битовых полей. (Шаблон класса bitset и определенные в нем операции рассматривались в разделе 4.12.)
Упражнение 13.17
Перепишите примеры из этого подраздела так, чтобы в классе File вместо объявления и прямого манипулирования битовыми полями использовался класс bitset и его операторы.
Тело класса определяет область видимости. Объявления членов класса внутри тела вводят их имена в область видимости класса.
Для обращения к ним применяются операторы доступа (точка и стрелка) и оператор разрешения области видимости (::). Когда употребляется оператор доступа, то предшествующее ему имя обозначает объект или указатель на объект типа класса, а следующее за ним имя должно находиться в области видимости этого класса. Аналогично при использовании оператора разрешения области видимости поиск имени, следующего за ним, идет в области видимости класса, имя которого стоит перед оператором. (В главах 17 и 18 мы увидим, что производный класс может обращаться к членам своих базовых.)
Однако применение операторов доступа или оператора разрешения области видимости нужно не всегда. Некоторые части программы сами по себе находятся в области видимости класса, и в них к членам класса можно обращаться напрямую. Одной из таких частей является само определение класса. Имя его члена можно использовать в теле после объявления:
class String {
public:
typedef int index_type;
// тип параметра - это на самом деле String::index_type
char& operator[]( index_type )
};
Порядок объявления членов класса в его теле важен: нельзя ссылаться на члены, которые будут объявлены позже. Например, если объявление оператора operator[]() находится раньше объявления typedef index_type, то приведенное ниже объявление operator[]() оказывается ошибочным, поскольку в нем используется еще неизвестное имя index_type:
class String {
public:
// ошибка: имя index_type не объявлено
char &operator[]( index_type );
typedef int index_type;
};
Однако из этого правила есть два исключения. Первое касается имен, использованных в определениях встроенных функций-членов, второе – имен, применяемых как аргументы по умолчанию. Рассмотрим обе ситуации.
Разрешение имен в определениях встроенных функций-членов происходит в два этапа. Сначала объявление функции (т.е. тип возвращаемого значения и список параметров) обрабатывается в том месте, где оно встретилось в определении класса. Затем тело функции обрабатывается во всей области видимости, сразу после того, как были просмотрены объявления всех членов. Посмотрим на наш пример, в котором оператор operator[]() определен как встроенный внутри тела класса:
class String {
public:
typedef int index_type;
char &operator[]( index_type elem )
{ return _string[ elem ]; }
private:
char *_string;
};
На первом этапе просматриваются имена, использованные в объявлении operator[](), чтобы найти имя типа параметра index_type. Поскольку первый шаг выполняется тогда, когда в теле класса встретилось определение функции-члена, то имя index_type должно быть объявлено до определения operator[]().
Обратите внимание, что член _string объявлен в теле класса после определения operator[](). Это правильно, и _string не является в теле operator[]() необъявленным именем. Имена в телах функций-членов просматриваются на втором шаге разрешения имен в определениях встроенных функций-членов. Этот этап выполняется во всей области видимости класса, как если бы тела функций-членов обрабатывались последними, прямо перед закрытием тела класса, когда все его члены уже объявлены.
Аргументы по умолчанию также разрешаются на втором шаге. Например, в объявлении функции-члена clear() используется имя статического члена bkground, который определен позже:
class Screen {
public:
// bkground относится к статическому члену,
// объявленному позже в определении класса
Screen& clear( char = bkground );
private:
static const char bkground = '#';
};
Хотя такие аргументы в объявлениях функций-членов разрешаются во всей области видимости класса, программа будет считаться ошибочной, если он ссылается на нестатический член. Нестатический член должен быть привязан к объекту своего класса или к указателю на такой объект, иначе использовать его нельзя. Употребление подобных членов в качестве аргументов по умолчанию нарушает это ограничение. Если переписать предыдущий пример так:
class Screen {
public:
// ...
// ошибка: bkground - нестатический член
Screen& clear( char = bkground );
private:
const char bkground = '#';
};
то имя аргумента по умолчанию разрешается нестатическим членом bkground, а это считается ошибкой.
Определения членов класса, появляющиеся вне его тела, – это еще один пример части программы, которая находится в области видимости класса. В ней имена членов распознаются несмотря на то, что оператор доступа или оператор разрешения области видимости при обращении к ним не применяется. Как же разрешаются имена в определениях членов?
Как правило, если такое определение появляется вне тела, то часть программы, следующая за именем определяемого члена, считается находящейся в области видимости класса вплоть до конца определения члена. Вынесем определение оператора operator[]() из класса String:
class String {
public:
typedef int index_type;
char& operator[]( index_type );
private:
char *_string;
};
// в operator[]() есть обращения к index_type и _string
inline char& operator[]( index_type elem )
{
return _string[ elem ];
}
Обратите внимание, что в списке параметров встречается typedef index_type без квалифицирующего имени класса String::.Текст, следующий за именем члена String::operator[] и до конца определения функции, находится в области видимости класса. Объявленные в этой области типы рассматриваются при разрешении имен типов, использованных в списке параметров функции-члена.
Определения статических данных-членов также появляются вне определения класса. В них часть программы, следующая за именем статического члена вплоть до конца определения, считается находящейся в области видимости класса. Например, инициализатор статического члена может непосредственно, без соответствующих операторов, ссылаться на члены класса:
class Account:
// ...
private:
static double _interestRate;
static double initInterestRate();
};
// ссылается на Account::initInterest()
double Account::_interestRate = initInterest();
Инициализатор _interestRate вызывает статическую функцию-член Account::initInterest() несмотря на то, что ее имя не квалифицировано именем класса.
Не только инициализатор, но и все, что следует за именем статического члена _interestRate до завершающей точки с запятой, находится в области видимости класса Account. Поэтому в определении статического члена name может быть обращение к члену класса nameSize:
class Account:
// ...
private:
static const int nameSize = 16;
static const char name[nameSize];
// nameSize не квалифицировано именем класса Account
const char Account::name[nameSize] = "Savins Account";
Хотя член nameSize не квалифицирован именем класса Account, определение name не является ошибкой, так как оно находится в области видимости своего класса и может ссылаться на его члены после того, как компилятор прочитал Account::name.
В определении члена, которое появляется вне тела, часть программы перед определяемым именем не находится в области видимости класса. При обращении к члену в этой части следует пользоваться оператором разрешения области видимости. Например, если типом статического члена является typedef Money, определенный в классе Account, то имя Money должно быть квалифицировано, когда статический член данных определяется вне тела класса:
class Account {
typedef double Money;
//...
private:
static Money _interestRate;
static Money initInterest();
};
// Money должно быть квалифицировано именем класса Account::
Account::Money Account::_interestRate = initInterest();
С каждым классом ассоциируется отдельная область видимости, причем у разных классов эти области различны. К членам одного класса нельзя напрямую обращаться в определениях членов другого класса, если только один из них не является для второго базовым. (Наследование и базовые классы рассматриваются в главах 17 и 18.)
Конечно, имена, используемые в области видимости класса, не обязаны быть именами членов класса. В процессе разрешения в этой области ведется поиск имен, объявленных и в других областях. Если имя, употребленное в области видимости класса, не разрешается именем члена класса, то компилятор ищет его в областях, включающих определение класса или члена. В этом подразделе мы покажем, как разрешаются имена, встречающиеся в области видимости класса.
Имя, использованное внутри определения класса (за исключением определений встроенных функций-членов и аргументов по умолчанию), разрешается следующим образом:
* Просматриваются объявления членов класса, появляющиеся перед употреблением имени.
* Если на шаге 1 разрешение не привело к успеху, то просматриваются объявления в пространстве имен перед определением класса. Напомним, что глобальная область видимости – это тоже область видимости пространства имен. (О пространствах имен речь шла в разделе 8.5.)
Например:
typedef double Money;
class Account {
// ...
private:
static Money _interestRate;
static Money initInterest();
// ...
};
Сначала компилятор ищет объявление Money в области видимости класса Account. При этом учитываются только те объявления, которые встречаются перед использованием Money. Поскольку таких объявлений нет, далее поиск ведется в глобальной области видимости. Объявление глобального typedef Money найдено, именно этот тип и используется в объявлениях _interestRate и initInterest().
Имя, встретившееся в определении функции-члена класса, разрешается следующим образом:
* Сначала просматриваются объявления в локальных областях видимости функции-члена. (О локальных областях видимости и локальных объявлениях говорилось в разделе 8.1.)
* Если шаг 1 не привел к успеху, то просматриваются объявления для всех членов класса.
* Если и этого оказалось недостаточно, просматриваются объявления в пространстве имен перед определением функции-члена.
Имена, встречающиеся в теле встроенной функции-члена, разрешаются так:
int _height;
class Screen {
public:
Screen( int _height ) {
_height = 0; // к чему относится _height? К параметру
}
private:
short _height;
};
В поисках объявления имени _height, которое встретилось в определении конструктора Screen, компилятор просматривает локальную область видимости функции и находит его там. Следовательно, это имя относится к объявлению параметра.
Если бы такое объявление не было найдено, компилятор начал бы поиск в области видимости класса Screen, просматривая все объявления его членов, пока не встретится объявление члена _height. Говорят, что имя члена _height скрыто объявлением параметра конструктора, но его можно использовать в теле конструктора, если квалифицировать имя члена именем его класса или явно использовать указатель this:
int _height;
class Screen {
public:
Screen( long _height ) {
this-_height = 0; // относится к Screen::_height
// тоже правильно:
// Screen::_height = 0;
}
private:
short _height;
};
Если бы не были найдены ни объявление параметра, ни объявление члена, компилятор стал бы искать их в объемлющих областях видимости пространств имен. В нашем примере в глобальной области видимости просматриваются объявления, которые расположены перед определением класса Screen. В результате было бы найдено объявление глобального объекта _height. Говорят, что такой объект скрыт за объявлением члена класса, однако его можно использовать в теле конструктора, если квалифицировать оператором разрешения глобальной области видимости:
int _height;
class Screen {
public:
Screen( long _height ) {
::_height = 0; // относится к глобальному объекту
}
private:
short _height;
};
Если конструктор объявлен вне определения класса, то на третьем шаге разрешения имени просматриваются объявления в глобальной области видимости, которые встретились перед определением класса Screen, а также перед определением функции-члена:
class Screen {
public:
// ...
void setHeight( int );
private:
short _height;
};
int verify(int);
void Screen::setHeight( int var ) {
// var: относится к параметру
// _height: относится к члену класса
// verify: относится к глобальной функции
_height = verify( var );
}
Обратите внимание, что объявление глобальной функции verify() невидимо до определения класса Screen. Однако на третьем шаге разрешения имени просматриваются объявления в областях видимости пространств имен, видимые перед определением члена, поэтому нужное объявление обнаруживается.
* Имя, встретившееся в определении статического члена класса, разрешается следующим образом: Просматриваются объявления всех членов класса.
* Если шаг 1 не привел к успеху, то просматриваются объявления, расположенные в областях видимости пространств имен перед определением статического члена, а не только предшествующие определению класса.
Упражнение 13.18
Назовите те части программы, которые находятся в области видимости класса.
Упражнение 13.19
Назовите те части программы, которые находятся в области видимости класса и для которых при разрешении имен просматривается полная область (т.е. принимаются во внимание все члены, объявленные в теле класса).
Упражнение 13.20
К каким объявлениям относится имя Type при использовании в теле класса Exersise и в определении его функции-члена setVal()? (Напоминаем, что разные вхождения могут относиться к разным объявлениям.) К каким объявлениям относится имя initVal при употреблении в определении функции-члена setVal()?
typedef int Type;
Type initVal();
class Exercise {
public:
// ...
typedef double Type;
Type setVal( Type );
Type initVal();
private:
int val;
};
Type Exercise::setVal( Type parm ) {
val = parm + initVal();
}
Определение функции-члена setVal() ошибочно. Можете ли вы сказать, почему? Внесите необходимые изменения, чтобы в классе Exercise использовался глобальный typedef Type и глобальная функция initVal().
Класс, объявленный внутри другого класса, называется вложенным. Он является членом объемлющего класса, а его определение может находиться в любой из секций public, private или protected объемлющего класса.
Имя вложенного класса известно в области видимости объемлющего класса, но ни в каких других областях. Это означает, что оно не конфликтует с таким же именем, объявленным в объемлющей области видимости. Например:
class Node { /* ... */ }
class Tree {
public:
// Node инкапсулирован внутри области видимости класса Tree
// В этой области Tree::Node скрывает ::Node
class Node {...};
// правильно: разрешается в пользу вложенного класса: Tree::Node
Node *tree;
};
// Tree::Node невидима в глобальной области видимости
// Node разрешается в пользу глобального объявления Node
Node *pnode;
class List {
public:
// Node инкапсулирован внутри области видимости класса List
// В этой области List::Node скрывает ::Node
class Node {...};
// правильно: разрешается в пользу вложенного класса: List::Node
Node *list;
};
Для вложенного класса допустимы такие же виды членов, как и для невложенного:
// Не идеально, будем улучшать
class List {
public:
class ListItem {
friend class List; // объявление друга
ListItem( int val=0 ); // конструктор
ListItem *next; // указатель на собственный класс
int value;
};
// ...
private:
ListItem *list;
ListItem *at_end;
};
Закрытым называется член, который доступен только в определениях членов и друзей класса. У объемлющего класса нет права доступа к закрытым членам вложенного. Чтобы в определениях членов List можно было обращаться к закрытым членам ListItem, класс ListItem объявляет List как друга. Равно и вложенный класс не имеет никаких специальных прав доступа к закрытым членам объемлющего класса. Если бы нужно было разрешить ListItem доступ к закрытым членам класса List, то в объемлющем классе List следовало бы объявить вложенный класс как друга. В приведенном выше примере этого не сделано, поэтому ListItem не может обращаться к закрытым членам List.
Объявление ListItem открытым членом класса List означает, что вложенный класс можно использовать как тип во всей программе, в том числе и за пределами определений членов и друзей класса. Например:
// правильно: объявление в глобальной области видимости
List::ListItem *headptr;
Это дает более широкую область видимости, чем мы планировали. Вложенный ListItem поддерживает абстракцию класса List и не должен быть доступен во всей программе. Поэтому лучше объявить вложенный класс ListItem закрытым членом List:
// Не идеально, будем улучшать
class List {
public:
// ...
private:
class ListItem {
// ...
};
ListItem *list;
ListItem *at_end;
};
Теперь тип ListItem доступен только из определений членов и друзей класса List, поэтому все члены класса ListItem можно сделать открытыми. При таком подходе объявление List как друга ListItem становится ненужным. Вот новое определение класса List:
// так лучше
class List {
public:
// ...
private:
// Теперь ListItem закрытый вложенный тип
class ListItem {
// а его члены открыты
public:
ListItem( int val=0 );
ListItem *next;
int value;
};
ListItem *list;
ListItem *at_end;
};
Конструктор ListItem не задан как встроенный внутри определения класса и, следовательно, должен быть определен вне него. Но где именно? Конструктор класса ListItem не является членом List и, значит, не может быть определен в теле последнего; его нужно определить в глобальной области видимости – той, которая содержит определение объемлющего класса. Когда функция-член вложенного класса не определяется как встроенная в теле, она должна быть определена вне самого внешнего из объемлющих классов.
Вот как могло бы выглядеть определение конструктора ListItem. Однако показанный ниже синтаксис в глобальной области видимости некорректен:
class List {
public:
// ...
private:
class ListItem {
public:
ListItem( int val=0 );
// ...
};
};
// ошибка: ListItem вне области видимости
ListItem:: ListItem( int val ) { ... }
Проблема в том, что имя ListItem отсутствует в глобальной области видимости. При использовании его таким образом следует указывать, что ListItem – вложенный класс в области видимости List. Это делается путем квалификации имени ListItem именем объемлющего класса. Следующая конструкция синтаксически правильна:
// имя вложенного класса квалифировано именем объемлющего
List::ListItem::ListItem( int val ) {
value = val;
next = 0;
}
Заметим, что квалифицировано только имя вложенного класса. Первый квалификатор List:: именует объемлющий класс и квалифицирует следующее за ним имя вложенного ListItem. Второе вхождение ListItem – это имя конструктора, а не вложенного класса. В данном определении имя члена некорректно:
// ошибка: конструктор называется ListItem, а не List::ListItem
List::ListItem::List::ListItem( int val ) {
value = val;
next = 0;
}
Если бы внутри ListItem был объявлен статический член, то его определение также следовало бы поместить в глобальную область видимости. Имя этого члена могло бы выглядеть так:
int List::ListItem::static_mem = 1024;
Обратите внимание, что функции-члены и статические данные-члены не обязаны быть открытыми членами вложенного класса для того, чтобы их можно было определить вне его тела. Закрытые члены ListItem также определяются в глобальной области видимости.
Вложенный класс разрешается определять вне тела объемлющего. Например, определение ListItem могло бы находиться и в глобальной области видимости:
class List {
public:
// ...
private:
// объявление необходимо
class ListItem;
ListItem *list;
ListItem *at_end;
};
// имя вложенного класса квалифицировано именем объемлющего класса
class List::ListItem {
public:
ListItem( int val=0 );
ListItem *next;
int value;
};
В глобальном определении имя вложенного ListItem должно быть квалифицировано именем объемлющего класса List. Заметьте, что объявление ListItem в теле List опустить нельзя. Определение вложенного класса не может быть задано в глобальной области видимости, если предварительно оно не было объявлено членом объемлющего класса. Но при этом вложенный класс не обязательно должен быть открытым членом объемлющего.
Пока компилятор не увидел определения вложенного класса, разрешается объявлять лишь указатели и ссылки на него. Объявления членов list и at_end класса List правильны несмотря на то, что ListItem определен в глобальной области видимости, поскольку оба члена – указатели. Если бы один из них был объектом, то его объявление в классе List привело бы к ошибке компиляции:
class List {
public:
// ...
private:
// объявление необходимо
class ListItem;
ListItem *list;
ListItem at_end; // ошибка: неопределенный вложенный класс ListItem
};
Зачем определять вложенный класс вне тела объемлющего? Возможно, он поддерживает некоторые детали реализации ListItem, а нам нужно скрыть их от пользователей класса List. Поэтому мы помещаем определение вложенного класса в заголовочный файл, содержащий интерфейс List. Таким образом, определение ListItem может находиться лишь внутри исходного файла, включающего реализацию класса List и его членов.
Вложенный класс можно сначала объявить, а затем определить в теле объемлющего. Это позволяет иметь во вложенных классах члены, ссылающиеся друг на друга:
class List {
public:
// ...
private:
// объявление List::ListItem
class ListItem;
class Ref {
// pli имеет тип List::ListItem*
ListItem *pli;
};
определение List::ListItem
class ListItem {
// pref имеет тип List::Ref*
Ref *pref;
};
};
Если бы ListItem не был объявлен перед определением класса Ref, то объявление члена pli было бы ошибкой.
Вложенный класс не может напрямую обращаться к нестатическим членам объемлющего, даже если они открыты. Любое такое обращение должно производиться через указатель, ссылку или объект объемлющего класса. Например:
class List {
public:
int init( int );
private:
class List::ListItem {
public:
ListItem( int val=0 );
void mf( const List & );
int value;
};
};
List::ListItem::ListItem { int val )
{
// List::init() - нестатический член класса List
// должен использоваться через объект или указатель на тип List
value = init( val ); // ошибка: неверное использование init
};
При использовании нестатических членов класса компилятор должен иметь возможность идентифицировать объект, которому принадлежит такой член. Внутри функции-члена класса ListItem указатель this неявно применяется лишь к его членам. Благодаря неявному this мы знаем, что член value относится к объекту, для которого вызван конструктор. Внутри конструктора ListItem указатель this имеет тип ListItem*. Для доступа же к функции-члену init() нужен объект типа List или указатель типа List*.
Следующая функция-член mf() обращается к init() с помощью параметра-ссылки. Таким образом, init() вызывается для объекта, переданного в аргументе функции:
void List::ListItem::mf( List &i1 ) {
memb = i1.init(); // правильно: обращается к init() по ссылке
}
Хотя для доступа к нестатическим членам объемлющего класса нужен объект, указатель или ссылка, к статическим его членам, именам типов и элементам перечисления вложенный класс может обращаться напрямую (если, конечно, эти члены открыты). Имя типа – это либо имя typedef, либо имя перечисления, либо имя класса. Например:
class List {
public:
typedef int (*pFunc)();
enum ListStatus { Good, Empty, Corrupted };
//...
private:
class ListItem {
public:
void check_status();
ListStatus status; // правильно
pFunc action; // правильно
// ...
};
// ...
};
pFunc, ListStatus и ListItem – все это вложенные имена типов в области видимости объемлющего класса List. К ним, а также к элементам перечисления ListStatus можно обращаться в области видимости класса ListItem даже без квалификации:
void List::ListItem::check_status()
{
ListStatus s = status;
switch ( s ) {
case Empty: ...
case Corrupted: ...
case Good: ...
}
}
Вне области видимости ListItem и List при обращении к статическим членам, именам типов и элементам перечисления объемлющего класса требуется оператор разрешения области видимости:
List::pFunc myAction; // правильно
List::ListStatus stat = List::Empty; // правильно
При обращении к элементам перечисления мы не пишем:
List::ListStatus::Empty
поскольку они доступны непосредственно в той области видимости, в которой определено само перечисление. Почему? Потому что с ним, в отличие от класса, не связана отдельная область.
Посмотрим, как разрешаются имена в определениях вложенного класса и его членов.
Имя, встречающееся в определении вложенного класса (кроме тех, которые употребляются во встроенных функциях-членах и аргументах по умолчанию) разрешается следующим образом:
* Просматриваются члены вложенного класса, расположенные перед употреблением имени.
* Если шаг 1 не привел к успеху, то просматриваются объявления членов объемлющего класса, расположенные перед употреблением имени. Если и этого недостаточно, то просматриваются объявления, расположенные в области видимости пространства имен перед определением вложенного класса.
Например:
enum ListStatus { Good, Empty, Corrupted };
class List {
public:
// ...
private:
class ListItem {
public:
// Смотрим в:
// 1) List::ListItem
// 2) List
// 3) глобальной области видимости
ListStatus status; // относится к глобальному перечислению
// ...
};
// ...
};
Сначала компилятор ищет объявление ListStatus в области видимости класса ListItem. Поскольку его там нет, поиск продолжается в области видимости List, а затем в глобальной. При этом во всех трех областях просматриваются только объявления, предшествующие использованию ListStatus. В конце концов находится глобальное объявление перечисления ListStatus – оно и будет типом, использованным в объявлении status.
Если вложенный класс ListItem определен в глобальной области видимости, вне тела объемлющего класса List, то все члены List уже были объявлены:
class List {
private:
class ListItem {
//...
public:
enum ListStatus { Good, Empty, Corrupted };
// ...
};
class List::ListItem {
public:
// Смотрим в:
// 1) List::ListItem
// 2) List
// 3) глобальной области видимости
ListStatus status; // относится к глобальному перечислению
// ...
};
При разрешении имени ListStatus сначала просматривается область видимости класса ListItem. Поскольку там его нет, поиск продолжается в области видимости List. Так как полное определение класса List уже встречалось, просматриваются все члены этого класса. Вложенное перечисление ListStatus найдено несмотря даже на то, что оно объявлено после объявления ListItem. Таким образом, status объявляется как указатель на данное перечисление в классе List. Если бы в List не было члена с таким именем, поиск был бы продолжен в глобальной области видимости среди тех объявлений, которые предшествуют определению класса ListItem.
Имя, встретившееся в определении функции-члена вложенного класса, разрешается следующим образом:
* Сначала просматриваются локальные области видимости функции-члена.
* Если шаг 1 не привел к успеху, то просматриваются объявления всех членов вложенного класса.
* Если имя еще не найдено, то просматриваются объявления всех членов объемлющего класса.
* Если и этого недостаточно, то просматриваются объявления, появляющиеся в области видимости пространства имен перед определением функции-члена.
Какое объявление относится к имени list в определении функции-члена check_status() в следующем фрагменте кода:
class List {
public:
enum ListStatus { Good, Empty, Corrupted };
// ...
private:
class ListItem {
public:
void check_status();
ListStatus status; // правильно
//...
};
ListItem *list;
};
int list = 0;
void List::ListItem::check_status()
{
int value = list; // какой list?
}
* Весьма вероятно, что при использовании list внутри check_status() программист имел в виду глобальный объект: и value, и глобальный объект list имеют тип int. Член List::list объявлен как указатель и не может быть присвоен value без явного приведения типа;
* ListItem не имеет прав доступа к закрытым членам объемлющего класса, в частности list;
* list – это нестатический член, и обращение к нему в функциях-членах ListItem должно производиться через объект, указатель или ссылку.
Однако, несмотря на все это, имя list, встречающееся в функции-члене check_status(), разрешается в пользу члена list класса List. Напоминаем, что если имя не найдено в области видимости вложенного ListItem, то далее просматривается область видимости объемлющего класса, а не глобальная. Член list в List скрывает глобальный объект. А так как использование указателя list в check_status() недопустимо, то выводится сообщение об ошибке.
Права доступа и совместимость типов проверяются только после того, как имя разрешено. Если при этом обнаруживается ошибка, то выдается сообщение о ней и дальнейший поиск объявления, которое было бы лучше согласовано с именем, уже не производится. Для доступа к глобальному объекту list следует использовать оператор разрешения области видимости:
void List::ListItem::check_status()
{
int value = ::list; // правильно
}
Если бы функция-член check_status() была определена как встроенная в теле класса ListItem, то последнее объявление привело бы к выдаче сообщения об ошибке из-за того, что имя list не объявлено в глобальной области видимости:
class List {
public:
// ...
private:
class ListItem {
public:
// ошибка: нет видимого объявления для ::list
void check_status() { int value = ::lis; }
//...
};
ListItem *list;
// ...
};
int list = 0;
Глобальный объект list объявлен после определения класса List. Во встроенной функции-члене, определенной внутри тела класса, рассматриваются только те глобальные объявления, которые были видны перед определением объемлющего класса. Если же определение check_status() следует за определением List, то рассматриваются глобальные объявления, расположенные перед ним, поэтому будет найдено глобальное определение объекта list.
Упражнение 13.21
В главе 11 был приведен пример программы, использующей класс iStack. Измените его, объявив классы исключений pushOnFull и popOnEmpty открытыми вложенными в iStack. Модифицируйте соответствующим образом определение класса iStack и его функций-членов, а также определение main().
Представленные до сих пор классы определены в области видимости глобального пространства имен. Но их можно определять и в объявленных пользователем пространствах. Имя класса, определенного таким образом, доступно только в области видимости этого пространства, т.е. оно не конфликтует с именами, объявленными в других пространствах имен. Например:
namespace cplusplus_primer {
class Node { /* ... */ };
}
namespace DisneyFeatureAnimation {
class Node { /* ... */ };
}
Node *pnode; // ошибка: Node не видно в глобальной области видимости
// правильно: объявляет nodeObj как объект
// квалифицированного типа DisneyFeatureAnimation::Node
DisneyFeatureAnimation::Node nodeObj;
// using-объявление делает Node видимым в глобальной области видимости
using cplusplus_primer::Node;
Node another; // cplusplus_primer::Node
Как было показано в двух предыдущих разделах, член класса (функция-член, статический член или вложенный класс) может быть определен вне его тела. Если мы реализуем библиотеку и помещаем определения наших классов в объявленное пользователем пространство имен, то где расположить определения членов, находящиеся вне тел своих классов? Их можно разместить либо в пространстве имен, которое содержит определение самого внешнего класса, либо в одном из объемлющих его пространств. Это дает возможность организовать код библиотеки следующим образом:
// --- 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. В любом случае имя члена в определении должно быть квалифицировано именами объемлющих классов и объявленных пользователем пространств, вне которых находится объявление члена.
Как происходит разрешение имени в определении члена, которое находится в объявленном пользователем пространстве? Например, как будет разрешено someVal:
int cplusplus_primer::List::ListItem::action() {
int local = someVal;
// ...
}
Сначала просматриваются локальные области видимости в определении функции-члена, затем поиск продолжается в области видимости ListItem, затем – в области видимости List. До этого момента все происходит так же, как в процессе разрешения имен, описанном в разделе 13.10. Далее просматриваются объявления из пространства cplusplus_primer и наконец объявления в глобальной области видимости, причем во внимание принимаются только те, которые расположены до определения функции-члена action():
// --- 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) { }
// ...
}
Определение пространства имен 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().
Здесь прослеживается аналогия с процессом поиска объявлений в глобальной области видимости, о котором мы говорили в предыдущих разделах: объявления, предшествующие определению члена, принимаются во внимание, тогда как следующие за ним игнорируются.
Довольно просто запомнить, в каком порядке просматриваются области видимости при поиске имени из определения функции, расположенного вне определения класса. Имена, которыми квалифицировано имя члена, указывают порядок рассмотрения пространств. Например, имя 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, объявите классы исключений pushOnFull и popOnEmpty как члены пространства имен LibException:
namespace LibException {
class pushOnFull{ };
class popOnEmpty{ };
}
а сам iStack – членом пространства имен Container. Модифицируйте соответствующим образом определение данного класса и его функций-членов, а также определение main().
Класс, определенный внутри тела функции, называется локальным. Он виден только в той локальной области, где определен. Не существует синтаксиса, позволяющего обратиться к члену такого класса, в отличие от вложенного, извне локальной области видимости, содержащей его определение. Поэтому функции-члены локального класса должны определяться внутри определения самого класса. На практике это ограничивает их сложность несколькими строками кода; помимо всего прочего, такой код становится трудно читать.
Поскольку невозможно определить член локального класса в области видимости пространства имен, то в таком классе не бывает статических членов.
Класс, вложенный в локальный, может быть определен вне определения объемлющего класса, но только в локальной области видимости, содержащей это определение. Имя вложенного класса в таком определении должно быть квалифицировано именем объемлющего класса. Объявление вложенного класса в объемлющем нельзя опускать:
void foo( int val )
{
class Bar {
public:
int barVal;
class nested; // объявление вложенного класса обязательно
};
// определение вложенного класса
class Bar::nexted {
// ...
};
}
У объемлющей функции нет никаких специальных прав доступа к закрытым членам локального класса. Разумеется, это можно обойти, объявив ее другом данного класса. Однако необходимость делать его члены закрытыми вообще сомнительна, поскольку часть программы, из которой разрешается обратиться к нему, весьма ограничена. Локальный класс инкапсулирован в своей локальной области видимости. Дальнейшая инкапсуляция путем сокрытия информации не требуется: вряд ли на практике найдется причина, по которой не все члены локального класса должны быть открыты.
У локального класса, как и у вложенного, ограничен доступ к именам из объемлющей области видимости. Он может обратиться только к именам типов, статических переменных и элементов перечислений, определенных в объемлющих локальных областях. Например:
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 не будет найдена, если только ее имени не предшествует оператор разрешения глобальной области видимости.
2014-09-07 13:25:31 Евгений
Вот это да, почитаю спасибо )
2013-11-01 03:57:59 Сергей
Спасибо за данное пособие, благодаря ему наконец то смог понять классы:)
2012-11-02 15:24:04 Pavel
спасибо за материал.ТО что нужно))
2012-06-08 07:31:08 cmd
Хороший материал
2012-03-27 20:42:52 Слава
Очень полезный материал и объяснено на доступном языке!!!
2012-01-31 09:33:40 Ivan
Пробуем:)
2012-01-03 03:10:13 Ion Botezatu
Супер, мне очень помог этот материа! Спосибо авторам!
2011-12-03 00:14:08 Makc
Отличный материал, спасибо за проделанную работу!
2011-10-05 21:44:13 Андрей
Отличный текст и всё нужно в одном месте. Часто пользуюсь! Спасибо.
2011-05-23 20:40:57 я
Спасибо!
2011-04-17 10:31:54 Аза
Спасибо за материал
В этой главе мы детально изучим автоматическую инициализацию, присваивание и уничтожение объектов классов в программе. Для поддержки инициализации служит конструктор - определенная проектировщиком функция (возможно, перегруженная), которая автоматически применяется к каждому объекту класса перед его первым использованием. Парная по отношению к конструктору функция, деструктор, автоматически применяется к каждому объекту класса по окончании его использования и предназначена для освобождения ресурсов, захваченных либо в конструкторе класса, либо на протяжении его жизни.
По умолчанию как инициализация, так и присваивание одного объекта класса другому выполняются почленно, т.е. путем последовательного копирования всех членов. Хотя этого обычно достаточно, при некоторых обстоятельствах такая семантика оказывается неадекватной. Тогда проектировщик класса должен предоставить специальный копирующий конструктор и копирующий оператор присваивания. Самое сложное в поддержке этих функций-членов - понять, что они должны быть написаны.
Рассмотрим следующее определение класса:
class Data {
public:
int ival;
char *ptr;
};
Чтобы безопасно пользоваться объектом класса, необходимо правильно инициализировать его члены. Однако смысл этого действия для разных классов различен. Например, может ли ival содержать отрицательное значение или нуль? Каковы правильные начальные значения обоих членов класса? Мы не ответим на эти вопросы, не понимая абстракции, представляемой классом. Если с его помощью описываются служащие компании, то ptr, вероятно, указывает на фамилию служащего, а ival - его уникальный номер. Тогда отрицательное или нулевое значения ошибочны. Если же класс представляет текущую температуру в городе, то допустимы любые значения ival. Возможно также, что класс Data представляет строку со счетчиком ссылок: в таком случае ival содержит текущее число ссылок на строку по адресу ptr. При такой абстракции ival инициализируется значением 1; как только значение становится равным 0, объект класса уничтожается.
Мнемонические имена класса и обоих его членов сделали бы, конечно, его назначение более понятным для читателя программы, но не дали бы никакой дополнительной информации компилятору. Чтобы компилятор понимал наши намерения, мы должны предоставить одну или несколько перегруженных функций инициализации - конструкторов. Подходящий конструктор выбирается в зависимости от множества начальных значений, указанных при определении объекта. Например, любая из приведенных ниже инструкций представляет корректную инициализацию объекта класса Data:
Data dat01( "Venus and the Graces", 107925 );
Data dat02( "about" );
Data dat03( 107925 );
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 объявлен перед ptr:
// ошибка: ival = "Anna Livia Plurabelle";
// ptr = 1024
Data.local2 - { "Anna Livia Plurabelle", 1024 };
Явная инициализация имеет два основных недостатка. Во-первых, она может быть применена лишь для объектов классов, все члены которых открыты (т.е. эта инициализация не поддерживает инкапсуляции данных и абстрактных типов - их не было в языке C, откуда она заимствована). А во-вторых, такая форма требует вмешательства программиста, что увеличивает вероятность появления ошибок (забыл включить список инициализации или перепутал порядок следования инициализаторов в нем).
Так нужно ли применять явную инициализацию вместо конструкторов? Да. Для некоторых приложений более эффективно использовать список для инициализации больших структур постоянными значениями. К примеру, мы можем таким образом построить палитру цветов или включить в текст программы фиксированные координаты вершин и значения в узлах сложной геометрической модели. В подобных случаях инициализация выполняется во время загрузки, что сокращает затраты времени на запуск конструктора, даже если он определен как встроенный. Это особенно удобно при работе с глобальными объектами1.
Однако в общем случае предпочтительным методом инициализации является конструктор, который гарантированно будет вызван компилятором для каждого объекта до его первого использования. В следующем разделе мы познакомимся с конструкторами детально.
Среди других функций-членов конструктор выделяется тем, что его имя совпадает с именем класса. Для объявления конструктора по умолчанию мы пишем2:
class Account {
public:
// конструктор по умолчанию ...
Account();
// ...
private:
char *_name;
unsigned int _acct_nmbr;
double _balance;
};
Единственное синтаксическое ограничение, налагаемое на конструктор, состоит в том, что он не должен иметь тип возвращаемого значения, даже void. Поэтому следующие объявления ошибочны:
// ошибки: у конструктора не может быть типа возвращаемого значения
void Account::Account() { ... }
Account* Account::Account( const char *pc ) { ... }
Количество конструкторов у одного класса может быть любым, лишь бы все они имели разные списки формальных параметров.
Откуда мы знаем, сколько и каких конструкторов определить? Как минимум, необходимо присвоить начальное значение каждому члену, который в этом нуждается. Например, номер счета либо задается явно, либо генерируется автоматически таким образом, чтобы гарантировать его уникальность. Предположим, что он будет создаваться автоматически. Тогда мы должны разрешить инициализировать оставшиеся два члена _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 );
Оба конструктора обладают необходимой пользователю функциональностью, поэтому оба решения приемлемы. Мы предпочитаем использовать аргумент по умолчанию, поскольку в такой ситуации общее число конструкторов класса сокращается.
Нужно ли поддерживать также задание одного лишь начального баланса без указания имени клиента? В данном случае спецификация класса явно запрещает это. Наш конструктор с двумя параметрами, из которых второй имеет значение по умолчанию, предоставляет полный интерфейс для указания начальных значений тех членов класса Account, которые могут быть инициализированы пользователем:
class Account {
public:
// конструктор по умолчанию ...
Account();
// имена параметров в объявлении указывать необязательно
Account( const char*, double=0.0 );
const char* name() { return name; }
// ...
private:
// ...
};
Ниже приведены два примера правильного определения объекта класса Account, где конструктору передается один или два аргумента:
int main()
{
// правильно: в обоих случаях вызывается конструктор
// с двумя параметрами
Account acct( "Ethan Stern" );
Account *pact = new Account( "Michael Lieberman", 5000 );
if ( strcmp( acct.name(), pact-name() ))
// ...
}
C++ требует, чтобы конструктор применялся к определенному объекту до его первого использования. Это означает, что как для acct, так и для объекта, на который указывает pact, конструктор будет вызван перед проверкой в инструкции if.
Компилятор перестраивает нашу программу, вставляя вызовы конструкторов. Вот как, по всей вероятности, будет модифицировано определение acct внутри main():
// псевдокод на C++,
// иллюстрирующий внутреннюю вставку конструктора
int main()
{
Account acct;
acct.Account::Account("Ethan Stern", 0.0);
// ...
}
Конечно, если конструктор определен как встроенный, то он подставляется в точке вызова.
Обработка оператора 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 закончился неудачей:
// конструктор не вызывается
}
// ...
}
Существует три в общем случае эквивалентных формы задания аргументов конструктора:
// в общем случае эти формы эквивалентны
Account acct1( "Anna Press" );
Account acct2 = Account( "Anna Press" );
Account acct3 = "Anna Press";
Форма acct3 может использоваться только при задании единственного аргумента. Если аргументов два или более, мы рекомендуем пользоваться формой acct1, хотя допустима и acct2.
// рекомендуемая форма вызова конструктора
Account acct1( "Anna Press" );
Новички часто допускают ошибку при объявлении объекта, инициализированного конструктором по умолчанию:
// увы! работает не так, как ожидалось
Account newAccount();
Эта инструкция компилируется без ошибок. Однако при попытке использовать объект в таком контексте:
// ошибка компиляции ...
if ( ! newAccount.name() ) ...
компилятор не сможет применить к функции нотацию доступа к членам класса. Определение
// определяет функцию newAccount,
// а не объект класса
Account newAccount();
интерпретируется компилятором как определение функции без параметров, которая возвращает объект типа Account. Правильное объявление объекта класса, инициализируемого конструктором по умолчанию, не содержит пустых скобок:
// правильно: определяется объект класса ...
Account newAccount;
Определять объект класса, не указывая списка фактических аргументов, можно в том случае, если в нем либо объявлен конструктор по умолчанию, либо вообще нет объявлений конструкторов. Если в классе объявлен хотя бы один конструктор, то не разрешается определять объект класса, не вызывая ни одного из них. В частности, если в классе определен конструктор, принимающий один или более параметров, но не определен конструктор по умолчанию, то в каждом определении объекта такого класса должны присутствовать необходимые аргументы. Можно возразить, что не имеет смысла определять конструктор по умолчанию для класса Account, поскольку не бывает счетов без имени владельца. В пересмотренной версии класса Account такой конструктор исключен:
class Account {
public:
// имена параметров в объявлении указывать необязательно
Account( const char*, double=0.0 );
const char* name() { return name; }
// ...
private:
// ...
};
Теперь при объявлении каждого объекта Account в конструкторе обязательно надо указать как минимум аргумент типа C-строки, но это скорее всего бессмысленно. Почему? Контейнерные классы (например, vector) требуют, чтобы для класса помещаемых в них элементов был либо задан конструктор по умолчанию, либо вообще никаких конструкторов. Аналогичная ситуация имеет место при выделении динамического массива объектов класса. Так, следующая инструкция вызвала бы ошибку компиляции для новой версии Account:
// ошибка: требуется конструктор по умолчанию для класса Account
Account *pact = new Account[ new_client_cnt ];
На практике часто требуется задавать конструктор по умолчанию, если имеются какие-либо другие конструкторы.
А если для класса нет разумных значений по умолчанию? Например, класс Account требует задавать для любого объекта фамилию владельца счета. В таком случае лучше всего установить состояние объекта так, чтобы было видно, что он еще не инициализирован корректными значениями:
// конструктор по умолчанию для класса Account
inline Account::
Account() {
_name = 0;
_balance = 0.0;
_acct_nmbr = 0;
}
Однако в функции-члены класса Account придется включить проверку целостности объекта перед его использованием.
Существует и альтернативный синтаксис: список инициализации членов, в котором через запятую указываются имена и начальные значения. Например, конструктор по умолчанию можно переписать следующим образом:
// конструктор по умолчанию класса Account с использованием
// списка инициализации членов
inline Account::
Account()
: _name(0),
_balance( 0.0 ), _acct_nmbr( 0 )
{}
Такой список допустим только в определении, но не в объявлении конструктора. Он помещается между списком параметров и телом конструктора и отделяется двоеточием. Вот как выглядит наш конструктор с двумя параметрами при частичном использовании списка инициализации членов:
inline Account::
Account( const char* name, double opening_bal )
: _balance( opening_bal )
{
_name = new char[ strlen(name)+1 ];
strcpy( _name, name );
_acct_nmbr = get_unique_acct_nmbr();
}
get_unique_acct_nmbr() - это не являющаяся открытой функция-член, которая возвращает гарантированно не использованный ранее номер счета.
Конструктор нельзя объявлять с ключевыми словами const или volatile (см. раздел 13.3.5), поэтому приведенные записи неверны:
class Account {
public:
Account() const; // ошибка
Account() volatile; // ошибка
// ...
};
Это не означает, что объекты класса с такими спецификаторами запрещено инициализировать конструктором. Просто к объекту применяется подходящий конструктор, причем без учета спецификаторов в объявлении объекта. Константность объекта класса устанавливается после того, как работа по его инициализации завершена, и пропадает в момент вызова деструктора. Таким образом, объект класса со спецификатором const считается константным с момента завершения работы конструктора до момента запуска деструктора. То же самое относится и к спецификатору volatile.
Рассмотрим следующий фрагмент программы:
// в каком-то заголовочном файле
extern void print( const Account &acct );
// ...
int main()
{
// преобразует строку "oops" в объект класса Account
// с помощью конструктора Account::Account( "oops", 0.0 )
print( "oops" );
// ...
}
По умолчанию конструктор с одним параметром (или с несколькими - при условии, что все параметры, кроме первого, имеют значения по умолчанию) играет роль оператора преобразования. В этом фрагменте программы конструктор Account неявно применяется компилятором для трансформации литеральной строки в объект класса Account при вызове print(), хотя в данной ситуации такое преобразование не нужно.
Непреднамеренные неявные преобразования классов, например трансформация "oops" в объект класса Account, оказались источником трудно обнаруживаемых ошибок. Поэтому в стандарт C++ было добавлено ключевое слово explicit, говорящее компилятору, что такие преобразования не нужны:
class Account {
public:
explicit Account( const char*, double=0.0 );
};
Данный модификатор применим только к конструктору. (Операторы преобразования и слово explicit обсуждаются в разделе 15.9.2.)
Конструктором по умолчанию называется конструктор, который можно вызывать, не задавая аргументов. Это не значит, что такой конструктор не может принимать аргументов; просто с каждым его формальным параметром ассоциировано значение по умолчанию:
// все это конструкторы по умолчанию
Account::Account() { ... }
iStack::iStack( int size = 0 ) { ... }
Complex::Complex(double re=0.0, double im=0.0) { ... }
Когда мы пишем:
int main()
{
Account acct;
// ...
}
* то компилятор сначала проверяет, определен ли для класса Account конструктор по умолчанию. Возникает одна из следующих ситуаций: Такой конструктор определен. Тогда он применяется к acct.
* Конструктор определен, но не является открытым. В данном случае определение acct помечается компилятором как ошибка: у функции main() нет прав доступа.
* Конструктор по умолчанию не определен, но есть один или несколько конструкторов, требующих задания аргументов. Определение acct помечается как ошибка: слишком мало аргументов у конструктора.
* Нет ни конструктора по умолчанию, ни какого-либо другого. Определение считается корректным, acct не инициализируется, конструктор не вызывается.
Пункты 1 и 3 должны быть уже достаточно понятны (если это не так, перечитайте данную главу) Посмотрим более внимательно на пункты 2 и 4.
Допустим, что все члены класса Account объявлены открытыми и не объявлено никакого конструктора:
class Account {
public:
char *_name;
unsigned int _acct_nmbr;
double _balance;
};
В таком случае при определении объекта класса Account специальной инициализации не производится. Начальные значения всех трех членов зависят только от контекста, в котором встретилось определение. Например, для статических объектов гарантируется, что все их члены будут обнулены (как и для объектов, не являющихся экземплярами классов):
// статический класс хранения
// вся ассоциированная с объектом память обнуляется
Account global_scope_acct;
static Account file_scope_acct;
Account foo()
{
static Account local_static_acct;
// ...
}
Однако объекты, определенные локально или распределенные динамически, в начальный момент будут содержать случайный набор битов, оставшихся в стеке программы:
// локальные и распределенные из хипа объекты не инициализированы
// до момента явной инициализации или присваивания
Account bar()
{
Account local_acct;
Account *heap_acct = new Account;
// ...
}
Новички часто полагают, что компилятор автоматически генерирует конструктор, если он не задан, и применяет его для инициализации членов класса. Для Account в том виде, в каком мы его определили, это неверно. Никакой конструктор не генерируется и не вызывается. Для более сложных классов, имеющих члены, которые сами являются классами, или использующих наследование, это отчасти справедливо: конструктор по умолчанию может быть сгенерирован, но и он не присваивает начальных значений членам встроенных или составных типов, таким, как указатели или массивы.
Если мы хотим, чтобы подобные члены инициализировались, то должны сами позаботиться об этом, предоставив один или несколько конструкторов. В противном случае отличить корректное значение члена такого типа от неинициализированного, если объект создан локально или распределен из хипа,3 практически невозможно.
Доступность конструктора определяется тем, в какой секции класса он объявлен. Мы можем ограничить или явно запретить некоторые формы создания объектов, если поместим соответствующий конструктор в неоткрытую секцию. В примере ниже конструктор по умолчанию класса Account объявлен закрытым, а с двумя параметрами - открытым:
class Account {
friend class vector;
public:
explicit Account( const char*, double = 0.0 );
// ...
private:
Account();
// ...
};
Обычная программа сможет теперь определять объекты класса Account, лишь указав как имя владельца счета, так и начальный баланс. Однако функции-члены Account и дружественный ему класс vector могут создавать объекты, пользуясь любым конструктором.
* Конструкторы, не являющиеся открытыми, в реальных программах C++ чаще всего используются для: предотвращения копирования одного объекта в другой объект того же класса (эта проблема рассматривается в следующем подразделе);
* указания на то, что конструктор должен вызываться только в случае, когда данный класс выступает в роли базового в иерархии наследования, а не для создания объектов, которыми программа может манипулировать напрямую (см. обсуждение наследования и объектно-ориентированного программирования в главе 17).
Инициализация объекта другим объектом того же класса называется почленной инициализацией по умолчанию. Копирование одного объекта в другой выполняется путем последовательного копирования каждого нестатического члена. Проектировщик класса может изменить такое поведение, предоставив специальный копирующий конструктор. Если он определен, то вызывается всякий раз, когда один объект инициализируется другим объектом того же класса.
Часто почленная инициализация не обеспечивает корректного поведения класса. Поэтому мы явно определяем копирующий конструктор. В нашем классе Account это необходимо, иначе два объекта будут иметь одинаковые номера счетов, что запрещено спецификацией класса.
Копирующий конструктор принимает в качестве формального параметра ссылку на объект класса (традиционно объявляемую со спецификатором const). Вот его реализация:
inline Account::
Account( const Account &rhs )
: _balance( rhs._balance )
{
_name = new char[ strlen(rhs._name) + 1 ];
strcpy( _name, rhs._name );
// копировать rhs._acct_nmbr нельзя
_acct_nmbr = get_unique_acct_nmbr();
}
Когда мы пишем:
Account acct2( acct1 );
компилятор определяет, объявлен ли явный копирующий конструктор для класса Account. Если он объявлен и доступен, то он и вызывается; а если недоступен, то определение acct2 считается ошибкой. В случае, когда копирующий конструктор не объявлен, выполняется почленная инициализация по умолчанию. Если впоследствии объявление копирующего конструктора будет добавлено или удалено, никаких изменений в программы пользователей вносить не придется. Однако перекомпилировать их все же необходимо. (Более подробно почленная инициализация рассматривается в разделе 14.6.)
Упражнение 14.1
* Какие из следующих утверждений ложны? Почему? У класса должен быть хотя бы один конструктор.
* Конструктор по умолчанию - это конструктор с пустым списком параметров.
* Если разумных начальных значений у членов класса нет, то не следует предоставлять конструктор по умолчанию.
* Если в классе нет конструктора по умолчанию, то компилятор генерирует его автоматически и инициализирует каждый член значением по умолчанию для соответствующего типа.
Упражнение 14.2
Предложите один или несколько конструкторов для данного множества членов. Объясните свой выбор:
class NoName {
public:
// здесь должны быть конструкторы
// ...
protected:
char *pstring;
int ival;
double dval;
};
Упражнение 14.3
* Выберите одну из следующих абстракций (или предложите свою собственную). Решите, какие данные (задаваемые пользователем) подходят для представляющего эту абстракцию класса. Напишите соответствующий набор конструкторов. Объясните свое решение. Книга Дата Служащий Транспортное средство Объект Дерево
Упражнение 14.4
Пользуясь приведенным определением класса:
class Account {
public:
Account();
explicit Account( const char*, double=0.0 );
// ...
};
объясните, что происходит в результате следующих определений:
(a) Account acct;
(b) Account acct2 = acct;
(c) Account acct3 = "Rena Stern ";
(d) Account acct4( "Anna Engel ", 400.00 );
(e) Account acct5 = Account( acct3 );
Упражнение 14.5
Параметр копирующего конструктора может и не быть константным, но обязан быть ссылкой. Почему ошибочна такая инструкция:
Account::Account( const Account rhs );
Одна из целей, стоящих перед конструктором, - обеспечить автоматическое выделение ресурса. Мы уже видели в примере с классом Account конструктор, где с помощью оператора new выделяется память для массива символов и присваивается уникальный номер счету. Можно также представить ситуацию, когда нужно получить монопольный доступ к разделяемой памяти или к критической секции потока. Для этого необходима симметричная операция, обеспечивающая автоматическое освобождение памяти или возврат ресурса по завершении времени жизни объекта, - деструктор. Деструктор - это специальная определяемая пользователем функция-член, которая автоматически вызывается, когда объект выходит из области видимости или когда к указателю на объект применяется операция delete. Имя этой функции образовано из имени класса с предшествующим символом "тильда" (~). Деструктор не возвращает значения и не принимает никаких параметров, а следовательно, не может быть перегружен. Хотя разрешается определять несколько таких функций-членов, лишь одна из них будет применяться ко всем объектам класса. Вот, например, деструктор для нашего класса Account:
class Account {
public:
Account();
explicit Account( const char*, double=0.0 );
Account( const Account& );
~Account();
// ...
private:
char *_name;
unsigned int _acct_nmbr;
double _balance;
};
inline
Account::~Account()
{
delete [] _name;
return_acct_number( _acct_nnmbr );
}
Обратите внимание, что в нашем деструкторе не сбрасываются значения членов:
inline
Account::~Account()
{
// необходимо
delete [] _name;
return_acct_number( _acct_nnmbr );
// необязательно
_name = 0;
_balance = 0.0;
_acct_nmbr = 0;
}
Делать это необязательно, поскольку отведенная под члены объекта память все равно будет освобождена. Рассмотрим следующий класс:
class Point3d {
public:
// ...
private:
float x, y, z;
};
Конструктор здесь необходим для инициализации членов, представляющих координаты точки. Нужен ли деструктор? Нет. Для объекта класса Point3d не требуется освобождать ресурсы: память выделяется и освобождается компилятором автоматически в начале и в конце его жизни.
В общем случае, если члены класса имеют простые значения, скажем, координаты точки, то деструктор не нужен. Не для каждого класса необходим деструктор, даже если у него есть один или более конструкторов. Основной целью деструктора является освобождения ресурсов, выделенных либо в конструкторе, либо во время жизни объекта, например освобождение замка или памяти, выделенной оператором new.
Но функции деструктора не ограничены только освобождением ресурсов. Он может реализовывать любую операцию, которая по замыслу проектировщика класса должна быть выполнена сразу по окончании использования объекта. Так, широко распространенным приемом для измерения производительности программы является определение класса Timer, в конструкторе которого запускается та или иная форма программного таймера. Деструктор останавливает таймер и выводит результаты замеров. Объект данного класса можно условно определять в критических участках программы, которые мы хотим профилировать, таким образом:
{
// начало критического участка программы
#ifdef PROFILE
Timer t;
#endif
// критический участок
// t уничтожается автоматически
// отображается затраченное время ...
}
Чтобы убедиться в том, что мы понимаем поведение деструктора (да и конструктора тоже), разберем следующий пример:
(1) #include "Account.h"
(2) Account global( "James Joyce" );
(3) int main()
(4) {
(5) Account local( "Anna Livia Plurabelle", 10000 );
(6) Account &loc_ref = global;
(7) Account *pact = 0;
(8)
(9) {
(10) Account local_too( "Stephen Hero" );
(11) pact = new Account( "Stephen Dedalus" );
(12) }
(13)
(14) delete pact;
(15) }
Сколько здесь вызывается конструкторов? Четыре: один для глобального объекта global в строке (2); по одному для каждого из локальных объектов local и local_too в строках (5) и (10) соответственно, и один для объекта, распределенного в хипе, в строке (11). Ни объявление ссылки loc_ref на объект в строке (6), ни объявление указателя pact в строке (7) не приводят к вызову конструктора. Ссылка - это псевдоним для уже сконструированного объекта, в данном случае для global. Указатель также лишь адресует объект, созданный ранее (в данном случае распределенный в хипе, строка (11)), или не адресует никакого объекта (строка (7)).
Аналогично вызываются четыре деструктора: для глобального объекта global, объявленного в строке (2), для двух локальных объектов и для объекта в хипе при вызове delete в строке (14). Однако в программе нет инструкции, с которой можно связать вызов деструктора. Компилятор просто вставляет эти вызовы за последним использованием объекта, но перед закрытием соответствующей области видимости.
Конструкторы и деструкторы глобальных объектов вызываются на стадиях инициализации и завершения выполнения программы. Хотя такие объекты нормально ведут себя при использовании в том файле, где они определены, но их применение в ситуации, когда производятся ссылки через границы файлов, становится в C++ серьезной проблемой.4
Деструктор не вызывается, когда из области видимости выходит ссылка или указатель на объект (сам объект при этом остается).
С++ с помощью внутренних механизмов препятствует применению оператора delete к указателю, не адресующему никакого объекта, так что соответствующие проверки кода необязательны:
// необязательно: неявно выполняется компилятором
if (pact != 0 ) delete pact;
Всякий раз, когда внутри функции этот оператор применяется к отдельному объекту, размещенному в хипе, лучше использовать объект класса auto_ptr, а не обычный указатель (см. обсуждение класса auto_ptr в разделе 8.4). Это особенно важно потому, что пропущенный вызов delete (скажем, в случае, когда возбуждается исключение) ведет не только к утечке памяти, но и к пропуску вызова деструктора. Ниже приводится пример программы, переписанной с использованием auto_ptr (она слегка модифицирована, так как объект класса auto_ptr может быть явно переустановлен для адресации другого объекта только присваиванием его другому auto_ptr):
#include memory
#include "Account.h"
Account global( "James Joyce" );
int main()
{
Account local( "Anna Livia Plurabelle", 10000 );
Account &loc_ref = global;
auto_ptr pact( new Account( "Stephen Dedalus" ));
{
Account local_too( "Stephen Hero" );
}
// объект auto_ptr уничтожается здесь
}
Иногда вызывать деструктор для некоторого объекта приходится явно. Особенно часто такая необходимость возникает в связи с оператором new (см. раздел 8.4). Рассмотрим пример. Когда мы пишем:
char *arena = new char[ sizeof Image ];
то из хипа выделяется память, размер которой равен размеру объекта типа Image, она не инициализирована и заполнена случайными битами. Если же написать:
Image *ptr = new (arena) Image( "Quasimodo");
то никакой новой памяти не выделяется. Вместо этого переменной ptr присваивается адрес, ассоциированный с переменной arena. Теперь память, на которую указывает ptr, интерпретируется как занимаемая объектом класса Image, и конструктор применяется к уже существующей области. Таким образом, оператор размещения new() позволяет сконструировать объект в ранее выделенной области памяти.
Закончив работать с изображением Quasimodo, мы можем произвести какие-то операции с изображением Esmerelda, размещенным по тому же адресу arena в памяти:
Image *ptr = new (arena) Image( "Esmerelda" );
Однако изображение Quasimodo при этом будет затерто, а мы его модифицировали и хотели бы записать на диск. Обычно сохранение выполняется в деструкторе класса Image, но если мы применим оператор delete:
// плохо: не только вызывает деструктор, но и освобождает память
delete ptr;
то, помимо вызова деструктора, еще и возвратим в хип память, чего делать не следовало бы. Вместо этого можно явно вызвать деструктор класса Image:
ptr-~Image();
сохранив отведенную под изображение память для последующего вызова оператора размещения new.
Отметим, что, хотя ptr и arena адресуют одну и ту же область памяти в хипе, применение оператора delete к arena
// деструктор не вызывается
delete arena;
не приводит к вызову деструктора класса Image, так как arena имеет тип char*, а компилятор вызывает деструктор только тогда, когда операндом в delete является указатель на объект класса, имеющего деструктор.
Встроенный деструктор может стать причиной непредвиденного увеличения размера программы, поскольку он вставляется в каждой точке выхода внутри функции для каждого активного локального объекта. Например, в следующем фрагменте
Account acct( "Tina Lee" );
int swt;
// ...
switch( swt ) {
case 0:
return;
case 1:
// что-то сделать
return;
case 2:
// сделать что-то другое
return;
// и так далее
}
компилятор подставит деструктор перед каждой инструкцией return. Деструктор класса Account невелик, и затраты времени и памяти на его подстановку тоже малы. В противном случае придется либо объявить деструктор невстроенным, либо реорганизовать программу. В примере выше инструкцию return в каждой метке case можно заменить инструкцией break с тем, чтобы у функции была единственная точка выхода:
// переписано для обеспечения единственной точки выхода
switch( swt ) {
case 0:
break;
case 1:
// что-то сделать
break;
case 2:
// сделать что-то другое
break;
// и так далее
}
// единственная точка выхода
return;
Упражнение 14.6
Напишите подходящий деструктор для приведенного набора членов класса, среди которых pstring адресует динамически выделенный массив символов:
class NoName {
public:
~NoName();
// ...
private:
char *pstring;
int ival;
double dval;
};
Упражнение 14.7
Необходим ли деструктор для класса, который вы выбрали в упражнении 14.3? Если нет, объясните почему. В противном случае предложите реализацию.
Упражнение 14.8
Сколько раз вызываются деструкторы в следующем фрагменте:
void mumble( const char *name, fouble balance, char acct_type )
{
Account acct;
if ( ! name )
return;
if ( balance = 99 )
return;
switch( acct_type ) {
case 'z': return;
case 'a':
case 'b': return;
}
// ...
}
Массив объектов класса определяется точно так же, как массив элементов встроенного типа. Например:
Account table[ 16 ];
определяет массив из 16 объектов Account. Каждый элемент по очереди инициализируется конструктором по умолчанию. Можно и явно передать конструкторам аргументы внутри заключенного в фигурные скобки списка инициализации массива. Строка:
Account pooh_pals[] = { "Piglet", "Eeyore", "Tigger" };
определяет массив из трех элементов, инициализируемых конструкторами:
Account( "Piglet", 0.0 ); // первый элемент (Пятачок)
Account( "Eeyore", 0.0 ); // второй элемент (Иа-Иа)
Account( "Tigger", 0.0 ); // третий элемент (Тигра)
Один аргумент можно задать явно, как в примере выше. Если же необходимо передать несколько аргументов, то придется воспользоваться явным вызовом конструктора:
Account pooh_pals[] = {
Account( "Piglet", 1000.0 ),
Account( "Eeyore", 1000.0 ),
Account( "Tigger", 1000.0 )
};
Чтобы включить в список инициализации массива конструктор по умолчанию, мы употребляем явный вызов с пустым списком параметров:
Account pooh_pals[] = {
Account( "Woozle", 10.0 ), // Бука
Account( "Heffalump", 10.0 ), // Слонопотам
Account();
};
Эквивалентный массив из трех элементов можно объявить и так:
Account pooh_pals[3] = {
Account( "Woozle", 10.0 ),
Account( "Heffalump", 10.0 )
};
Таким образом, члены списка инициализации последовательно используются для заполнения очередного элемента массива. Те элементы, для которых явные аргументы не заданы, инициализируются конструктором по умолчанию. Если его нет, то в списке должны быть заданы аргументы конструктора для каждого элемента массива.
Доступ к отдельным элементам массива объектов производится с помощью оператора взятия индекса, как и для массива элементов любого из встроенных типов. Например:
pooh_pals[0];
обращается к Piglet, а
pooh_pals[1];
к Eeyore и т.д. Для доступа к членам объекта, находящегося в некотором элементе массива, мы сочетаем операторы взятия индекса и доступа к членам:
pooh_pals[1]._name != pooh_pals[2]._name;
Не существует способа явно указать начальные значения элементов массива, память для которого выделена из хипа. Если класс поддерживает создание динамических массивов с помощью оператора new, он должен либо иметь конструктор по умолчанию, либо не иметь никаких конструкторов. На практике почти у всех классов есть такой конструктор.
Объявление
Account *pact = new Account[ 10 ];
создает в памяти, выделенной из хипа, массив из десяти объектов класса Account, причем каждый инициализируется конструктором по умолчанию.
Чтобы уничтожить массив, адресованный указателем pact, необходимо применить оператор delete. Однако написать
// увы! это не совсем правильно
delete pact;
недостаточно, так как pact при этом не идентифицируется как массив объектов. В результате деструктор класса Account применяется лишь к первому элементу массива. Чтобы применить его к каждому элементу, мы должны включить пустую пару скобок между оператором delete и адресом удаляемого объекта:
// правильно:
// показывает, что pact адресует массив
delete [] pact;
Пустая пара скобок говорит о том, что pact адресует именно массив. Компилятор определяет, сколько в нем элементов, и применяет деструктор к каждому из них.
По умолчанию инициализация массива объектов, распределенного из хипа, проходит в два этапа: выделение памяти для массива, к каждому элементу которого применяется конструктор по умолчанию, если он определен, и последующее присваивание значения каждому элементу.
Чтобы свести инициализацию к одному шагу, программист должен вмешаться и поддержать следующую семантику: задать начальные значения для всех или некоторых элементов массива и гарантировать применение конструктора по умолчанию для тех элементов, начальные значения которых не заданы. Ниже приведено одно из возможных программных решений, где используется оператор размещения new:
#include utility
#include vector
#include new
#include cstddef
#include "Accounts.h"
typedef pairchar*, double
value_pair;
/* init_heap_array()
* объявлена как статическая функция-член
* обеспечивает выделение памяти из хипа и инициализацию
* массива объектов
* init_values: пары начальных значений элементов массива
* elem_count: число элементов в массиве
* если 0, то размером массива считается размер вектора
* init_values
*/
Account*
Account::
init_heap_array(
vectorvalue_pair &init_values,
vectorvalue_pair ::size_type elem_count = 0 )
{
vectorvalue_pair ::size_type
vec_size = init_value.size();
if ( vec_size == 0 && elem_count == 0 )
return 0;
// размер массива равен либо elem_count,
// либо, если elem_count == 0, размеру вектора ...
size_t elems = elem_count
? elem_count : vec_size();
// получить блок памяти для размещения массива
char *p = new char[sizeof(Account)*elems];
// по отдельности инициализировать каждый элемент массива
int offset = sizeof( Account );
for ( int ix = 0; ix elems; ++ix )
{
// смещение ix-ого элемента
// если пара начальных значений задана,
// передать ее конструктору;
// в противном случае вызвать конструктор по умолчанию
if ( ix vec_size )
new( p+offset*ix ) Account( init_values[ix].first,
init_values[ix].second );
else new( p+offset*ix ) Account;
}
// отлично: элементы распределены и инициализированы;
// вернуть указатель на первый элемент
return (Account*)p;
}
Необходимо заранее выделить блок памяти, достаточный для хранения запрошенного массива, как массив байт, чтобы избежать применения к каждому элементу конструктора по умолчанию. Это делается в такой инструкции:
char *p = new char[sizeof(Account)*elems];
Далее программа в цикле обходит этот блок, присваивая на каждой итерации переменной p адрес следующего элемента и вызывая либо конструктор с двумя параметрами, если задана пара начальных значений, либо конструктор по умолчанию:
for ( int ix = 0; ix
В разделе 14.3 говорилось, что оператор размещения new позволяет применить конструктор класса к уже выделенной области памяти. В данном случае мы используем new для поочередного применения конструктора класса Account к каждому из выделенных элементов массива. Поскольку при создании инициализированного массива мы подменили стандартный механизм выделения памяти, то должны сами позаботиться о ее освобождении. Оператор delete работать не будет:
delete [] ps;
Почему? Потому что ps (мы предполагаем, что эта переменная была инициализирована вызовом init_heap_array()) указывает на блок памяти, полученный не с помощью стандартного оператора new, поэтому число элементов в массиве компилятору неизвестно. Так что всю работу придется сделать самим:
void
Account::
dealloc_heap_array( Account *ps, size_t elems )
{
for ( int ix = 0; ix elems; ++ix )
ps[ix].Account::~Account();
delete [] reinterpret_castchar*(ps);
}
Если в функции инициализации мы пользовались арифметическими операциями над указателями для доступа к элементам:
new( p+offset*ix ) Account;
то здесь мы обращаемся к ним, задавая индекс в массиве ps:
ps[ix].Account::~Account();
Хотя и ps, и p адресуют одну и ту же область памяти, ps объявлен как указатель на объект класса Account, а p - как указатель на char. Индексирование p дало бы ix-й байт, а не ix-й объект класса Account. Поскольку с p ассоциирован не тот тип, что нужно, арифметические операции над указателями приходится программировать самостоятельно.
Мы объявляем обе функции статическими членами класса:
typedef pairchar*, double value_pair;
class Account {
public:
// ...
static Account* init_heap_array(
vector value_pair &init_values,
vector value_pair ::size_type elem_count = 0 );
static void dealloc_heap_array( Account*, size_t );
// ...
};
Когда определяется вектор из пяти объектов класса, например:
vector Point vec( 5 );
* то инициализация элементов производится в следующем порядке5: С помощью конструктора по умолчанию создается временный объект типа класса, хранящегося в векторе.
* К каждому элементу вектора применяется копирующий конструктор, в результате чего каждый объект инициализируется копией временного объекта.Временный объект уничтожается.
Хотя конечный результат оказывается таким же, как при определении массива из пяти объектов класса:
Point pa[ 5 ];
эффективность подобной инициализации вектора ниже, так как, во-первых, на конструирование и уничтожение временного объекта, естественно, нужны ресурсы, а во-вторых, копирующий конструктор обычно оказывается вычислительно более сложным, чем конструктор по умолчанию.
Общее правило проектирования таково: вектор объектов класса удобнее только для вставки элементов, т.е. в случае, когда изначально определяется пустой вектор. Если мы заранее вычислили, сколько придется вставлять элементов, или имеем на этот счет обоснованное предположение, то надо зарезервировать необходимую память, а затем приступать к вставке. Например:
vector Point cvs; // пустой
int cv_cnt = calc_control_vertices();
// зарезервировать память для хранения cv_cnt объектов класса Point
// cvs все еще пуст ...
cvs.reserve( cv_cnt );
// открыть файл и подготовиться к чтению из него
ifstream infile( "spriteModel" );
istream_iterator Point cvfile( infile ),eos;
// вот теперь можно вставлять элементы
copy( cvfile, eos, inserter( cvs, cvs.begin() ));
(Алгоритм copy(), итератор вставки inserter и потоковый итератор чтения istream_iterator рассматривались в главе 12.) Поведение объектов list (список) и deque (двусторонняя очередь) аналогично поведению объектов vector (векторов). Вставка объекта в любой из этих контейнеров осуществляется с помощью копирующего конструктора.
Упражнение 14.9
Какие из приведенных инструкций неверны? Исправьте их.
(a) Account *parray[10] = new Account[10];
(b) Account iA[1024] = {
"Nhi", "Le", "Jon", "Mike", "Greg", "Brent", "Hank"
"Roy", "Elena" };
(c) string *ps=string[5]("Tina","Tim","Chyuan","Mira","Mike");
(d) string as[] = *ps;
Упражнение 14.10
Что лучше применить в каждой из следующих ситуаций: статический массив (такой, как Account pA[10]), динамический массив или вектор? Объясните свой выбор.
Внутри функции Lut() нужен набор из 256 элементов для хранения объектов класса Color. Значения являются константами.
Необходимо хранить набор из неизвестного числа объектов класса Account. Данные счетов читаются из файла.
Функция gen_words(elem_size) должна сгенерировать и передать обработчику текста набор из elem_size строк.
Упражнение 14.11
Потенциальным источником ошибок при использовании динамических массивов является пропуск пары квадратных скобок, говорящей, что указатель адресует массив, т.е. неверная запись
// печально: не проверяется, что parray адресует массив
delete parray;
вместо
// правильно: определяется размер массива, адресуемого parray
delete [] parray;
Наличие пары скобок заставляет компилятор найти размер массива. Затем к каждому элементу по очереди применяется деструктор (всего size раз). Если же скобок нет, уничтожается только один элемент. В любом случае освобождается вся память, занятая массивом.
При обсуждении первоначального варианта языка С++ много спорили о том, должно ли наличие квадратных скобок инициировать поиск или же (как было в исходной спецификации) лучше поручить программисту явно указывать размер массива:
// в первоначальном варианте языка размер массива требовалось задавать явно
delete p[10] parray;
Как вы думаете, почему язык был изменен таким образом, что явного задания размера не требуется (а значит, нужно уметь его сохранять и извлекать), но скобки, хотя и пустые, в операторе delete остались (так что компилятор не должен запоминать, адресует указатель единственный объект или массив)? Какой вариант языка предложили бы вы?
Модифицируем наш класс Account, объявив член _name типа string:
#include string
class Account {
public:
// ...
private:
unsigned int _acct_nmbr;
double _balance;
string _name;
};
Придется заодно изменить и конструкторы. Возникает две проблемы: поддержание совместимости с первоначальным интерфейсом и инициализация объекта класса с помощью подходящего набора конструкторов.
Исходный конструктор Account с двумя параметрами
Account( const char*, double = 0.0 );
не может инициализировать член типа string. Например:
string new_client( " Steve Hall" );
Account new_acct( new_client, 25000 );
не будет компилироваться, так как не существует неявного преобразования из типа string в тип char*. Инструкция
Account new_acct( new_client.c_str(), 25000 );
правильна, но вызовет у пользователей класса недоумение. Одно из решений - добавить новый конструктор вида:
Account( string, double = 0.0 );
Если написать:
Account new_acct( new_client, 25000 );
вызывается именно этот конструктор, тогда как старый код
Account *open_new_account( const char *nm )
{
Account *pact = new Account( nm );
// ...
return pacct;
}
по-прежнему будет приводить к вызову исходного конструктора с двумя параметрами.
Так как в классе string определено преобразование из типа char* в тип string (преобразования классов обсуждаются в этой главе ниже), то можно заменить исходный конструктор на новый, которому в качестве первого параметра передается тип string. В таком случае, когда встречается инструкция:
Account myAcct( " Tinkerbell" );
" Tinkerbell" преобразуется во временный объект типа string. Затем этот объект передается новому конструктору с двумя параметрами.
При проектировании приходится идти на компромисс между увеличением числа конструкторов класса Account и несколько менее эффективной обработкой аргументов типа char* из-за необходимости создавать временный объект. Мы предоставили две версии конструктора с двумя параметрами. Тогда модифицированный набор конструкторов Account будет таким:
#include string
class Account {
public:
Account();
Account( const char*, double=0.0 );
Account( const string&, double=0.0 );
Account( const Account& );
// ...
private:
// ...
};
* Как правильно инициализировать член, являющийся объектом некоторого класса с собственным набором конструкторов? Этот вопрос можно разделить на три: где вызывается конструктор по умолчанию? Внутри конструктора по умолчанию класса Account;
* где вызывается копирующий конструктор? Внутри копирующего конструктора класса Account и внутри конструктора с двумя параметрами, принимающего в качестве первого тип string;
* как передать аргументы конструктору класса, являющегося членом другого класса? Это необходимо делать внутри конструктора Account с двумя параметрами, принимающего в качестве первого тип char*.
Решение заключается в использовании списка инициализации членов (мы упоминали о нем в разделе 14.2). Члены, являющиеся классами, можно явно инициализировать с помощью списка, состоящего из разделенных запятыми пар "имя члена/значение". Наш конструктор с двумя параметрами теперь выглядит так (напомним, что _name - это член, являющийся объектом класса string):
inline Account::
Account( const char* name, double opening_bal )
: _name( name ), _balance( opening_bal )
{
_acct_nmbr = het_unique_acct_nmbr();
}
Список инициализации членов следует за сигнатурой конструктора и отделяется от нее двоеточием. В нем указывается имя члена, а в скобках - начальные значения, что аналогично синтаксису вызова функции. Если член является объектом класса, то эти значения становятся аргументами, передаваемыми подходящему конструктору, который затем и используется. В нашем примере значение name передается конструктору string, который применяется к члену _name. Член _balance инициализируется значением opening_bal.
Аналогично выглядит второй конструктор с двумя параметрами:
inline Account::
Account( const string& name, double opening_bal )
: _name( name ), _balance( opening_bal )
{
_acct_nmbr = het_unique_acct_nmbr();
}
В этом случае вызывается копирующий конструктор string, инициализирующий член _name значением параметра name типа string.
Часто у новичков возникает вопрос: в чем разница между использованием списка инициализации и присваиванием значений членам в теле конструктора? Например, в чем разница между
inline Account::
Account( const char* name, double opening_bal )
: _name( name ), _balance( opening_bal )
{
_acct_nmbr = het_unique_acct_nmbr();
}
и
Account( const char* name, double opening_bal )
{
_name = name;
_balance = opening_bal;
_acct_nmbr = het_unique_acct_nmbr();
}
В конце работы обоих конструкторов все три члена будут иметь одинаковые значения. Разница в том, что только список обеспечивает инициализацию тех членов, которые являются объектами класса. В теле конструктора установка значения члена - это не инициализация, а присваивание. Важно это различие или нет, зависит от природы члена.
С концептуальной точки зрения выполнение конструктора состоит из двух фаз: фаза явной или неявной инициализации и фаза вычислений, включающая все инструкции в теле конструктора. Любая установка значений членов во второй фазе рассматривается как присваивание, а не инициализация. Непонимание этого различия приводит к ошибкам и неэффективным программам.
Первая фаза может быть явной или неявной в зависимости от того, имеется ли список инициализации членов. При неявной инициализации сначала вызываются конструкторы по умолчанию всех базовых классов в порядке их объявления, а затем конструкторы по умолчанию всех членов, являющихся объектами классов. (Базовые классы мы будем рассматривать в главе 17 при обсуждении объектно-ориентированного программирования.) Например, если написать:
inline Account::
Account()
{
_name = "";
_balance = 0.0;
_acct_nmbr = 0;
}
то фаза инициализации будет неявной. Еще до выполнения тела конструктора вызывается конструктор по умолчанию класса string, ассоциированный с членом _name. Это означает, что присваивание _name пустой строки излишне.
Для объектов классов различие между инициализацией и присваиванием существенно. Член, являющийся объектом класса, всегда следует инициализировать с помощью списка, а не присваивать ему значение в теле конструктора. Более правильной является следующая реализация конструктора по умолчанию класса Account:
inline Account::
Account() : _name( string() )
{
_balance = 0.0;
_acct_nmbr = 0;
}
Мы удалили ненужное присваивание _name из тела конструктора. Явный же вызов конструктора по умолчанию string излишен. Ниже приведена эквивалентная, но более компактная версия:
inline Account::
Account()
{
_balance = 0.0;
_acct_nmbr = 0;
}
Однако мы еще не ответили на вопрос об инициализации двух членов встроенных типов. Например, так ли существенно, где происходит инициализация _balance: в списке инициализации или в теле конструктора? Инициализация и присваивание членам, не являющимся объектами классов, эквивалентны как с точки зрения результата, так и с точки зрения производительности (за двумя исключениями). Мы предпочитаем использовать список:
// предпочтительный стиль инициализации
inline Account::
Account() : _balance( 0.0 ), _acct_nmbr( 0 )
{}
Два вышеупомянутых исключения - это константные члены и члены-ссылки независимо от типа. Для них всегда нужно использовать список инициализации, в противном случае компилятор выдаст ошибку:
class ConstRef {
public:
ConstRef(int ii );
private:
int i;
const int ci;
int
};
ConstRef::
ConstRef( int ii )
{ // присваивание
i = ii; // правильно
ci = ii; // ошибка: нельзя присваивать константному члену
ri = i; // ошибка: ri не инициализирована
}
К началу выполнения тела конструктора инициализация всех константных членов и членов-ссылок должна быть завершена. Для этого нужно указать их в списке инициализации. Правильная реализация предыдущего примера такова:
// правильно: инициализируются константные члены и ссылки
ConstRef::
ConstRef( int ii )
: ci( ii ), ri ( i )
{ i = ii; }
Каждый член должен встречаться в списке инициализации не более одного раза. Порядок инициализации определяется не порядком следования имен в списке, а порядком объявления членов. Если дано следующее объявление членов класса Account:
class Account {
public:
// ...
private:
unsigned int _acct_nmbr;
double _balance;
string _name;
};
то порядок инициализации для такой реализации конструктора по умолчанию
inline Account::
Account() : _name( string() ), _balance( 0.0 ), _acct_nmbr( 0 )
{}
будет следующим: _acct_nmbr, _balance, _name. Однако члены, указанные в списке (или в неявно инициализируемом члене-объекте класса), всегда инициализируются раньше, чем производится присваивание членам в теле конструктора. Например, в следующем конструкторе:
inline Account::
Account( const char* name, double bal )
: _name( name ), _balance( bal )
{
_acct_nmbr = get_unique_acct_nmbr();
}
порядок инициализации такой: _balance, _name, _acct_nmbr.
Расхождение между порядком инициализации и порядком следования членов в соответствующем списке может приводить к трудным для обнаружения ошибкам, когда один член класса используется для инициализации другого:
class X {
int i;
int j;
public:
// видите проблему?
X( int val )
: j( val ), i( j )
{}
// ...
};
кажется, что перед использованием для инициализации i член j уже инициализирован значением val, но на самом деле i инициализируется первым, для чего применяется еще неинициализированный член j. Мы рекомендуем помещать инициализацию одного члена другим (если вы считаете это необходимым) в тело конструктора:
// предпочтительная идиома
X::X( int val ) : i( val ) { j = i; }
Упражнение 14.12
Что неверно в следующих определениях конструкторов? Как бы вы исправили обнаруженные ошибки?
(a) Word::Word( char *ps, int count = 1 )
: _ps( new char[strlen(ps)+1] ),
_count( count )
{
if ( ps )
strcpy( _ps, ps );
else {
_ps = 0;
_count = 0;
}
}
(b) class CL1 {
public:
CL1() { c.real(0.0); c.imag(0.0); s = " not set" ; }
// ...
private:
complex c;
string s;
}
(c) class CL2 {
public:
CL2( mapstring,location *pmap, string key )
: _text( key ), _loc( (*pmap)[key] ) {}
// ...
private:
location _loc;
string _text;
};
Инициализация одного объекта класса другим объектом того же класса, как, например:
Account oldAcct( " Anna Livia Plurabelle" );
Account newAcct( oldAcct );
называется почленной инициализацией по умолчанию. По умолчанию - потому, что она производится автоматически, независимо от того, есть явный конструктор или нет. Почленной - потому, что единицей инициализации является отдельный нестатический член, а не побитовая копия всего объекта класса.
Такую инициализацию проще всего представить, если считать, что компилятор создает специальный внутренний копирующий конструктор, где поочередно, в порядке объявления, инициализируются все нестатические члены. Если рассмотреть первое определение нашего класса Account:
class Account {
public:
// ...
private:
char *_name;
unsigned int _acct_nmbr;
double _balance;
};
то можно представить, что копирующий конструктор по умолчанию определен так:
inline Account::
Account( const Account &rhs )
{
_name = rhs._name;
_acct_nmbr = rhs._acct_nmbr;
_balance = rhs._balance;
}
Почленная инициализация одного объекта класса другим встречается в следующих ситуациях:
1. явная инициализация одного объекта другим:
Account newAcct( oldAcct );
1. передача объекта класса в качестве аргумента функции:
extern bool cash_on_hand( Account acct );
if ( cash_on_hand( oldAcct ))
// ...
1. передача объекта класса в качестве возвращаемого функцией значения:
extern Account
consolidate_accts( const vector Account & )
{
Account final_acct;
// выполнить финансовую операцию
return final_acct;
}
1. определение непустого последовательного контейнера:
// вызывается пять копирующих конструкторов класса string
vector string svec( 5 );
(В этом примере с помощью конструктора string по умолчанию создается один временный объект, который затем копируется в пять элементов вектора посредством копирующего конструктора string.)
1. вставка объекта класса в контейнер:
svec.push_back( string( "pooh"));
Для большинства определений реальных классов почленная инициализация по умолчанию не соответствует семантике класса. Чаще всего это случается, когда его член представляет собой указатель, который адресует освобождаемую деструктором память в хипе, как, например, в нашем Account.
В результате такой инициализации newAcct._name и oldAcct._name указывают на одну и ту же C-строку. Если oldAcct выходит из области видимости и к нему применяется деструктор, то newAcct._name указывает на освобожденную область памяти. С другой стороны, если newAcct модифицирует строку, адресуемую _name, то она изменяется и для oldAcct. Подобные ошибки очень трудно найти.
Одно из решений псевдонимов указателей заключается в том, чтобы выделить область памяти для копии строки и инициализировать newAcct._name адресом этой области. Следовательно, почленную инициализацию по умолчанию для класса Account нужно подавить за счет предоставления явного копирующего конструктора, который реализует правильную семантику инициализации.
Внутренняя семантика класса также может не соответствовать почленной инициализации по умолчанию. Ранее мы уже объясняли, что два разных объекта Account не должны иметь одинаковые номера счетов. Чтобы гарантировать такое поведение, мы должны подавить почленную инициализацию по умолчанию для класса Account. Вот как выглядит копирующий конструктор, решающий обе эти проблемы:
inline Account::
Account( const Account &rhs )
{
// решить проблему псевдонима указателя
_name = new char[ strlen(rhs._name)+1 ];
strcpy( _name, rhs._name );
// решить проблему уникальности номера счета
_acct_nmbr = get_unique_acct_nmbr();
// копирование этого члена и так работает
_balance = rhs._balance;
}
* Альтернативой написанию копирующего конструктора является полный запрет почленной инициализации. Это можно сделать следующим образом: Объявить копирующий конструктор закрытым членом. Это предотвратит почленную инициализацию всюду, кроме функций-членов и друзей класса.
* Запретить почленную инициализацию в функциях-членах и друзьях класса, намеренно не предоставляя определения копирующего конструктора (однако объявить его так, как описано на шаге 1, все равно нужно). Язык не дает нам возможности ограничить доступ к закрытым членам класса со стороны функций-членов и друзей. Но если определение отсутствует, то любая попытка вызвать копирующий конструктор, законная с точки зрения компилятора, приведет к ошибке во время редактирования связей, поскольку не удастся найти определение символа.
Чтобы запретить почленную инициализацию, класс Account можно объявить так:
class Account {
public:
Account();
Account( const char*, double=0.0 );
// ...
private:
Account( const Account& );
// ...
};
Что произойдет, если в объявлении _name заменить C-строку на тип класса string? Как это повлияет на почленную инициализацию по умолчанию? Как надо будет изменить явный копирующий конструктор? Мы ответим на эти вопросы в данном подразделе.
При почленной инициализации по умолчанию исследуется каждый член. Если он принадлежит к встроенному или составному типу, то такая инициализация применяется непосредственно. Например, в первоначальном определении класса Account член _name инициализируется непосредственно, так как это указатель:
newAcct._name = oldAcct._name;
Члены, являющиеся объектами классов, обрабатываются по-другому. В инструкции
Account newAcct( oldAcct );
оба объекта распознаются как экземпляры Account. Если у этого класса есть явный копирующий конструктор, то он и применяется для задания начального значения, в противном случае выполняется почленная инициализация по умолчанию.
Таким образом, если обнаруживается член-объект класса, то описанный выше процесс применяется рекурсивно. У класса есть явный копирующий конструктор? Если да, вызвать его для задания начального значения члена-объекта класса. Иначе применить к этому члену почленную инициализацию по умолчанию. Если все члены этого класса принадлежат к встроенным или составным типам, то каждый инициализируется непосредственно и процесс на этом завершается. Если же некоторые члены сами являются объектами классов, то алгоритм применяется к ним рекурсивно, пока не останется ничего, кроме встроенных и составных типов.
В нашем примере у класса string есть явный копирующий конструктор, поэтому _name инициализируется с помощью его вызова. Копирующий конструктор по умолчанию для класса Account выглядит следующим образом (хотя явно он не определен):
inline Account::
Account( const Account &rhs )
{
_acct_nmbr = rhs._acct_nmbr;
_balance = rhs._balance;
// Псевдокод на C++
// иллюстрирует вызов копирующего конструктора
// для члена, являющегося объектом класса
_name.string::string( rhs._name );
}
Теперь почленная инициализация по умолчанию для класса Account корректно обрабатывает выделение и освобождение памяти для _name, но все еще неверно копирует номер счета, поэтому приходится кодировать явный копирующий конструктор. Однако приведенный ниже фрагмент не совсем правилен. Можете ли вы сказать, почему?
// не совсем правильно...
inline Account::
Account( const Account &rhs )
{
_name = rhs._name;
_balance = rhs._balance;
_acct_nmbr = get_unique_acct_nmbr();
}
Эта реализация ошибочна, поскольку в ней не различаются инициализация и присваивание. В результате вместо вызова копирующего конструктора string мы вызываем конструктор string по умолчанию на фазе неявной инициализации и копирующий оператор присваивания string - в теле конструктора. Исправить это несложно:
inline Account::
Account( const Account &rhs )
: _name( rhs._name )
{
_balance = rhs._balance;
_acct_nmbr = get_unique_acct_nmbr();
}
Самое главное - понять, что такое исправление необходимо. (Обе реализации приводят к тому, что в _name копируется значение из rhs._name, но в первой одна и та же работа выполняется дважды.) Общее эвристическое правило состоит в том, чтобы по возможности инициализировать все члены-объекты классов в списке инициализации членов.
Упражнение 14.13
* Для какого определения класса скорее всего понадобится копирующий конструктор? Представление Point3w, содержащее четыре числа с плавающей точкой.
* Класс matrix, в котором память для хранения матрицы выделяется динамически в конструкторе и освобождается в деструкторе.
* Класс payroll (платежная ведомость), где каждому объекту приписывается уникальный идентификатор.
* Класс word (слово), содержащий объект класса string и вектор, в котором хранятся пары (номер строки, смещение в строке).
Упражнение 14.14
Реализуйте для каждого из данных классов копирующий конструктор, конструктор по умолчанию и деструктор.
(a) class BinStrTreeNode {
public:
// ...
private:
string _value;
int _count;
BinStrTreeNode *_leftchild;
BinStrTreeNode *_rightchild;
};
(b) class BinStrTree {
public:
// ...
private:
BinStrTreeNode *_root;
};
(c) class iMatrix {
public:
// ...
private:
int _rows;
int _cols;
int *_matrix;
};
(d) class theBigMix {
public:
// ...
private:
BinStrTree _bst;
iMatrix _im;
string _name;
vectorMfloat *_pvec;
};
Упражнение 14.15
Нужен ли копирующий конструктор для того класса, который вы выбрали в упражнении 14.3 из раздела 14.2? Если нет, объясните почему. Если да, реализуйте его.
Упражнение 14.16
Идентифицируйте в следующем фрагменте программы все места, где происходит почленная инициализация:
Point global;
Point foo_bar( Point arg )
{
Point local = arg;
Point *heap = new Point( global );
*heap = local;
Point pa[ 4 ] = { local, *heap };
return *heap;
}
Присваивание одному объекту класса значения другого объекта того же класса реализуется почленным присваиванием по умолчанию. От почленной инициализации по умолчанию оно отличается только использованием копирующего оператора присваивания вместо копирующего конструктора:
newAcct = oldAcct;
по умолчанию присваивает каждому нестатическому члену newAcct значение соответственного члена oldAcct. Компилятор генерирует следующий копирующий оператор присваивания:
inline Account&
Account::
operator=( const Account &rhs )
{
_name = rhs._name;
_balance = rhs._balance;
_acct_nmbr = rhs._acct_nmbr;
}
Как правило, если для класса не подходит почленная инициализация по умолчанию, то не подходит и почленное присваивание по умолчанию. Например, для первоначального определения класса Account, где член _name был объявлен как char*, такое присваивание не годится ни для _name, ни для _acct_nmbr.
Мы можем подавить его, если предоставим явный копирующий оператор присваивания, где будет реализована подходящая для класса семантика:
// общий вид копирующего оператора присваивания
className&
className::
operator=( const className &rhs )
{
// не надо присваивать самому себе
if ( this != &rhs )
{
// здесь реализуется семантика копирования класса
}
// вернуть объект, которому присвоено значение
return *this;
}
Здесь условная инструкция
if ( this != &rhs )
предотвращает присваивание объекта класса самому себе, что особенно неприятно в ситуации, когда копирующий оператор присваивания сначала освобождает некоторый ресурс, ассоциированный с объектом в левой части, чтобы назначить вместо него ресурс, ассоциированный с объектом в правой части. Рассмотрим копирующий оператор присваивания для класса Account:
Account&
Account::
operator=( const Account &rhs )
{
// не надо присваивать самому себе
if ( this != &rhs )
{
delete [] _name;
_name = new char[strlen(rhs._name)+1];
strcpy( _name,rhs._name );
_balance = rhs._balance;
_acct_nmbr = rhs._acct_nmbr;
}
return *this;
}
Когда один объект класса присваивается другому, как, например, в инструкции:
newAcct = oldAcct;
выполняются следующие шаги:
* Выясняется, есть ли в классе явный копирующий оператор присваивания.
* Если есть, проверяются права доступа к нему, чтобы понять, можно ли его вызывать в данном месте программы.
* Оператор вызывается для выполнения присваивания; если же он недоступен, компилятор выдает сообщение об ошибке.
* Если явного оператора нет, выполняется почленное присваивание по умолчанию.
* При почленном присваивании каждому члену встроенного или составного члена объекта в левой части присваивается значение соответственного члена объекта в правой части.
* Для каждого члена, являющегося объектом класса, рекурсивно применяются шаги 1-6, пока не останутся только члены встроенных и составных типов.
Если мы снова модифицируем определение класса Account так, что _name будет иметь тип string, то почленное присваивание по умолчанию
newAcct = oldAcct;
будет выполняться так же, как при создании компилятором следующего оператора присваивания:
inline Account&
Account::
operator=( const Account &rhs )
{
_balance = rhs._balance;
_acct_nmbr = rhs._acct_nmbr;
// этот вызов правилен и с точки зрения программиста
name.string::operator=( rhs._name );
}
Однако почленное присваивание по умолчанию для объектов класса Account не подходит из-за _acct_nmbr. Нужно реализовать явный копирующий оператор присваивания с учетом того, что _name - это объект класса string:
Account&
Account::
operator=( const Account &rhs )
{
// не надо присваивать самому себе
if ( this != &rhs )
{
// вызывается string::operator=( const string& )
_name = rhs._name;
_balance = rhs._balance;
}
return *this;
}
Чтобы запретить почленное копирование, мы поступаем так же, как и в случае почленной инициализации: объявляем оператор закрытым и не предоставляем его определения.
Копирующий конструктор и копирующий оператор присваивания обычно рассматривают вместе. Если необходим один, то, как правило, необходим и другой. Если запрещается один, то, вероятно, следует запретить и другой.
Упражнение 14.17
Реализуйте копирующий оператор присваивания для каждого из классов, определенных в упражнении 14.14 из раздела 14.6.
Упражнение 14.18
Нужен ли копирующий оператор присваивания для того класса, который вы выбрали в упражнении 14.3 из раздела 14.2? Если да, реализуйте его. В противном случае объясните, почему он не нужен.
В общем случае объект класса эффективнее передавать функции по указателю или по ссылке, нежели по значению. Например, если дана функция с сигнатурой:
bool sufficient_funds( Account acct, double );
то при каждом ее вызове требуется выполнить почленную инициализацию формального параметра acct значением фактического аргумента-объекта класса Account. Если же функция имеет любую из таких сигнатур:
bool sufficient_funds( Account *pacct, double );
bool sufficient_funds( Account &acct, double );
то достаточно скопировать адрес объекта Account. В этом случае никакой инициализации класса не происходит (см. обсуждение взаимосвязи между ссылочными и указательными параметрами в разделе 7.3).
Хотя возвращать указатель или ссылку на объект класса также более эффективно, чем сам объект, но корректно запрограммировать это достаточно сложно. Рассмотрим такой оператор сложения:
// задача решается, но для больших матриц эффективность может
// оказаться неприемлемо низкой
Matrix
operator+( const Matrix& m1, const Matrix& m2 )
{
Matrix result;
// выполнить арифметические операции ...
return result;
}
Этот перегруженный оператор позволяет пользователю писать
Matrix a, b;
// ...
// в обоих случаях вызывается operator+()
Matrix c = a + b;
a = b + c;
Однако возврат результата по значению может потребовать слишком больших затрат времени и памяти, если Matrix представляет собой большой и сложный класс. Если эта операция выполняется часто, то она, вероятно, резко снизит производительность.
Следующая пересмотренная реализация намного увеличивает скорость:
// более эффективно, но после возврата адрес оказывается недействительным
// это может привести к краху программы
Matrix&
operator+( const Matrix& m1, const Matrix& m2 )
{
Matrix result;
// выполнить сложение ...
return result;
}
но при этом происходят частые сбои программы. Дело в том, что значение переменной result не определено после выхода из функции, в которой она объявлена. (Мы возвращаем ссылку на локальный объект, который после возврата не существует.)
Значение возвращаемого адреса должно оставаться действительным после выхода из функции. В приведенной реализации возвращаемый адрес не затирается:
// нет возможности гарантировать отсутствие утечки памяти
// поскольку матрица может быть большой, утечки будут весьма заметными
Matrix&
operator+( const Matrix& m1, const Matrix& m2 )
{
Matrix *result = new Matrix;
// выполнить сложение ...
return *result;
}
Однако это неприемлемо: происходит большая утечка памяти, так как ни одна из частей программы не отвечает за применение оператора delete к объекту по окончании его использования.
Вместо оператора сложения лучше применять именованную функцию, которой в качестве третьего параметра передается ссылка, где следует сохранить результат:
// это обеспечивает нужную эффективность,
// но не является интуитивно понятным для пользователя
void
mat_add( Matrix &result,
const Matrix& m1, const Matrix& m3 )
{
// вычислить результат
}
Таким образом, проблема производительности решается, но для класса уже нельзя использовать операторный синтаксис, так что теряется возможность инициализировать объекты
// более не поддерживается
Matrix c = a + b;
и использовать их в выражениях:
// тоже не поддерживается
if ( a + b c ) ...
Неэффективный возврат объекта класса - слабое место С++. В качестве одного из решений предлагалось расширить язык, введя имя возвращаемого функцией объекта:
Matrix&
operator+( const Matrix& m1, const Matrix& m2 )
name result
{
Matrix result;
// ...
return result;
}
Тогда компилятор мог бы самостоятельно переписать функцию, добавив к ней третий параметр-ссылку:
// переписанная компилятором функция
// в случае принятия предлагавшегося расширения языка
void
operator+( Matrix &result, const Matrix& m1, const Matrix& m2 )
name result
{
// вычислить результат
}
и преобразовать все вызовы этой функции, разместив результат непосредственно в области, на которую ссылается первый параметр. Например:
Matrix c = a + b;
было бы трансформировано в
Matrix c;
operator+(c, a, b);
Это расширение так и не стало частью языка, но предложенная оптимизация прижилась. Компилятор в состоянии распознать, что возвращается объект класса и выполнить трансформацию его значения и без явного расширения языка. Если дана функция общего вида:
classType
functionName( paramList )
{
classType namedResult;
// выполнить какие-то действия ...
return namedResult;
}
то компилятор самостоятельно трансформирует как саму функцию, так и все обращения к ней:
void
functionName( classType &namedResult, paramList )
{
// вычислить результат и разместить его по адресу namedResult
}
что позволяет уйти от необходимости возвращать значение объекта и вызывать копирующий конструктор. Чтобы такая оптимизация была применена, в каждой точке возврата из функции должен возвращаться один и тот же именованный объект класса.
И последнее замечание об эффективности работы с объектами в C++. Инициализация объекта класса вида
Matrix c = a + b;
всегда эффективнее присваивания. Например, результат следующих двух инструкций такой же, как и в предыдущем случае:
Matrix c;
c = a + b;
но объем требуемых вычислений значительно больше. Аналогично эффективнее писать:
for ( int ix = 0; ix
чем
Matrix matSum;
for ( int ix = 0; ix
Причина, по которой присваивание всегда менее эффективно, состоит в том, что возвращенный локальный объект нельзя подставить вместо объекта в левой части оператора присваивания. Иными словами, в то время как инструкцию
Point3d p3 = operator+( p1, p2 );
можно безопасно трансформировать:
// Псевдокод на C++
Point3d p3;
operator+( p3, p1, p2 );
преобразование
Point3d p3;
p3 = operator+( p1, p2 );
в
// Псевдокод на C++
// небезопасно в случае присваивания
operator+( p3, p1, p2 );
небезопасно.
Преобразованная функция требует, чтобы переданный ей объект представлял собой неформатированную область памяти. Почему? Потому что к объекту сразу применяется конструктор, который уже был применен к именованному локальному объекту. Если переданный объект уже был сконструирован, то делать это еще раз с семантической точки зрения неверно.
Что касается инициализируемого объекта, то отведенная под него память еще не подвергалась обработке. Если же объекту присваивается значение и в классе объявлены конструкторы (а именно этот случай мы и рассматриваем), можно утверждать, что эта память уже форматировалась одним из них, так что непосредственно передавать объект функции небезопасно.
Вместо этого компилятор должен создать неформатированную область памяти в виде временного объекта класса, передать его функции, а затем почленно присвоить возвращенный временный объект объекту, стоящему в левой части оператора присваивания. Наконец, если у класса есть деструктор, то он применяется к временному объекту. Например, следующий фрагмент
Point3d p3;
p3 = operator+( p1, p2 );
трансформируется в такой:
// Псевдокод на C++
Point3d temp;
operator+( temp, p1, p2 );
p3.Point3d::operator=( temp );
temp.Point3d::~Point3d();
Майкл Тиманн (Michael Tiemann), автор компилятора GNU C++, предложил назвать это расширение языка именованным возвращаемым значением (return value language extension). Его точка зрения изложена в работе [LIPPMAN96b]. В нашей книге “Inside the C++ Object Model” ([LIPPMAN96a]) приводится детальное обсуждение затронутых в этой главе тем.
2012-11-28 08:45:13 vertical
эффективнее писать: for ( int ix = 0; ix size-2; ++ix ) { Matrix matSum = mat[ix] + mat[ix+1]; // ... } чем Matrix matSum; for ( int ix = 0; ix size-2; ++ix ) { matSum = mat[ix] + mat[ix+1]; // ... } Не всегда. На каждой итерации цикла будет создаваться и уничтожаться объект matSum. Тут производительность зависит еще и от вычислительной сложности конструктора и деструктора объекта.
2012-10-19 15:36:29 Ярослав
Спасибо, очень содержательно!