Язык программирования C++. Вводный курс. С для начинающих
Скачать 5.41 Mb.
|
} Запрещено объявлять константную функцию-член, которая модифицирует члены класса. Например, в следующем упрощенном определении: }; определение функции-члена ok() корректно, так как она не изменяет значения _cursor. В определении же error() значение _cursor изменяется, поэтому такая функция-член не может быть объявлена константной и компилятор выдает сообщение об ошибке: error: cannot modify a data member within a const member function ошибка: не могу модифицировать данные-члены внутри константной функции-члена Если класс будет интенсивно использоваться, лучше объявить его функции-члены, не модифицирующие данных, константными. Однако наличие спецификатора 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; // ... С++ для начинающих 603 } Модифицировать _text нельзя, но это объект типа char*, и символы, на которые он указывает, можно изменить внутри константной функции-члена класса Text. Функция- член bad() демонстрирует плохой стиль программирования. Константность функции- члена не гарантирует, что объекты внутри класса останутся неизменными после ее вызова, причем компилятор не поможет обнаружить такую ситуацию. Константную функцию-член можно перегружать неконстантной функцией с тем же списком параметров: }; В этом случае наличие спецификатора const у объекта класса определяет, какая из двух функций будет вызвана: } Хотя конструкторы и деструкторы не являются константными функциями-членами, они все же могут вызываться для константных объектов. Объект становится константным после того, как конструктор проинициализирует его, и перестает быть таковым, как только вызывается деструктор. Таким образом, объект со спецификатором const трактуется как константный с момента завершения работы конструктора и до вызова деструктора. #include }; void Text::bad( const string &parm ) const { _text = parm.c_str(); // ошибка: нельзя модифицировать _text for ( int ix = 0; ix < parm.size(); ++ix ) _text[ix] = parm[ix]; // плохой стиль, но не ошибка class Screen { public: char get(int x, int y); char get(int x, int y) const; // ... int main() { const Screen cs; Screen s; char ch = cs.get(0,0); // вызывает константную функцию-член ch = s.get(0,0); // вызывает неконстантную функцию-член С++ для начинающих 604 Функцию-член можно также объявить со спецификатором volatile (он был введен в разделе 3.13). Объект класса объявляется как volatile, если его значение изменяется способом, который не обнаруживается компилятором (например, если это структура данных, представляющая порт ввода/вывода). Для таких объектов вызываются только функции-члены с тем же спецификатором, конструкторы и деструкторы: char Screen::poll() volatile { ... } 13.3.6. Объявление mutable При объявлении объекта класса Screen константным возникают некоторые проблемы. Предполагается, что после инициализации объекта Screen, его содержимое уже нельзя изменять. Но это не должно мешать нам читать содержимое экрана. Рассмотрим следующий константный объект класса Screen: const Screen cs ( 5, 5 ); Если мы хотим прочитать символ, находящийся в позиции (3,4), то попробуем сделать так: char ch = cs.get(); Но такая конструкция не работает: move() – это не константная функция-член, и сделать ее таковой непросто. Определение move() выглядит следующим образом: } Обратите внимание, что move()изменяет член класса _cursor, следовательно, не может быть объявлена константной. Но почему нельзя модифицировать _cursor для константного объекта класса Screen? Ведь _cursor – это просто индекс. Изменяя его, мы не модифицируем содержимое class Screen { public: char poll() volatile; // ... }; // прочитать содержимое экрана в позиции (3,4) // Увы! Это не работает cs.move( 3, 4 ); inline void Screen::move( int r, int c ) { if ( checkRange( r, c ) ) { int row = (r-1) * _width; _cursor = row + c - 1; // модифицирует _cursor } С++ для начинающих 605 экрана, а лишь пытаемся установить позицию внутри него. Модификация _cursor должна быть разрешена несмотря на то, что у класса Screen есть спецификатор const. Чтобы разрешить модификацию члена класса, принадлежащего константному объекту, объявим его изменчивым (mutable). Член с таким спецификатором не бывает константным, даже если он член константного объекта. Его можно обновлять, в том числе функцией-членом со спецификатором const. Объявлению изменчивого члена класса должно предшествовать ключевое слово mutable: }; Теперь любая константная функция способна модифицировать _cursor, и move() может быть объявлена константной. Хотя move() изменяет данный член, компилятор не считает это ошибкой. } Показанные в начале этого подраздела операции позиционирования внутри экрана теперь можно выполнить без сообщения об ошибке. Отметим, что изменчивым объявлен только член _cursor, тогда как _screen, _height и _width не имеют спецификатора mutable, поскольку их значения в константном объекте класса Screen изменять нельзя. Упражнение 13.3 Объясните, как будет вести себя copy() при следующих вызовах: myScreen.copy( myScreen ); Упражнение 13.4 class Screen { public: // функции-члены private: string _screen; mutable string::size_type _cursor; // изменчивый член short _height; short _width; // move() - константная функция-член inline void Screen::move( int r, int c ) const { // ... // правильно: константная функция-член может модифицировать члены // со спецификатором mutable _cursor = row + c - 1; // ... Screen myScreen; С++ для начинающих 606 К дополнительным перемещениям курсора можно отнести его передвижение вперед и назад на один символ. Из правого нижнего угла экрана курсор должен попасть в левый верхний угол. Реализуйте функции forward() и backward(). Упражнение 13.5 Еще одной полезной возможностью является перемещение курсора вниз и вверх на одну строку. По достижении верхней или нижней строки экрана курсор не перепрыгивает на противоположный край; вместо этого подается звуковой сигнал, и курсор остается на месте. Реализуйте функции up() и down(). Для подачи сигнала следует вывести на стандартный вывод cout символ с кодом '007'. Упражнение 13.6 Пересмотрите описанные функции-члены класса Screen и объявите те, которые сочтете нужными, константными. Объясните свое решение. 13.4. Неявный указатель this У каждого объекта класса есть собственная копия данных-членов. Например: } У объекта myScreen есть свои члены _width, _height, _cursor и _screen, а у объекта bufScreen – свои. Однако каждая функция-член класса существует в единственном экземпляре. Их и вызывают myScreen и bufScreen. В предыдущем разделе мы видели, что функция-член может обращаться к членам своего класса, не используя операторы доступа. Так, определение функции move() выглядит следующим образом: } Если функция move() вызывается для объекта myScreen, то члены _width и _height, к которым внутри нее имеются обращения, – это члены объекта myScreen. Если же она вызывается для объекта bufScreen, то и обращения производятся к членам данного int main() { Screen myScreen( 3, 3 ), bufScreen; myScreen.clear(); myScreen.move( 2, 2 ); myScreen.set( '*' ); myScreen.display(); bufScreen.resize( 5, 5 ); bufScreen.display(); inline void Screen::move( int r, int c ) { if ( checkRange( r, c ) ) // позиция на экране задана корректно? { int row = (r-1) * _width; // смещение строки _cursor = row + c - 1; } С++ для начинающих 607 объекта. Каким же образом _cursor, которым манипулирует move(), оказывается членом то myScreen, то bufScreen? Дело в указателе this. Каждой функции-члену передается указатель на объект, для которого она вызвана, – this . В неконстантной функции-члене это указатель на тип класса, в константной – константный указатель на тот же тип, а в функции со спецификатором volatile указатель с тем же спецификатором. Например, внутри функции-члена move() класса Screen указатель this имеет тип Screen*, а в неконстантной функции-члене List – тип List* Поскольку this адресует объект, для которого вызвана функция-член, то при вызове move() для myScreen он указывает на объект myScreen, а при вызове для bufScreen – на объект bufScreen. Таким образом, член _cursor, с которым работает функция move() , в первом случае принадлежит объекту myScreen, а во втором – bufScreen. Понять все это можно, если представить себе, как компилятор реализует объект this. Для его поддержки необходимо две трансформации: 1. Изменить определение функции-члена класса, добавив дополнительный параметр: } В этом определении использование указателя this для доступа к членам _width и _cursor сделано явным. 2. Изменение каждого вызова функции-члена класса с целью передачи одного дополнительного аргумента – адреса объекта, для которого она вызвана: myScreen.move( 2, 2 ); транслируется в move( &myScreen, 2, 2 ); Программист может явно обращаться к указателю this внутри функции. Так, вполне корректно, хотя и излишне, определить функцию-член home() следующим образом: } // псевдокод, показывающий, как происходит расширение // определения функции-члена // ЭТО НЕ КОРРЕКТНЫЙ КОД 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; } inline void Screen::home() { this->_cursor = 0; С++ для начинающих 608 Однако бывают случаи, когда без такого обращения не обойтись, как мы видели на примере функции-члена copy() класса Screen. В следующем подразделе мы рассмотрим и другие примеры. 13.4.1. Когда использовать указатель this Наша функция main() вызывает функции-члены класса Screen для объектов myScreen и bufScreen таким образом, что каждое действие – это отдельная инструкция. У нас есть возможность определить функции-члены так, чтобы конкатенировать их вызовы при обращении к одному и тому же объекту. Например, все вызовы внутри main() будут выглядеть так: } Именно так интуитивно представляется последовательность операций с экраном: очистить экран myScreen, переместить курсор в позицию (2,2), записать в эту позицию символ '*' и вывести результат. Операторы доступа “точка” и “стрелка” левоассоциативны, т.е. их последовательность выполняется слева направо. Например, сначала вызывается myScreen.clear(), затем myScreen.move() и т.д. Чтобы myScreen.move() можно было вызвать после myScreen.clear() , функция clear() должна возвращать объект myScreen, для которого она была вызвана. Мы уже видели, что доступ к объекту внутри функции-члена класса производится в помощью указателя this. Вот реализация clear(): } Обратите внимание, что возвращаемый тип этой функции-члена – Screen& – ссылка на объект ее же класса. Чтобы конкатенировать вызовы, необходимо также пересмотреть реализацию move() и set(). Возвращаемый тип следует изменить с void на Screen&, а в определении возвращать *this. Аналогично функцию-член display() можно написать так: int main() { // ... myScreen.clear().move( 2, 2 ), set( '*' ). display(); bufScreen.reSize( 5, 5 ).display(); // объявление clear() находится в теле класса // в нем задан аргумент по умолчанию bkground = '#' Screen& Screen::clear( char bkground ) { // установить курсор в левый верхний угол и очистить экран _cursor = 0; _screen.assign( // записать в строку _screen.size(), // size() символов bkground // со значением bkground ); // вернуть объект, для которого была вызвана функция return *this; С++ для начинающих 609 } А вот реализация reSize(): } Работа указателя this не исчерпывается возвратом объекта, к которому была применена функция-член. При рассмотрении copy() в разделе 13.3 мы видели и другой способ его использования: 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() находится в теле класса // в нем задан аргумент по умолчанию 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; С++ для начинающих 610 } Указатель this хранит адрес объекта, для которого была вызвана функция-член. Если адрес, на который ссылается sobj, совпадает со значением this, то sobj и this относятся к одному и тому же объекту, так что операция копирования не нужна. (Мы еще встретимся с этой конструкцией, когда будем рассматривать копирующий оператор присваивания в разделе 14.7.) Упражнение 13.7 Указатель this можно использовать для модификации адресуемого объекта, а также для его замены другим объектом того же типа. Например, функция-член assign() класса classType выглядит так. Можете ли вы объяснить, что она делает? } Напомним, что classType – это имя деструктора. Оператор new выглядит несколько причудливо, но мы уже встречались с подобным в разделе 8.4. Как вы относитесь к такому стилю программирования? Безопасна ли эта операция? Почему? 13.5. Статические члены класса Иногда нужно, чтобы все объекты некоторого класса имели доступ к единственному глобальному объекту. Допустим, необходимо подсчитать, сколько их было создано; глобальным может быть указатель на процедуру обработки ошибок для класса или, скажем, указатель на свободную память для его объектов. В подобных случаях более эффективно иметь один глобальный объект, используемый всеми объектами класса, чем отдельные члены в каждом объекте. Хотя такой объект является глобальным, он существует лишь для поддержки реализации абстракции класса. В этой ситуации приемлемым решением является статический член класса, который ведет себя как глобальный объект, принадлежащий своему классу. В отличие от других членов, которые присутствуют в каждом объекте как отдельные элементы данных, статический член существует в единственном экземпляре и связан с самим типом, а не с конкретным его объектом. Это разделяемая сущность, доступная всем объектам одного класса. void Screen::copy( const Screen& sobj ) { // если этот объект Screen и sobj - одно и то же, // копирование излишне if ( this != sobj ) { // скопировать значение sobj в this } classType& classType::assign( const classType &source ) { if ( this != &source ) { this->classType(); new (this) classType( source ); } return *this; С++ для начинающих 611 По сравнению с глобальным объектом у статического члена есть следующие преимущества: • статический член не находится в глобальном пространстве имен программы, следовательно, уменьшается вероятность случайного конфликта имен с другими глобальными объектами; • остается возможность сокрытия информации, так как статический член может быть закрытым, а глобальный объект – никогда. Чтобы сделать член статическим, надо поместить в начале его объявления в теле класса ключевое слово static. К ним применимы все правила доступа к открытым, закрытым и защищенным членам. Например, для определенного ниже класса Account член _interestRate объявлен как закрытый и статический типа double: }; Почему _interestRate сделан статическим, а _amount и _owner нет? Потому что у всех счетов разные владельцы и суммы, но процентная ставка одинакова. Следовательно, объявление члена _interestRate статическим уменьшает объем памяти, необходимый для хранения объекта Account. Хотя текущее значение _interestRate для всех счетов одинаково, но со временем оно может изменяться. Поэтому мы решили не объявлять этот член как const. Достаточно модифицировать его лишь один раз, и с этого момента все объекты Account будут видеть новое значение. Если бы у каждого объекта была собственная копия, то пришлось бы обновить их все, что неэффективно и является потенциальным источником ошибок. В общем случае статический член инициализируется вне определения класса. Его имя во внешнем определении должно быть специфицировано именем класса. Вот так можно инициализировать _interestRate: double Account::_interestRate = 0.0589; В программе может быть только одно определение статического члена. Это означает, что инициализацию таких членов следует помещать не в заголовочные файлы, а туда, где находятся определения невстроенных функций-членов класса. В объявлении статического члена можно указать любой тип. Это могут быть константные объекты, массивы, объекты классов и т.д. Например: class Account { // расчетный счет Account( double amount, const string &owner ); string owner() { return _owner; } private: static double _interestRate; // процентная ставка double _amount; // сумма на счету string _owner; // владелец // явная инициализация статического члена класса #include "account.h" |