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

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

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

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

622

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

Screen& (Screen::*)()

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

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

Action next = &Screen::forward;

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

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

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

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 );

объект. Вызвать action() можно любым из следующих способов:

}

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

13.6.2. Работа с указателями на члены класса

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

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

623

 

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(), которую мы обсуждали в начале этого

 

раздела. Теперь она будет принимать указатель на функцию-член:

 

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

624

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;

// repeat( &Screen::forward, 1 );

myScreen.repeat();

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

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

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

Action::Menu() = { &Screen::home, &Screen::forward, &Screen::back, &Screen::up, &Screen::down, &Screen::end

};

enum CursorMovements {

HOME, FORWARD, BACK, UP, DOWN, END

Menu.

};

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

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

625

Screen& Screen::move( CursorMovements cm )

{

( this->*Menu[ cm ] )(); return *this;

}

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

13.6.3. Указатели на статические члены класса

Между указателями на статические и нестатические члены класса есть разница.

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

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

class Account { public:

static void raiseInterest( double incr );

static double interest() { return _interestRate ; } double amount() { return _amount; }

private:

_interestRate;

static double

double

_amount;

string

_owner;

};

 

inline void Account::raiseInterest( double incr )

{

_interestRate += incr;

не требуется. Рассмотрим класс Account:

}

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

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

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

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

626

// правильно: 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

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

627

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

парсеру увидеть следующее:

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

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

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

628

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;

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

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

629

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;

// ошибка: член-ссылка

оператор присваивания. Например:

};

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

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

630

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(); //...

}

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

public: TokenKind tok;

TokenValue val;

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

};

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;

// ... и т.д.

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

}

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

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

631

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

char *idVal;

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

хранится в объединении. В классе Token роль такого объекта играет член tok: idVal = curToken.val._sval;

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

#include <cassert>

// функции доступа к члену объединения 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, содержащее анонимное объединение: