<< Пред.           стр. 57 (из 121)           След. >>

Список литературы по разделу

 {
  move( r, c ); // устанавливаем _cursor
  return get(); // вызываем другую функцию-член get()
 }
 Так как встроенные функции-члены должны быть определены в каждом исходном файле, где они вызываются, то встроенную функцию, не определенную в теле класса, следует поместить в тот же заголовочный файл, в котором определен ее класс. Например, представленные ранее определения move() и get() должны находиться в заголовочном файле Screen.h после определения класса Screen.
 13.3.2. Доступ к членам класса
 Говорят, что определение функции-члена принадлежит области видимости класса независимо от того, находится ли оно вне или внутри его тела. Отсюда следуют два вывода:
 в определении функции-члена могут быть обращения к любым членам класса, открытым или закрытым, и это не нарушает ограничений доступа;
 когда функция-член обращается к членам класса, операторы доступа “точка” и “стрелка” не необходимы.
 Например:
 #include
 
 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 "Screen.h"
 
 int main()
 {
  Screen s1;
  // Установить s1
 
  Screen s2;
  s2.copy(s1);
 
  // ...
 }
 то параметр sobj внутри определения copy() соотносится с объектом s1 из функции main(). Функция-член copy() вызвана для объекта s2, стоящего перед оператором “точка”. Для такого вызова члены _screen, _height, _width и _cursor, при обращении к которым внутри определения этой функции нет оператора доступа, – это члены объекта s2. В следующем разделе мы рассмотрим доступ к членам класса внутри определения функции-члена более подробно и, в частности, покажем, как для поддержки такого доступа применяется указатель this.
 13.3.3. Закрытые и открытые функции-члены
 Функцию-член можно объявить в любой из секций public, private или protected тела класса. Где именно это следует делать? Открытая функция-член задает операцию, которая может понадобиться пользователю. Множество открытых функций-членов составляет интерфейс класса. Например, функции-члены home(), move() и get() класса Screen определяют операции, с помощью которых программа манипулирует объектами этого типа.
 Поскольку мы прячем от пользователей внутреннее представление класса, объявляя его члены закрытыми, то для манипуляции объектами типа 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
 
 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
 
 13.3.4. Специальные функции-члены
 Существует специальная категория функций-членов, отвечающих за такие действия с объектами, как инициализация, присваивание, управление памятью, преобразование типов и уничтожение. Такие функции называются конструкторами. Они вызываются компилятором неявно каждый раз, когда объект класса определяется или создается оператором 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 обсуждаются конвертеры и функции управления памятью.)
 13.3.5. Функции-члены со спецификаторами 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
 
 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 { ... }
 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() – это не константная функция-член, и сделать ее таковой непросто. Определение 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 и объявите те, которые сочтете нужными, константными. Объясните свое решение.
 13.4. Неявный указатель this
 У каждого объекта класса есть собственная копия данных-членов. Например:
 int main() {
  Screen myScreen( 3, 3 ), bufScreen;
 
  myScreen.clear();

<< Пред.           стр. 57 (из 121)           След. >>

Список литературы по разделу