Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология
Образование Политология Производство Психология Стандартизация Технологии


Функции-члены со спецификаторами const и volatile



Любая попытка модифицировать константный объект из программы обычно помечается компилятором как ошибка. Например:

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 {... }

Объявление mutable

При объявлении объекта класса 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 и объявите те, которые сочтете нужными, константными. Объясните свое решение.

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

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

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

// ЭТО НЕ КОРРЕКТНЫЙ КОД 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 сделано явным.

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

myScreen.move( 2, 2 );

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

move( & myScreen, 2, 2 );

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

inline void Screen:: home()

{

this-> _cursor = 0;

}

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


Поделиться:



Последнее изменение этой страницы: 2019-04-09; Просмотров: 277; Нарушение авторского права страницы


lektsia.com 2007 - 2024 год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! (0.053 с.)
Главная | Случайная страница | Обратная связь