НА ГЛАВНУЮ
Меню сайта
Категория
Ghost++ [1]
С++ [55]
Развлечение
ON - LINE
Опрос
Часто ли вы сталкиваетесь в игре с Noob'ami?
Всего ответов: 269
Оbserver Ward

Онлайн всего: 1
Гостей: 1
Пользователей: 0


Друзья сайта
Заведи себе Бота
Hаша кнопка
Для обмена банерами , наша кнопка для размещения у вас на сайте

Клансайт USSR


Главная » Статьи » Программирование » С++

13. Классы (2)
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, для которого она была вызвана. Мы уже видели, что доступ к объекту внутри функции-члена класса производится в помощью указателя this. Вот реализация clear():

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

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

Screen& Screen::clear( char bkground )
{ // установить курсор в левый верхний угол и очистить экран

   _cursor = 0;
   _screen.assign(       // записать в строку
      _screen.size(),    // size() символов
      bkground           // со значением bkground
   );

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

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

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

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():

// объявление 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;
}

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

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& classType::assign( const classType &source )
{
   if ( this != &source )
   {
      this->~classType();
      new (this) classType( source );
   }
   return *this;
}

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

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

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

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

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

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

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

class Account {                  // расчетный счет
   Account( double amount, const string &owner );
   string owner() { return _owner; }
private:
   static double _interestRate;  // процентная ставка
   double        _amount;        // сумма на счету
   string        _owner;         // владелец
};

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

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

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

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

#include "account.h"
double Account::_interestRate = 0.0589;

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

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

#include <string>
class Account {
   // ...
private:
   static const string name;
};
const string Account::name( "Savings Account" );

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

// заголовочный файл
class Account {
   //...
private:
   static const int nameSize = 16;
   static const string name[nameSize];
};

// исходный файл

const string Account::nameSize;   // необходимо определение члена
const string Account::name[nameSize] = "Savings Account";

Отметим, что константный статический член целого типа, инициализированный константой, – это константное выражение. Проектировщик может объявить такой статический член, если внутри тела класса возникает необходимость в именованной константе. Например, поскольку константный статический член nameSize является константным выражением, проектировщик использует его для задания размера члена-массива с именем name.

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

Так как name – это массив (и не целого типа), его нельзя инициализировать в теле класса. Попытка поступить таким образом приведет к ошибке компиляции:

class Account {
   //...
private:
   static const int nameSize = 16;   // правильно: целый тип
   static const string name[nameSize] = "Savings Account";  // ошибка
};

Член name должен быть инициализирован вне определения класса.

Обратите внимание, что член nameSize задает размер массива name в определении, находящемся вне тела класса:

const string Account::name[nameSize] = "Savings Account";

nameSize не квалифицирован именем класса Account. И хотя это закрытый член, определение name не приводит к ошибке. Как такое может быть? Определение статического члена аналогично определению функции-члена класса, которое может ссылаться на закрытые члены. Определение статического члена name находится в области видимости класса и может ссылаться на закрытые члены, после того как распознано квалифицированное имя Account::name. (Подробнее об области видимости класса мы поговорим в разделе 13.9.)

Статический член класса доступен функции-члену того же класса и без использования соответствующих операторов:

inline double Account::dailyReturn()
{
   return( _interestRate / 365 * _amount );
}

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

class Account {
   // ...
private:
   friend int compareRevenue( Account&, Account* );
   // остальное без изменения
};

// мы используем ссылочный и указательный параметры,

// чтобы проиллюстрировать оба оператора доступа

int compareRevenue( Account &ac1, Account *ac2 );
{
   double ret1, ret2;
   ret1 = ac1._interestRate * ac1._amount;
   ret2 = ac2->_interestRate * ac2->_amount;
   // ...
}

Как ac1._interestRate, так и ac2->_interestRate относятся к статическому члену Account::_interestRate.

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

// доступ к статическому члену с указанием квалифицированного имени

if ( Account::_interestRate < 0.05 )

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

Account::

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

