Язык программирования C++. Вводный курс. С для начинающих
Скачать 5.41 Mb.
|
612 const string Account::name( "Savings Account" ); Константный статический член целого типа инициализируется константой внутри тела класса: это особый случай. Если бы для хранения названия счета мы решили использовать массив символов вместо строки, то его размер можно было бы задать с помощью константного члена типа int: const string Account::name[nameSize] = "Savings Account"; Отметим, что константный статический член целого типа, инициализированный константой, – это константное выражение. Проектировщик может объявить такой статический член, если внутри тела класса возникает необходимость в именованной константе. Например, поскольку константный статический член nameSize является константным выражением, проектировщик использует его для задания размера члена- массива с именем name. Даже если такой член инициализируется в теле класса, его все равно необходимо задать вне определения класса. Однако поскольку начальное значение уже задано в объявлении, то при определении оно не указывается. Так как name – это массив (и не целого типа), его нельзя инициализировать в теле класса. Попытка поступить таким образом приведет к ошибке компиляции: }; Член name должен быть инициализирован вне определения класса. Обратите внимание, что член nameSize задает размер массива name в определении, находящемся вне тела класса: #include // ... private: static const string name; }; // заголовочный файл class Account { //... private: static const int nameSize = 16; static const string name[nameSize]; }; // исходный файл const string Account::nameSize; // необходимо определение члена class Account { //... private: static const int nameSize = 16; // правильно: целый тип static const string name[nameSize] = "Savings Account"; // ошибка С++ для начинающих 613 const string Account::name[nameSize] = "Savings Account"; nameSize не квалифицирован именем класса Account. И хотя это закрытый член, определение name не приводит к ошибке. Как такое может быть? Определение статического члена аналогично определению функции-члена класса, которое может ссылаться на закрытые члены. Определение статического члена name находится в области видимости класса и может ссылаться на закрытые члены, после того как распознано квалифицированное имя Account::name. (Подробнее об области видимости класса мы поговорим в разделе 13.9.) Статический член класса доступен функции-члену того же класса и без использования соответствующих операторов: } Что же касается функций, не являющихся членами класса, то они могут обращаться к статическому члену двумя способами. Во-первых, посредством операторов доступа: } Как ac1._interestRate, так и ac2->_interestRate относятся к статическому члену Account::_interestRate Поскольку есть лишь одна копия статического члена класса, до нее необязательно добираться через объект или указатель. Другой способ заключается в том, чтобы обратиться к статическому члену напрямую, квалифицировав его имя именем класса: if ( Account::_interestRate < 0.05 ) Если обращение к статическому члену производится без помощи оператора доступа, то его имя следует квалифицировать именем класса, за которым следует оператор разрешения области видимости: 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; // ... // доступ к статическому члену с указанием квалифицированного имени С++ для начинающих 614 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 ); С++ для начинающих 615 13.5.1. Статические функции-члены Функции-члены raiseInterest() и interest() обращаются к глобальному статическому члену _interestRate: } Проблема в том, что любая функция-член должна вызываться с помощью оператора доступа к конкретному объекту класса. Поскольку приведенные выше функции обращаются только к статическому _interestRate, то совершенно безразлично, для какого объекта они вызываются. Нестатические члены при вызове этих функций не читаются и не модифицируются. Поэтому лучше объявить такие функции-члены как статические. Это можно сделать следующим образом: } Объявление статической функции-члена почти такое же, как и нестатической: в теле класса ему предшествует ключевое слово static, а спецификаторы const или volatile запрещены. В ее определении, находящемся вне тела класса, слова static быть не должно. Такой функции-члену указатель this не передается, поэтому явное или неявное обращение к нему внутри ее тела вызывает ошибку компиляции. В частности, попытка обращения к нестатическому члену класса неявно требует наличия указателя this и, следовательно, запрещена. Например, представленную ранее функцию-член dailyReturn() нельзя объявить статической, поскольку она обращается к нестатическому члену _amount. class Account { public: void raiseInterest( double incr ); double interest() { return _interestRate; } private: static double _interestRate; }; inline void Account::raiseInterest( double incr ) { _interestRate += incr; 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; С++ для начинающих 616 Статическую функцию-член можно вызвать для объекта класса, пользуясь одним из операторов доступа. Ее также можно вызвать непосредственно, квалифицировав ее имя, даже если никаких объектов класса не объявлено. Вот небольшая программа, иллюстрирующая их применение: } Упражнение 13.8 Пусть дан класс Y с двумя статическими данными-членами и двумя статическими функциями-членами: #include #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; С++ для начинающих 617 }; Инициализируйте _xval значением 20, а _callsXval значением 0. Упражнение 13.9 Используя классы из упражнения 13.8, реализуйте обе статические функции-члена для класса Y. callsXval() должна подсчитывать, сколько раз вызывалась xval(). Упражнение 13.10 Какие из следующих объявлений и определений статических членов ошибочны? Почему? vector 13.6. Указатель на член класса Предположим, что в нашем классе Screen определены четыре новых функции-члена: forward() , back(), up() и down(), которые перемещают курсор соответственно вправо, влево, вверх и вниз. Сначала мы должны объявить их в теле класса: 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; // example.h class Example { public: static double rate = 6.5; static const int vecSize = 20; static vector }; // example.c #include "example.h" double Example::rate; С++ для начинающих 618 }; Функции-члены forward() и back() перемещают курсор на один символ. По достижении правого нижнего или левого верхнего угла экрана курсор переходит в противоположный угол. } end() перемещает курсор в правый нижний угол экрана и является парной по отношению к функции-члену home(): } Функции up() и down() перемещают курсор вверх и вниз на одну строку. По достижении верхней или нижней строки курсор остается на месте и подается звуковой сигнал: class Screen { public: inline Screen& forward(); inline Screen& back(); inline Screen& end(); inline Screen& up(); inline Screen& down(); // другие функции-члены не изменяются private: inline int row(); // другие функции-члены не изменяются 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; inline Screen& Screen::end() { _cursor = _width * _height - 1; return *this; С++ для начинающих 619 } row() – это закрытая функция-член, которая используется в функциях up() и down(), возвращая номер строки, где находится курсор: } Пользователи класса Screen попросили нас добавить функцию repeat(), которая повторяет указанное действие n раз. Ее реализация могла бы выглядеть так: } Такая реализация имеет ряд недостатков. В частности, предполагается, что функции- члены класса Screen останутся неизменными, поэтому при добавлении или удалении функции-члена repeat() необходимо модифицировать. Вторая проблема – размер функции. Поскольку приходится проверять все возможные функции-члены, то исходный текст становится громоздким и неоправданно сложным. В более общей реализации параметр op заменяется параметром типа указателя на функцию-член класса Screen. Теперь repeat() не должна сама устанавливать, какую 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; inline int Screen::row() { // вернуть текущую строку return ( _cursor + _width ) / height; Screen &repeat( char op, int times ) { switch( op ) { case DOWN: // n раз вызвать Screen::down() break; case DOWN: // n раз вызвать Screen::up() break; // ... } С++ для начинающих 620 операцию следует выполнить, и всю инструкцию switch можно удалить. Определение и использование указателей на члены класса – тема последующих подразделов. 13.6.1. Тип члена класса Указателю на функцию нельзя присвоить адрес функции-члена, даже если типы возвращаемых значений и списки параметров полностью совпадают. Например, переменная pfi – это указатель на функцию без параметров, которая возвращает значение типа int: int (*pfi)(); Если имеются глобальные функции HeightIs() и WidthIs() вида: int WidthIs(); то допустимо присваивание pfi адреса любой из этих переменных: pfi = WidthIs; В классе Screen также определены две функции доступа, height() и width(), не имеющие параметров и возвращающие значение типа int: inline int Screen::width() { return _width; } Однако попытка присвоить их переменной pfi является нарушением типизации и влечет ошибку компиляции: В чем нарушение? У функций-членов есть дополнительный атрибут типа, отсутствующий у функций, не являющихся членами, – класс. Указатель на функцию-член должен соответствовать типу присваиваемой ему функции не в двух, а в трех отношениях: по типу и количеству формальных параметров; типу возвращаемого значения; типу класса, членом которого является функция. Несоответствие типов между двумя указателями – на функцию-член и на обычную функцию – обусловлено их разницей в представлении. В указателе на обычную функцию хранится ее адрес, который можно использовать для непосредственного вызова. (Указатели на функции рассматривались в разделе 7.9.) Указатель же на функцию-член int HeightIs(); pfi = HeightIs; inline int Screen::height() { return _height; } // неверное присваивание: нарушение типизации pfi = &Screen::height; С++ для начинающих 621 должен быть сначала привязан к объекту или указателю на объект, чтобы получить 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::*)() Указатели на функции-члены можно объявлять, инициализировать и присваивать: pmf2 = &Screen::width; // всем указателям на функции-члены класса можно присвоить значение 0 int (Screen::*pmf1)() = 0; int (Screen::*pmf2)() = &Screen::height; pmf1 = pmf2; С++ для начинающих 622 Использование typedef может облегчить чтение объявлений указателей на члены. Например, для типа “указатель на функцию-член класса Screen без параметров, которая возвращает ссылку на объект Screen”, т.е. Screen& (Screen::*)() Следующий typedef определяет Action как альтернативное имя: Action next = &Screen::forward; Тип “указатель на функцию-член” можно использовать для объявления формальных параметров и типа возвращаемого значения функции. Для параметра того же типа можно также указать значение аргумента по умолчанию: Screen& action( Screen&, Action)(); action() объявлена как принимающая два параметра: ссылку на объект класса Screen и указатель на функцию-член Screen без параметров, которая возвращает ссылку на его объект. Вызвать action() можно любым из следующих способов: } В следующем подразделе обсуждается вызов функции-члена посредством указателя. 13.6.2. Работа с указателями на члены класса К указателям на члены класса можно обращаться только с помощью конкретного объекта или указателя на объект типа класса. Для этого применяется любой из двух операторов доступа (.* для объектов класса и ссылок на них или ->* для указателей). Например, так вызывается функция-член через указатель на нее: typedef Screen& (Screen::*Action)(); Action default = &Screen::home; 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 ); С++ для начинающих 623 (bufScreen->*pmfS)( myScreen ); Вызовы (bufScreen->*pmfi)(); требуют скобок, поскольку приоритет оператора вызова () выше, чем приоритет взятия указателя на функцию-член. Без скобок myScreen.*pmfi() интерпретируется как myScreen.*(pmfi()) Это означает вызов функции pmfi() и привязку возвращенного ей значения к оператору (.*). Разумеется, тип pmfi не поддерживает такого использования, так что компилятор выдаст сообщение об ошибке. Указатели на данные-члены используются аналогично: tmpScreen->*pW = myScreen.*pW; Приведем реализацию функции-члена repeat(), которую мы обсуждали в начале этого раздела. Теперь она будет принимать указатель на функцию-член: 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)() ) (myScreen.*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; |