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

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

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

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

602

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 в объявлении функции-члена не предотвращает все возможные изменения. Такое объявление гарантирует лишь, что функции-члены не смогут изменять данные-члены, но если класс содержит указатели, то адресуемые ими объекты могут быть модифицированы константной функцией, не вызывая ошибки компиляции. Это часто приводит в недоумение начинающих программистов. Например:

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

603

#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

трактуется как константный с момента завершения работы конструктора и до вызова деструктора.

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

604

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

разделе 3.13). Объект класса объявляется как volatile, если его значение изменяется способом, который не обнаруживается компилятором (например, если это структура данных, представляющая порт ввода/вывода). Для таких объектов вызываются только

class Screen { public:

char poll() volatile; // ...

};

функции-члены с тем же спецификатором, конструкторы и деструкторы: char Screen::poll() volatile { ... }

13.3.6. Объявление mutable

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

const Screen cs ( 5, 5 );

Если мы хотим прочитать символ, находящийся в позиции (3,4), то попробуем сделать

//прочитать содержимое экрана в позиции (3,4)

//Увы! Это не работает

cs.move( 3, 4 );

так:

char ch = cs.get();

Но такая конструкция не работает: move() это не константная функция-член, и сделать

inline void Screen::move( int r, int c )

{

if ( checkRange( r, c ) )

{

int row

=

(r-1)

*

_width;

// модифицирует _cursor

_cursor

=

row +

c

- 1;

}

еетаковой непросто. Определение move() выглядит следующим образом:

}

Обратите внимание, что move()изменяет член класса _cursor, следовательно, не может быть объявлена константной.

Но почему нельзя модифицировать _cursor для константного объекта класса Screen? Ведь _cursor это просто индекс. Изменяя его, мы не модифицируем содержимое

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

605

экрана, а лишь пытаемся установить позицию внутри него. Модификация _cursor должна быть разрешена несмотря на то, что у класса Screen есть спецификатор const.

Чтобы разрешить модификацию члена класса, принадлежащего константному объекту, объявим его изменчивым (mutable). Член с таким спецификатором не бывает константным, даже если он член константного объекта. Его можно обновлять, в том числе функцией-членом со спецификатором const. Объявлению изменчивого члена

class Screen {

 

public:

 

// функции-члены

 

private:

_screen;

string

mutable string::size_type

_cursor; // изменчивый член

short

_height;

short

_width;

класса должно предшествовать ключевое слово mutable:

};

Теперь любая константная функция способна модифицировать _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

Screen myScreen;

Объясните, как будет вести себя copy() при следующих вызовах: myScreen.copy( myScreen );

Упражнение 13.4

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

606

К дополнительным перемещениям курсора можно отнести его передвижение вперед и назад на один символ. Из правого нижнего угла экрана курсор должен попасть в левый верхний угол. Реализуйте функции forward() и backward().

Упражнение 13.5

Еще одной полезной возможностью является перемещение курсора вниз и вверх на одну строку. По достижении верхней или нижней строки экрана курсор не перепрыгивает на противоположный край; вместо этого подается звуковой сигнал, и курсор остается на месте. Реализуйте функции up() и down(). Для подачи сигнала следует вывести на стандартный вывод cout символ с кодом '007'.

Упражнение 13.6

Пересмотрите описанные функции-члены класса Screen и объявите те, которые сочтете нужными, константными. Объясните свое решение.

13.4. Неявный указатель this

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, то и обращения производятся к членам данного

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

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. Для его поддержки необходимо две трансформации:

//псевдокод, показывающий, как происходит расширение

//определения функции-члена

//ЭТО НЕ КОРРЕКТНЫЙ КОД 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;

}

1.Изменить определение функции-члена класса, добавив дополнительный параметр:

}

Вэтом определении использование указателя this для доступа к членам _width и _cursor сделано явным.

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

myScreen.move( 2, 2 );

транслируется в

move( &myScreen, 2, 2 );

Программист может явно обращаться к указателю this внутри функции. Так, вполне

inline void Screen::home()

{

this->_cursor = 0;

корректно, хотя и излишне, определить функцию-член home() следующим образом:

}

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

608

Однако бывают случаи, когда без такого обращения не обойтись, как мы видели на примере функции-члена copy() класса Screen. В следующем подразделе мы рассмотрим и другие примеры.

13.4.1. Когда использовать указатель this

Наша функция 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, для которого она была вызвана. Мы уже видели, что доступ к объекту внутри функции-члена

//объявление clear() находится в теле класса

//в нем задан аргумент по умолчанию bkground = '#'

Screen& Screen::clear( char bkground )

{ // установить курсор в левый верхний угол и очистить экран

_cursor = 0;

// записать в строку

_screen.assign(

_screen.size(),

// size() символов

bkground

// со значением bkground

);

 

// вернуть объект, для которого была вызвана функция return *this;

класса производится в помощью указателя this. Вот реализация clear():

}

Обратите внимание, что возвращаемый тип этой функции-члена Screen& ссылка на объект ее же класса. Чтобы конкатенировать вызовы, необходимо также пересмотреть реализацию move() и set(). Возвращаемый тип следует изменить с void на Screen&, а в определении возвращать *this.

Аналогично функцию-член display() можно написать так:

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

609

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;

Авот реализация reSize():

}

Работа указателя this не исчерпывается возвратом объекта, к которому была применена функция-член. При рассмотрении copy() в разделе 13.3 мы видели и другой способ его использования:

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

610

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::assign( const classType &source )

{

if ( this != &source )

{

this->~classType();

new (this) classType( source );

}

return *this;

classType выглядит так. Можете ли вы объяснить, что она делает?

}

Напомним, что ~classType это имя деструктора. Оператор new выглядит несколько причудливо, но мы уже встречались с подобным в разделе 8.4.

Как вы относитесь к такому стилю программирования? Безопасна ли эта операция? Почему?

13.5. Статические члены класса

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

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

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

611

По сравнению с глобальным объектом у статического члена есть следующие преимущества:

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

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

Чтобы сделать член статическим, надо поместить в начале его объявления в теле класса ключевое слово static. К ним применимы все правила доступа к открытым, закрытым и защищенным членам. Например, для определенного ниже класса Account член

class Account { // расчетный счет Account( double amount, const string &owner ); string owner() { return _owner; }

private:

 

// процентная ставка

static double _interestRate;

double

_amount;

// сумма на счету

string

_owner;

// владелец

_interestRate объявлен как закрытый и статический типа double:

};

Почему _interestRate сделан статическим, а _amount и _owner нет? Потому что у всех счетов разные владельцы и суммы, но процентная ставка одинакова. Следовательно, объявление члена _interestRate статическим уменьшает объем памяти, необходимый для хранения объекта Account.

Хотя текущее значение _interestRate для всех счетов одинаково, но со временем оно может изменяться. Поэтому мы решили не объявлять этот член как const. Достаточно модифицировать его лишь один раз, и с этого момента все объекты Account будут видеть новое значение. Если бы у каждого объекта была собственная копия, то пришлось бы обновить их все, что неэффективно и является потенциальным источником ошибок.

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

// явная инициализация статического члена класса

#include "account.h"

инициализировать _interestRate:

double Account::_interestRate = 0.0589;

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

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