int compareRevenue( Account &ac1, Account *ac2 );
{
   double ret1, ret2;
   ret1 = Account::_interestRate * ac1._amount;
   ret2 = Account::_interestRate * ac2->_amount;
   // ...
}

Уникальная особенность статического члена – то, что он существует независимо от объектов класса, – позволяет использовать его такими способами, которые для нестатических членов недопустимы.

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

      class Bar {
      public:
         // ...
      private:
         static Bar mem1;   // правильно
         Bar *mem2;         // правильно
         Bar mem3;          // ошибка
      };

    * статический член может выступать в роли аргумента по умолчанию для функции-члена класса, а для нестатического это запрещено:

extern int var;

class Foo {
private:
   int var;
   static int stcvar;
public:
   // ошибка: трактуется как Foo::var,
   // но ассоциированного объекта класса не существует
   int mem1( int = var );

   // правильно: трактуется как static Foo::stcvar,
   // ассоциированный объект и не нужен
   int mem2( int = stcvar );
   // правильно: трактуется как глобальная переменная var
   int mem3( int = :: var );
};

13.5.1. Статические функции-члены

Функции-члены raiseInterest() и interest() обращаются к глобальному статическому члену _interestRate:

class Account {
public:
   void raiseInterest( double incr );
   double interest() { return _interestRate; }
private:
   static double  _interestRate;
};

inline void Account::raiseInterest( double incr )
{
  _interestRate += incr;
}

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

Поэтому лучше объявить такие функции-члены как статические. Это можно сделать следующим образом:

class Account {
public:
   static void raiseInterest( double incr );
   static double interest() { return _interestRate; }
private:
   static double  _interestRate;
};

inline void Account::raiseInterest( double incr )
{
  _interestRate += incr;
}

Объявление статической функции-члена почти такое же, как и нестатической: в теле класса ему предшествует ключевое слово static, а спецификаторы const или volatile запрещены. В ее определении, находящемся вне тела класса, слова static быть не должно.

Такой функции-члену указатель this не передается, поэтому явное или неявное обращение к нему внутри ее тела вызывает ошибку компиляции. В частности, попытка обращения к нестатическому члену класса неявно требует наличия указателя this и, следовательно, запрещена. Например, представленную ранее функцию-член dailyReturn() нельзя объявить статической, поскольку она обращается к нестатическому члену _amount.

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

#include <iostream >
#include "account.h"

bool limitTest( double limit )
{
   // пока еще ни одного объекта класса Account не объявлено
   // правильно: вызов статической функции-члена
   return limit <= Account::interest() ;
}

int main() {
   double limit = 0.05;

   if ( limitTest( limit ) )
   {
      // указатель на статическую функцию-член
      // объявлен как обычный указатель
      void (*psf)(double) = &Account::raiseInterest;
      psf( 0.0025 );
   }

   Account ac1( 5000, "Asterix" );
   Account ac2( 10000, "Obelix" );
   if ( compareRevenue( ac1, &ac2 ) > 0 )
      cout << ac1.owner()
           << "is richer than "
           << ac2.owner() << "\n";
   else
      cout << ac1.owner()
           << " is poorer than"
          <<ac2.owner() << "\n";
   return 0;
}

Упражнение 13.8

Пусть дан класс Y с двумя статическими данными-членами и двумя статическими функциями-членами:

class X {
public:
   X( int i ) { _val = i; }
   int val() { return _val; }
private:
   int _val;
};

class Y {
public:
   Y( int i );
   static X xval();
   static int callsXval();
private:
   static X _xval;
   static int _callsXval;
};

Инициализируйте _xval значением 20, а _callsXval значением 0.

Упражнение 13.9

Используя классы из упражнения 13.8, реализуйте обе статические функции-члена для класса Y. callsXval() должна подсчитывать, сколько раз вызывалась xval().

Упражнение 13.10

Какие из следующих объявлений и определений статических членов ошибочны? Почему?

// example.h
class Example {
public:
   static double rate = 6.5;

   static const int vecSize = 20;
   static vector<double> vec(vecSize);
};

