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

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

 13.1.1. Данные-члены
 Данные-члены класса объявляются так же, как переменные. Например, у класса Screen могут быть следующие данные-члены:
 #include
 class Screen {
  string _screen; // string( _height * _width )
  string::size_type _cursor; // текущее положение на экране
  short _height; // число строк
  short _width; // число колонок
 };
 Поскольку мы решили использовать строки для внутреннего представления объекта класса Screen, то член _screen имеет тип string. Член _cursor – это смещение в строке, он применяется для указания текущей позиции на экране. Для него использован переносимый тип string::size_type. (Тип size_type рассматривался в разделе 6.8.)
 Необязательно объявлять два члена типа short по отдельности. Вот объявление класса Screen, эквивалентное приведенному выше:
 class Screen {
 /*
  * _ screen адресует строку размером _height * _width
  * _cursor указывает текущую позицию на экране
  * _height и _width - соответственно число строк и колонок
  */
  string _screen;
  string::size_type _cursor;
  short _height, _width;
 };
 Член класса может иметь любой тип:
 class StackScreen {
  int topStack;
  void (*handler)(); // указатель на функцию
  vector stack; // вектор классов
 };
 Описанные данные-члены называются нестатическими. Класс может иметь также и статические данные-члены. (У них есть особые свойства, которые мы рассмотрим в разделе 13.5.)
 Объявления данных-членов очень похожи на объявления переменных в области видимости блока или пространства имен. Однако их, за исключением статических членов, нельзя явно инициализировать в теле класса:
 class First {
  int memi = 0; // ошибка
  double memd = 0.0; // ошибка
 };
 Данные-члены класса инициализируются с помощью конструктора класса. (Мы рассказывали о конструкторах в разделе 2.3; более подробно они рассматриваются в главе 14.)
 13.1.2. Функции-члены
 Пользователям, по-видимому, понадобится широкий набор операций над объектами типа Screen: возможность перемещать курсор, проверять и устанавливать области экрана и рассчитывать его реальные размеры во время выполнения, а также копировать один объект в другой. Все эти операции можно реализовать с помощью функций-членов.
 Функции-члены класса объявляются в его теле. Это объявление выглядит точно так же, как объявление функции в области видимости пространства имен. (Напомним, что глобальная область видимости – это тоже область видимости пространства имен. Глобальные функции рассматривались в разделе 8.2, а пространства имен – в разделе 8.5.) Например:
 class Screen {
 public:
  void home();
  void move( int, int );
  char get();
  char get( int, int );
  void checkRange( int, int );
  // ...
 };
 Определение функции-члена также можно поместить внутрь тела класса:
 class Screen {
 public:
  // определения функций home() и get()
  void home() { _cursor = 0; }
  char get() { return _screen[_cursor]; }
  // ...
 };
 home() перемещает курсор в левый верхний угол экрана; get() возвращает символ, находящийся в текущей позиции курсора.
 Функции-члены отличаются от обычных функций следующим:
 функция-член объявлена в области видимости своего класса, следовательно, ее имя не видно за пределами этой области. К функции-члену можно обратиться с помощью одного из операторов доступа к членам – точки (.) или стрелки (->):
 ptrScreen->home();
 myScreen.home();
 (в разделе 13.9 область видимости класса обсуждается более детально);
 функции-члены имеют право доступа как к открытым, так и к закрытым членам класса, тогда как обычным функциям доступны лишь открытые. Конечно, функции-члены одного класса, как правило, не имеют доступа к данным-членам другого класса.
 Функция-член может быть перегруженной (перегруженные функции рассматриваются в главе 9). Однако она способна перегружать лишь другую функцию-член своего класса. По отношению к функциям, объявленным в других классах или пространствах имен, функция-член находится в отдельной области видимости и, следовательно, не может перегружать их. Например, объявление get(int, int) перегружает лишь get() из того же класса Screen:
 class Screen {
 public:
  // объявления перегруженных функций-членов get()
  char get() { return _screen[_cursor]; }
  char get( int, int );
  // ...
 };
 (Подробнее мы остановимся на функциях-членах класса в разделе 13.3.)
 13.1.3. Доступ к членам
 Часто бывает так, что внутреннее представление типа класса изменяется в последующих версиях программы. Допустим, опрос пользователей нашего класса Screen показал, что для его объектов всегда задается размер экрана 80 ? 24. В таком случае было бы желательно заменить внутреннее представление экрана менее гибким, но более эффективным:
 class Screen {
 public:
  // функции-члены
 private:
  // инициализация статических членов (см. 13.5)
  static const int _height = 24;
  static const int _width = 80;
  string _screen;
  string::size_type _cursor;
 };
 Прежняя реализация функций-членов (то, как они манипулируют данными-членами класса) больше не годится, ее нужно переписать. Но это не означает, что должен измениться и интерфейс функций-членов (список формальных параметров и тип возвращаемого значения).
 Если бы данные-члены класса Screen были открыты и доступны любой функции внутри программы, как отразилось бы на пользователях изменение внутреннего представления этого класса?
 все функции, которые напрямую обращались к данным-членам старого представления, перестали бы работать. Следовательно, пришлось бы отыскивать и изменять соответствующие части кода;
 так как интерфейс не изменился, то коды, манипулировавшие объектами класса Screen только через функции-члены, не пришлось бы модифицировать. Но поскольку сами функции-члены все же изменились, программу пришлось бы откомпилировать заново.
 Сокрытие информации – это формальный механизм, предотвращающий прямой доступ к внутреннему представлению типа класса из функций программы. Ограничение доступа к членам задается с помощью секций тела класса, помеченных ключевыми словами public, private и protected – спецификаторами доступа. Члены, объявленные в секции public, называются открытыми, а объявленные в секциях private и protected соответственно закрытыми или защищенными.
 открытый член доступен из любого места программы. Класс, скрывающий информацию, оставляет открытыми только функции-члены, определяющие операции, с помощью которых внешняя программа может манипулировать его объектами;
 закрытый член доступен только функциям-членам и друзьям класса. Класс, который хочет скрыть информацию, объявляет свои данные-члены закрытыми;
 защищенный член ведет себя как открытый по отношению к производному классу и как закрытый по отношению к остальной части программы. (В главе 2 мы видели пример использования защищенных членов в классе IntArray. Детально они рассматриваются в главе 17, где вводится понятие наследования.)
 В следующем определении класса Screen указаны секции public и private:
 class Screen {
 public:
  void home() { _cursor = 0; }
  char get() { return _screen[_cursor]; }
  char get( int, int );
  void move( int, int );
  // ...
 private:
  string _screen;
  string::size_type _cursor;
  short _height, _width;
 };
 Согласно принятому соглашению, сначала объявляются открытые члены класса. (Обсуждение того, почему в старых программах C++ сначала шли закрытые члены и почему этот стиль еще кое-где сохранился, см. в книге [LIPPMAN96a].) В теле класса может быть несколько секций public, protected и private. Каждая секция продолжается либо до метки следующей секции, либо до закрывающей фигурной скобки. Если спецификатор доступа не указан, то секция, непосредственно следующая за открывающей скобкой, по умолчанию считается private.
 13.1.4. Друзья
 Иногда удобно разрешить некоторым функциям доступ к закрытым членам класса. Механизм друзей позволяет классу разрешать доступ к своим неоткрытым членам.
 Объявление друга начинается с ключевого слова friend и может встречаться только внутри определения класса. Так как друзья не являются членами класса, то не имеет значения, в какой секции они объявлены. В примере ниже мы сгруппировали все подобные объявления сразу после заголовка класса:
 class Screen {
  friend istream&
  operator>>( istream&, Screen& );
  friend ostream&
  operator<<( ostream&, const Screen& );
 public:
  // ... оставшаяся часть класса Screen
 };
 Операторы ввода и вывода теперь могут напрямую обращаться к закрытым членам класса Screen. Простая реализация оператора вывода выглядит следующим образом:
 #include
 ostream& operator<<( ostream& os, const Screen& s )
 {
  // правильно: можно обращаться к _height, _width и _screen
  os << "<" << s._height
  << "," << s._width << ">";
  os << s._screen;
 
  return os;
 }
 Другом может быть функция из пространства имен, функция-член другого класса или даже целый класс. В последнем случае всем его функциям-членам предоставляется доступ к неоткрытым членам класса, объявляющего дружественные отношения. (В разделе 15.2 друзья обсуждаются более подробно.)
 13.1.5. Объявление и определение класса
 О классе говорят, что он определен, как только встретилась скобка, закрывающая его тело. После этого становятся известными все члены класса, а следовательно, и его размер.
 Можно объявить класс, не определяя его. Например:
 class Screen; // объявление класса Screen
 Это объявление вводит в программу имя Screen и указывает, что оно относится к типу класса.
 Тип объявленного, но еще не определенного класса допустимо использовать весьма ограниченно. Нельзя определять объект типа класса, если сам класс еще не определен, поскольку размер класса в этом момент неизвестен и компилятор не знает, сколько памяти отвести под объект.
 Однако указатель или ссылку на объект такого класса объявлять можно, так как они имеют фиксированный размер, не зависящий от типа. Но, поскольку размеры класса и его членов неизвестны, применять оператор разыменования (*) к такому указателю, а также использовать указатель или ссылку для обращения к члену не разрешается, пока класс не будет полностью определен.
 Член некоторого класса можно объявить принадлежащим к типу какого-либо класса только тогда, когда компилятор уже видел определение этого класса. До этого объявляются лишь члены, являющиеся указателями или ссылками на такой тип. Ниже приведено определение StackScreen, один из членов которого служит указателем на Screen, который объявлен, но еще не определен:
 class Screen; // объявление
 class StackScreen {
  int topStack;
  // правильно: указатель на объект Screen
  Screen *stack;
  void (*handler)();
 };
 Поскольку класс не считается определенным, пока не закончилось его тело, то в нем не может быть данных-членов его собственного типа. Однако класс считается объявленным, как только распознан его заголовок, поэтому в нем допустимы члены, являющиеся ссылками или указателями на его тип. Например:
 class LinkScreen {
  Screen window;
  LinkScreen *next;
  LinkScreen *prev;
 };
 Упражнение 13.1
 Пусть дан класс Person со следующими двумя членами:
 string _name;
 string _address;
 и такие функции-члены:
 Person( const string &n, const string &s )
  : _name( n ), _address( a ) { }
 string name() { return _name; }
 string address() { return _address; }
 Какие члены вы объявили бы в секции public, а какие – в секции private? Поясните свой выбор.
 Упражнение 13.2
 Объясните разницу между объявлением и определением класса. Когда вы стали бы использовать объявление класса? А определение?
 13.2. Объекты классов
 Определение класса, например Screen, не приводит к выделению памяти. Память выделяется только тогда, когда определяется объект типа класса. Так, если имеется следующая реализация Screen:
 class Screen {
 public:
  // функции-члены
 private:
  string _screen;
  string:size_type _cursor;
  short _height;
  short _width;
 };
 то определение
 Screen myScreen;
 выделяет область памяти, достаточную для хранения четырех членов Screen. Имя myScreen относится к этой области. У каждого объекта класса есть собственная копия данных-членов. Изменение членов myScreen не отражается на значениях членов любого другого объекта типа Screen.
 Область видимости объекта класса зависит от его положения в тексте программы. Он определяется в иной области, нежели сам тип класса:
 class Screen {
  // список членов
 };
 
 int main()
 {
  Screen mainScreen;
 }
 Тип Screen объявлен в глобальной области видимости, тогда как объект mainScreen – в локальной области функции main().
 Объект класса также имеет время жизни. В зависимости от того, где (в области видимости пространства имен или в локальной области) и как (статическим или нестатическим) он объявлен, он может существовать в течение всего времени выполнения программы или только во время вызова некоторой функции. Область видимости объекта класса и его время жизни ведут себя очень похоже. (Понятия области видимости и времени жизни введены в главе 8.)
 Объекты одного и того же класса можно инициализировать и присваивать друг другу. По умолчанию копирование объекта класса эквивалентно копированию всех его членов. Например:
 Screen bufScreen = myScreen;
 // bufScreen._height = myScreen._height;
 // bufScreen._width = myScreen._width;
 // bufScreen._cursor = myScreen._cursor;
 // bufScreen._screen = myScreen._screen;
 Указатели и ссылки на объекты класса также можно объявлять. Указатель на тип класса разрешается инициализировать адресом объекта того же класса или присвоить ему такой адрес. Аналогично ссылка инициализируется l-значением объекта того же класса. (В объектно-ориентированном программировании указатель или ссылка на объект базового класса могут относиться и к объекту производного от него класса.)
 int main()
 {
  Screen myScreen, bufScreen[10];
  Screen *ptr = new Screen;
  myScreen = *ptr;
  delete ptr;
  ptr = bufScreen;
  Screen &ref = *ptr;
  Screen &ref2 = bufScreen[6];
 }
 По умолчанию объект класса передается по значению, если он выступает в роли аргумента функции или ее возвращаемого значения. Можно объявить формальный параметр функции или возвращаемое ею значение как указатель или ссылку на тип класса. (В разделе 7.3 были представлены параметры, являющиеся указателями или ссылками на типы классов, и объяснялось, когда их следует использовать. В разделе 7.4 с этой точки зрения рассматривались типы возвращаемых значений.)
 Для доступа к данным или функциям-членам объекта класса следует пользоваться соответствующими операторами. Оператор “точка” (.) применяется, когда операндом является сам объект или ссылка на него; а “стрелка” (->) – когда операндом служит указатель на объект:
 #include "Screen.h"
 
 bool isEqual( Screen& s1, Screen *s2 )
 { // возвращает false, если объекты не равны, и true - если равны
 
  if (s1.height() != s2->height() ||
  s2.width() != s2->width() )
  return false;
 
  for ( int ix = 0; ix < s1.height(); ++ix )
  for ( int jy = 0; jy < s2->width(); ++jy )
  if ( s1.get( ix, jy ) != s2->get( ix, jy ) )
  return false;
 
  return true; // попали сюда? значит, объекты равны
 }
 isEqual() – это не являющаяся членом функция, которая сравнивает два объекта Screen. У нее нет права доступа к закрытым членам Screen, поэтому напрямую обращаться к ним она не может. Сравнение проводится с помощью открытых функций-членов данного класса.
 Для получения высоты и ширины экрана isEqual() должна пользоваться функциями-членами height() и width() для чтения закрытых членов класса. Их реализация тривиальна:
 class Screen {
 public:
  int height() { return _height; }
  int width() { return _width; }
  // ...
 private:
  short _heigh, _width;
  // ...
 };
 Применение оператора доступа к указателю на объект класса эквивалентно последовательному выполнению двух операций: применению оператора разыменования (*) к указателю, чтобы получить адресуемый объект, и последующему применению оператора “точка” для доступа к нужному члену класса. Например, выражение
 s2->height()
 можно переписать так:
 (*s2).height()
 Результат будет одним и тем же.
 13.3. Функции-члены класса
 Функции-члены реализуют набор операций, применимых к объектам класса. Например, для Screen такой набор состоит из следующих объявленных в нем функций-членов:
 class Screen {
 public:
  void home() { _cursor = 0; }
  char get() { return _screen[_cursor]; }
  char get( int, int );
  void move( int, int );
  bool checkRange( int, int );
  int height() { return _height; }
  int width() { return _width; }
  // ...
 };
 Хотя у любого объекта класса есть собственная копия всех данных-членов, каждая функция-член существует в единственном экземпляре:
 Screen myScreen, groupScreen;
 myScreen.home();
 groupScreen.home();
 При вызове функции home() для объекта myScreen происходит обращение к его члену _cursor. Когда же эта функция вызывается для объекта groupScreen, то она обращается к члену _cursor именно этого объекта, причем сама функция home() одна и та же. Как же может одна функция-член обращаться к данным-членам разных объектов? Для этого применяется указатель this, рассматриваемый в следующем разделе.
 13.3.1. Когда использовать встроенные функции-члены
 Обратите внимание, что определения функций home(), get(), height() и width() приведены прямо в теле класса. Такие функции называются встроенными. (Мы говорили об этом в разделе 7.6.)
 Функции-члены можно объявить в теле класса встроенными и явно, поместив перед типом возвращаемого значения ключевое слово inline:
 class Screen {
 public:
  // использование ключевого слова inline
  // для объявления встроенных функций-членов
  inline void home() { _cursor = 0; }
  inline char get() { return _screen[_cursor]; }
  // ...
 };
 Определения home() и get() в приведенных примерах эквивалентны. Поскольку ключевое слово inline избыточно, мы в этой книге не пишем его явно для функций-членов, определенных в теле класса.
 Функции-члены, состоящие из двух или более строк, лучше определять вне тела. Для идентификации функции как члена некоторого класса требуется специальный синтаксис объявления: имя функции должно быть квалифицировано именем ее класса. Вот как выглядит определение функции checkRange(), квалифицированное именем Screen:
 #include
 #include "screen.h"
 
 // имя функции-члена квалифицировано именем Screen::
 bool Screen::checkRange( int row, int col )
 { // проверить корректность координат
  if ( row < 1 || row > _height ||
  col < 1 || col > _width ) {
  cerr << "Screen coordinates ( "
  << row << ", " << col
  << " ) out of bounds.\n";
  return false;
  }
  return true;
 }
 Прежде чем определять функцию-член вне тела класса, необходимо объявить ее внутри тела, обеспечив ее видимость. Например, если бы перед определением функции checkRange() не был включен заголовочный файл Screen.h, то компилятор выдал бы сообщение об ошибке. Тело класса определяет полный список его членов. Этот список не может быть расширен после закрытия тела.
 Обычно функции-члены, определенные вне тела класса, не делают встроенными. Но объявить такую функцию встроенной можно, если явно добавить слово inline в объявление функции внутри тела класса или в ее определение вне тела, либо сделав то и другое одновременно. В следующем примере move() определена как встроенная функция-член класса Screen:
 inline void Screen::move( int r, int c )
 { // переместить курсор в абсолютную позицию
  if ( checkRange( r, c ) ) // позиция на экране задана корректно?
  {
  int row = (r-1) * _width; // смещение начала строки
  _cursor = row + c - 1;
  }
 }
 Функция get(int, int) объявляется встроенной с помощью слова inline:
 class Screen {
 public:
  inline char get( int, int );
  // объявления других функций-членов не изменяются
 };
 Определение функции следует после объявления класса. При этом слово inline можно опустить:
 char Screen::get( int r, int c )

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

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