// example.c
#include "example.h "
double Example::rate;
vector<double> Example::vec;

13.6. Указатель на член класса

Предположим, что в нашем классе Screen определены четыре новых функции-члена: forward(), back(), up() и down(), которые перемещают курсор соответственно вправо, влево, вверх и вниз. Сначала мы должны объявить их в теле класса:

class Screen {
public:
   inline Screen& forward();
   inline Screen& back();
   inline Screen& end();
   inline Screen& up();
   inline Screen& down();
   // другие функции-члены не изменяются
private:
   inline int row();
   // другие функции-члены не изменяются
};

Функции-члены forward() и back() перемещают курсор на один символ. По достижении правого нижнего или левого верхнего угла экрана курсор переходит в противоположный угол.

inline Screen& Screen::forward()
{ // переместить _cursor вперед на одну экранную позицию

   ++_cursor;

   // если достигли конца экрана, перепрыгнуть в противоположный угол
   if ( _cursor == _screen.size() )
      home();

   return *this;
}

inline Screen& Screen::back()
{ // переместить _cursor назад на одну экранную позицию

   // если достигли начала экрана, перепрыгнуть в противоположный угол
   if ( _cursor == 0 )
      end();
   else
      --_cursor;

   return *this;
}

end() перемещает курсор в правый нижний угол экрана и является парной по отношению к функции-члену home():

inline Screen& Screen::end()
{
   _cursor = _width * _height - 1;
   return *this;
}

Функции up() и down() перемещают курсор вверх и вниз на одну строку. По достижении верхней или нижней строки курсор остается на месте и подается звуковой сигнал:

const char BELL = '\007';

inline Screen& Screen::up()
{ // переместить _cursor на одну строку вверх
  // если уже наверху, остаться на месте и подать сигнал
  if ( row() == 1 ) // наверху?
     cout << BELL<< endl;
  else
     _cursor -= _width;

  return *this;
}

inline Screen& Screen::down()
{
   if ( row() == _height ) //внизу?
      cout << BELL << endl;
   else
      _cursor += _width;

   return *this;
}

row() – это закрытая функция-член, которая используется в функциях up() и down(), возвращая номер строки, где находится курсор:

inline int Screen::row()
{ // вернуть текущую строку
   return ( _cursor + _width ) / height;
}

Пользователи класса Screen попросили нас добавить функцию repeat(), которая повторяет указанное действие n раз. Ее реализация могла бы выглядеть так:

Screen &repeat( char op, int times )
{
   switch( op ) {
      case DOWN:    // n раз вызвать Screen::down()
         break;
      case DOWN:    // n раз вызвать Screen::up()
         break;
      // ...
   }
}

Такая реализация имеет ряд недостатков. В частности, предполагается, что функции-члены класса Screen останутся неизменными, поэтому при добавлении или удалении функции-члена repeat() необходимо модифицировать. Вторая проблема – размер функции. Поскольку приходится проверять все возможные функции-члены, то исходный текст становится громоздким и неоправданно сложным.

В более общей реализации параметр op заменяется параметром типа указателя на функцию-член класса Screen. Теперь repeat() не должна сама устанавливать, какую операцию следует выполнить, и всю инструкцию switch можно удалить. Определение и использование указателей на члены класса – тема последующих подразделов.
13.6.1. Тип члена класса

Указателю на функцию нельзя присвоить адрес функции-члена, даже если типы возвращаемых значений и списки параметров полностью совпадают. Например, переменная pfi – это указатель на функцию без параметров, которая возвращает значение типа int:

int (*pfi)();

Если имеются глобальные функции HeightIs() и WidthIs() вида:

int HeightIs();
int WidthIs();

то допустимо присваивание pfi адреса любой из этих переменных:

pfi = HeightIs;
pfi = WidthIs;

В классе Screen также определены две функции доступа, height() и width(), не имеющие параметров и возвращающие значение типа int:

inline int Screen::height() { return _height; }
inline int Screen::width() { return _width; }

Однако попытка присвоить их переменной pfi является нарушением типизации и влечет ошибку компиляции:

// неверное присваивание: нарушение типизации

pfi = &Screen::height;

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

Несоответствие типов между двумя указателями – на функцию-член и на обычную функцию – обусловлено их разницей в представлении. В указателе на обычную функцию хранится ее адрес, который можно использовать для непосредственного вызова. (Указатели на функции рассматривались в разделе 7.9.) Указатель же на функцию-член должен быть сначала привязан к объекту или указателю на объект, чтобы получить this, и только после этого он применяется для вызова функции-члена. (В следующем подразделе мы покажем, как осуществить такую привязку.) Хотя для указателя на обычную функцию и для указателя на функцию-член используется один и тот же термин, их природа различна.

Синтаксис объявления указателя на функцию-член должен принимать во внимание тип класса. То же верно и в отношении указателей на данные-члены. Рассмотрим член _height класса Screen. Его полный тип таков: член класса Screen типа short. Следовательно, полный тип указателя на _height – это указатель на член класса Screen типа short:

short Screen::*

Определение указателя на член класса Screen типа short выглядит следующим образом:

short Screen::*ps_Screen;

Переменную ps_Screen можно инициализировать адресом _height:

short Screen::*ps_Screen = &Screen::_height;

или присвоить ей адрес _width:

short Screen::*ps_Screen = &Screen::_width;

Переменной ps_Screen разрешается присваивать указатель на _width или _height, так как они являются членами класса Screen типа short.

Несоответствие типов указателя на данные-члены и обычного указателя также связано с различием в их представлении. Обычный указатель содержит всю информацию, необходимую для обращения к объекту. Указатель на данные-члены следует сначала привязать к объекту или указателю на него, а лишь затем использовать для доступа к члену этого объекта. (В книге "Inside the C++ Object Model” ([LIPPMAN96a]) также описывается представление указателей на члены.)

Указатель на функцию-член определяется путем задания типа возвращаемого функцией значения, списка ее параметров и класса. Например, следующий указатель, с помощью которого можно вызвать функции height() и width(), имеет тип указателя на функцию-член класса Screen без параметров, которая возвращает значение типа int:

int (Screen::*)()

Указатели на функции-члены можно объявлять, инициализировать и присваивать:

// всем указателям на функции-члены класса можно присвоить значение 0
int (Screen::*pmf1)() = 0;
int (Screen::*pmf2)() = &Screen::height;
pmf1 = pmf2;
pmf2 = &Screen::width;

Использование typedef может облегчить чтение объявлений указателей на члены. Например, для типа "указатель на функцию-член класса Screen без параметров, которая возвращает ссылку на объект Screen”, т.е.

Screen& (Screen::*)()

Следующий typedef определяет Action как альтернативное имя:

typedef Screen& (Screen::*Action)();
Action default = &Screen::home;
Action next = &Screen::forward;

Тип "указатель на функцию-член" можно использовать для объявления формальных параметров и типа возвращаемого значения функции. Для параметра того же типа можно также указать значение аргумента по умолчанию:

Screen& action( Screen&, Action)();

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

Screen meScreen;
typedef Screen& (Screen::*Action)();
Action default = &Screen::home;
extern Screen& action( Screen&, Sction = &Screen::display );
void ff()
{
   action( myScreen );
   action( myScreen, default );
   action( myScreen, &Screen::end );
}

В следующем подразделе обсуждается вызов функции-члена посредством указателя.
13.6.2. Работа с указателями на члены класса

К указателям на члены класса можно обращаться только с помощью конкретного объекта или указателя на объект типа класса. Для этого применяется любой из двух операторов доступа (.* для объектов класса и ссылок на них или ->* для указателей). Например, так вызывается функция-член через указатель на нее:

int (Screen::*pmfi)() = &Screen::height;
Screen& (Screen::*pmfS)( const Screen& ) = &Screen::copy;
Screen myScreen, *bufScreen;
// прямой вызов функции-члена
if ( myScreen.height() == bufScreen->height() )
   bufScreen->copy( myScreen );
// эквивалентный вызов по указателю
if ( (myScreen.*pmfi)() == (bufScreen->*pmfi)() )
   (bufScreen->*pmfS)( myScreen );

Вызовы

(myScreen.*pmfi)()
(bufScreen->*pmfi)();

требуют скобок, поскольку приоритет оператора вызова () выше, чем приоритет взятия указателя на функцию-член. Без скобок

myScreen.*pmfi()

интерпретируется как

myScreen.*(pmfi())

Это означает вызов функции pmfi() и привязку возвращенного ей значения к оператору (.*). Разумеется, тип pmfi не поддерживает такого использования, так что компилятор выдаст сообщение об ошибке.

Указатели на данные-члены используются аналогично:

typedef short Screen::*ps_Screen;
Screen myScreen, *tmpScreen = new Screen( 10, 10 );
ps_Screen pH = &Screen::_height;
ps_Screen pW = &Screen::_width;
tmpScreen->*pH = myScreen.*pH;
tmpScreen->*pW = myScreen.*pW;

Приведем реализацию функции-члена repeat(), которую мы обсуждали в начале этого раздела. Теперь она будет принимать указатель на функцию-член:

typedef Screen& (Screen::Action)();
Screen& Screen::repeat( Action op, int times )
{
   for ( int i = 0; i < times; ++i )
      (this->*op)();
   return *this;
}

Параметр op – это указатель на функцию-член, которая должна вызываться times раз.

Если бы нужно было задать значения аргументов по умолчанию, то объявление repeat() выглядело бы следующим образом:

class Screen {
public:
   Screen &repeat( Action = &Screen::forward, int = 1 );
   // ...
};

А ее вызовы так:

Screen myScreen;
myScreen.repeat();    // repeat( &Screen::forward, 1 );
myScreen.repeat( &Screen::down, 20 );

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

Action::Menu() = {
   &Screen::home,
   &Screen::forward,
   &Screen::back,
   &Screen::up,
   &Screen::down,
   &Screen::end
};
enum CursorMovements {
   HOME, FORWARD, BACK, UP, DOWN, END
};

Можно определить перегруженную функцию-член move(), которая принимает параметр CursorMovements и использует таблицу Menu для вызова указанной функции-члена. Вот ее реализация:

Screen& Screen::move( CursorMovements cm )
{
   ( this->*Menu[ cm ] )();
   return *this;
}

У оператора взятия индекса ([]) приоритет выше, чем у оператора указателя на функцию-член (->*). Первая инструкция в move() сначала по индексу выбирает из таблицы Menu нужную функцию-член, которая и вызывается с помощью указателя this и оператора указателя на функцию-член. move() можно применять в интерактивной программе, где пользователь выбирает вид перемещения курсора из отображаемого на экране меню.
13.6.3. Указатели на статические члены класса

Между указателями на статические и нестатические члены класса есть разница. Синтаксис указателя на член класса не используется для обращения к статическому члену. Статические члены – это глобальные объекты и функции, принадлежащие классу. Указатели на них – это обычные указатели. (Напомним, что статической функции-члену не передается указатель this.)

Объявление указателя на статический член класса выглядит так же, как и для указателя на объект, не являющийся членом класса. Для разыменования указателя никакой объект не требуется. Рассмотрим класс Account:

class Account {
public:
   static void raiseInterest( double incr );
   static double interest() { return _interestRate ; }
   double amount() { return _amount; }
private:
   static double  _interestRate;
   double         _amount;
   string         _owner;
};

inline void Account::raiseInterest( double incr )
{
 _interestRate += incr;
}

Тип &_interestRate – это double*:

// это неправильный тип для &_interestRate
double Account::*

Определение указателя на &_interestRate имеет вид:

// правильно: double*, а не double Account::*
double *pd = &Account::_interestRate;

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

Account unit;

// используется обычный оператор разыменования
double daily = *pd / 365 * unit._amount;

Однако, поскольку _interestRate и _amount – закрытые члены, необходимо иметь статическую функцию-член interest() и нестатическую amount().

Указатель на interest() – это обычный указатель на функцию:

// правильно
double (*)()
а не на функцию-член класса Account:
// неправильно
double (Account::*)()

Определение указателя и косвенный вызов interest() реализуются так же, как и для обычных указателей:

// правильно: double(*pf)(), а не double(Account::*pf)()
double(*pf)() = &Account::interest;
double daily = pf() / 365 * unit.amount();

Упражнение 13.11

К какому типу принадлежат члены _screen и _cursor класса Screen?

Упражнение 13.12

Определите указатель на член и инициализируйте его значением Screen::_screen; присвойте ему значение Screen::_cursor.

Упражнение 13.13

Определите typedef для каждой из функций-членов класса Screen.

Упражнение 13.14

Указатели на члены можно также объявлять как данные-члены класса. Модифицируйте определение класса Screen так, чтобы оно содержало указатель на его функцию-член того же типа, что home() и end().

Упражнение 13.15

Модифицируйте имеющийся конструктор класса Screen (или напишите новый) так, чтобы он принимал параметр типа указателя на функцию-член класса Screen, для которой список формальных параметров и тип возвращаемого значения такие же, как у home() и end(). Реализуйте для этого параметра значение по умолчанию и используйте параметр для инициализации члена класса, описанного в упражнении 13.14. Напишите функцию-член Screen, позволяющую пользователю задать ее значение.

Упражнение 13.16

Определите перегруженный вариант repeat(), который принимает параметр типа cursorMovements.
13.7. Объединение – класс, экономящий память

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

Рассмотрим пример, иллюстрирующий использование объединения. Лексический анализатор, входящий в состав компилятора, разбивает программу на последовательность лексем. Так, инструкция

int i = 0;

преобразуется в последовательность из пяти лексем:

   1. Ключевое слово int.
   2. Идентификатор i.
   3. Оператор =
   4. Константа 0 типа int.
   5. Точка с запятой.

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

Type ID Assign Constant Semicolon

(Тип ИД Присваивание Константа Точка с запятой)

Далее парсер анализирует значения каждой лексемы. В данном случае он видит:

Type <==> int
ID <==> i
Constant <==> 0

Для Assign и Semicolon дополнительной информации не нужно, так как у них может быть только одно значение: соответственно := и ;

Таким образом, в представлении лексемы могло бы быть два члена – token и value. token – это уникальный код, показывающий, что лексема имеет тип Type, ID, Assign, Constant или Semicolon, например 85 для ID и 72 для Semicolon.value содержит конкретное значение лексемы. Так, для лексемы ID в предыдущем объявлении value будет содержать строку "i", а для лексемы Type – некоторое представление типа int.

Представление члена value несколько проблематично. Хотя для любой отдельной лексемы в нем хранится всего одно значение, их типы для разных лексем могут различаться. Для лексемы ID в value хранится строка символов, а для Constant – целое число.

Конечно, для хранения данных нескольких типов можно использовать класс. Разработчик компилятора может объявить, что value принадлежит к типу класса, в котором для каждого типа данных есть отдельный член.

Применение класса решает проблему представления value. Однако для любой данной лексемы value имеет лишь один из множества возможных типов и, следовательно, будет задействован только один член класса, хотя памяти выделяется столько, сколько нужно для хранения всех членов. Чтобы память резервировалась только для нужного в данный момент члена, применяется объединение. Вот как оно определяется:

union TokenValue {
   char _cval;
   int _ival;
   char *_sval;
   double _dval;
};

Если самым большим типом среди всех членов TokenValue является dval, то размер TokenValue будет равен размеру объекта типа double. По умолчанию члены объединения открыты. Имя объединения можно использовать в программе всюду, где допустимо имя класса:

// объект типа TokenValue
TokenValue last_token;
// указатель на объект типа TokenValue
TokenValue *pt = new TokenValue;

Обращение к членам объединения, как и к членам класса, производится с помощью операторов доступа:

last_token._ival = 97;
char ch = pt->_cval;

Члены объединения можно объявлять открытыми, закрытыми или защищенными:

union TokenValue {
public:
   char _cval;
   // ...
private:
   int priv;
}

int main() {
   TokenValue tp;
   tp._cval = '\n';   // правильно

   // ошибка: main() не может обращаться к закрытому члену
   //         TokenValue::priv
   tp.priv = 1024;
}

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

union illegal_members {
   Screen s;      // ошибка: есть конструктор
   Screen *ps;    // правильно
   static int is; // ошибка: статический член
   int &rfi;      // ошибка: член-ссылка
};

Для объединения разрешается определять функции-члены, включая конструкторы и деструкторы:

union TokenValue {
public:
   TokenValue(int ix) : _ival(ix) { }
   TokenValue(char ch) : _cval(ch) { }
   // ...
   int ival() { return _ival; }
   char cval() { return _cval; }
private:
   int _ival;
   char _cval;
   // ...
};

int main() {
   TokenValue tp(10);
   int ix = tp.ival();
   //...
}

Вот пример работы объединения TokenValue:

enum TokenKind ( ID, Constant /* и другие типы лексем */ }
class Token {
public:
   TokenKind tok;
   TokenValue val;
};

Объект типа Token можно использовать так:

int lex() {
   Token curToken;
   char *curString;
   int curIval;

   // ...
   case ID:  // идентификатор
      curToken.tok = ID;
      curToken.val._sval = curString;
      break;

   case Constant:   // целая константа
      curToken.tok = Constant;
      curToken.val._ival = curIval;
      break;

   // ... и т.д.
}

Опасность, связанная с применением объединения, заключается в том, что можно случайно извлечь хранящееся в нем значение, пользуясь не тем членом. Например, если в последний раз значение присваивалось _ival, то вряд ли понадобится значение, оказавшееся в _sval. Это, по всей вероятности, приведет к ошибке в программе.

Чтобы защититься от подобного рода ошибок, следует создать дополнительный объект, дискриминант объединения, определяющий тип значения, которое в данный момент хранится в объединении. В классе Token роль такого объекта играет член tok:

char *idVal;
// проверить значение дискриминанта перед тем, как обращаться к sval
if ( curToken.tok == ID )
   idVal = curToken.val._sval;

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

#include
// функции доступа к члену объединения sval
string Token::sval() {
   assert( tok==ID );
   return val._sval;
}

Имя в определении объединения задавать необязательно. Если оно не используется в программе как имя типа для объявления других объектов, его можно опустить. Например, следующее определение объединения Token эквивалентно приведенному выше, но без указания имени:

class Token {
public:
   TokenKind tok;
   // имя типа объединения опущено
   union {
      char _cval;
      int _ival;
      char *_sval;
      double _dval;
   } val;
};

Существует анонимное объединение – объединение без имени, за которым не следует определение объекта. Вот, например, определение класса Token, содержащее анонимное объединение:

class Token {
public:
   TokenKind tok;
   // анонимное объединение
   union {
      char _cval;
      int _ival;
      char *_sval;
      double _dval;
   };
};

К данным-членам анонимного объединения можно напрямую обращаться в той области видимости, в которой оно определено. Перепишем функцию lex(), используя предыдущее определение:

int lex() {
   Token curToken;
   char *curString;
   int curIval;

   // ... выяснить, что находится в лексеме
   // ... затем установить curToken
   case ID:
      curToken.tok = ID;
      curToken._sval = curString;
      break;
   case Constant:   // целая константа
      curToken.tok = Constant;
      curToken._ival = curIval;
      break;

   // ... и т.д.
}

Анонимное объединение позволяет убрать один уровень доступа, поскольку обращение к его членам идет как к членам класса Token. У него не может быть закрытых или защищенных членов, а также функций-членов. Такое объединение, определенное в глобальной области видимости, должно быть объявлено в безымянном пространстве имен или иметь модификатор static.
Категория: С++ | Добавил: r2d2 (29.09.2011)
Просмотров: 370 | Рейтинг: 0.0/0
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Born in Ussr
Залогиниться
Турниры

/j clan ussr /j clan cccp