<< Пред. стр. 60 (из 121) След. >>
// это неправильный тип для &_interestRatedouble 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;
преобразуется в последовательность из пяти лексем:
Ключевое слово int.
Идентификатор i.
Оператор =
Константа 0 типа int.
Точка с запятой.
Лексический анализатор передает эти лексемы синтаксическому анализатору, парсеру, который идентифицирует полученную последовательность. Полученная информация должна дать парсеру возможность распознать эту последовательность лексем как объявление. Для этого с каждой лексемой ассоциируется информация, позволяющая парсеру увидеть следующее:
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.
13.8. Битовое поле – член, экономящий память
Для хранения заданного числа битов можно объявить член класса специального вида, называемый битовым полем. Он должен иметь целый тип данных, со знаком или без знака:
class File {
// ...
unsigned int modified : 1; // битовое поле
};
После идентификатора битового поля следует двоеточие, а за ним – константное выражение, задающее число битов. К примеру, modified – это поле из одного бита.
Битовые поля, определенные в теле класса подряд, по возможности упаковываются в соседние биты одного целого числа, делая хранение объекта более компактным. Так, в следующем объявлении пять битовых полей будут содержаться в одном числе типа unsigned int, ассоциированном с первым полем mode:
typedef unsigned int Bit;
class File {
public:
Bit mode: 2;
Bit modified: 1;
Bit prot_owner: 3;
Bit prot_group: 3;
Bit prot_world: 3;
// ...
};
Доступ к битовому полю осуществляется так же, как к прочим членам класса. Скажем, к битовому полю, являющемуся закрытым членом класса, можно обратиться лишь из функций-членов и друзей этого класса:
void File::write()
{
modified = 1;
// ...
}
void File::close()
{
if ( modified )
// ... сохранить содержимое
}
Вот простой пример использования битового поля длиной больше 1 (примененные здесь побитовые операции рассматривались в разделе 4.11):
enum { READ = 01, WRITE = 02 }; // режимы открытия файла
int main() {
File myFile;
myFile.mode |= READ;
if ( myFile.mode & READ )
cout << "myFile.mode is set to READ\n";
}
Обычно для проверки значения битового поля-члена определяются встроенные функции-члены. Допустим, в классе File можно ввести члены isRead() и isWrite():
inline int File::isRead() { return mode & READ; }
inline int File::isWrite() { return mode & WRITE; }
if ( myFile.isRead() ) /* ... */
С помощью таких функций-членов битовые поля можно сделать закрытыми членами класса File.
К битовому полю нельзя применять оператор взятия адреса (&), поэтому не может быть и указателя на подобные поля-члены. Кроме того, полю запрещено быть статическим членом.
В стандартной библиотеке C++ имеется шаблон класса bitset, который облегчает манипуляции с битовыми множествами. Мы рекомендуем использовать его вместо битовых полей. (Шаблон класса bitset и определенные в нем операции рассматривались в разделе 4.12.)
Упражнение 13.17
Перепишите примеры из этого подраздела так, чтобы в классе File вместо объявления и прямого манипулирования битовыми полями использовался класс bitset и его операторы.
13.9. Область видимости класса A
Тело класса определяет область видимости. Объявления членов класса внутри тела вводят их имена в область видимости класса.
Для обращения к ним применяются операторы доступа (точка и стрелка) и оператор разрешения области видимости (::). Когда употребляется оператор доступа, то предшествующее ему имя обозначает объект или указатель на объект типа класса, а следующее за ним имя должно находиться в области видимости этого класса. Аналогично при использовании оператора разрешения области видимости поиск имени, следующего за ним, идет в области видимости класса, имя которого стоит перед оператором. (В главах 17 и 18 мы увидим, что производный класс может обращаться к членам своих базовых.)
Однако применение операторов доступа или оператора разрешения области видимости нужно не всегда. Некоторые части программы сами по себе находятся в области видимости класса, и в них к членам класса можно обращаться напрямую. Одной из таких частей является само определение класса. Имя его члена можно использовать в теле после объявления:
class String {
public:
typedef int index_type;
// тип параметра - это на самом деле String::index_type
char& operator[]( index_type )
};
Порядок объявления членов класса в его теле важен: нельзя ссылаться на члены, которые будут объявлены позже. Например, если объявление оператора operator[]() находится раньше объявления typedef index_type, то приведенное ниже объявление operator[]() оказывается ошибочным, поскольку в нем используется еще неизвестное имя index_type:
class String {
public:
// ошибка: имя index_type не объявлено
char &operator[]( index_type );
typedef int index_type;
};
Однако из этого правила есть два исключения. Первое касается имен, использованных в определениях встроенных функций-членов, второе – имен, применяемых как аргументы по умолчанию. Рассмотрим обе ситуации.
Разрешение имен в определениях встроенных функций-членов происходит в два этапа. Сначала объявление функции (т.е. тип возвращаемого значения и список параметров) обрабатывается в том месте, где оно встретилось в определении класса. Затем тело функции обрабатывается во всей области видимости, сразу после того, как были просмотрены объявления всех членов. Посмотрим на наш пример, в котором оператор operator[]() определен как встроенный внутри тела класса:
class String {
public:
typedef int index_type;
char &operator[]( index_type elem )
{ return _string[ elem ]; }
private:
char *_string;
};
На первом этапе просматриваются имена, использованные в объявлении operator[](), чтобы найти имя типа параметра index_type. Поскольку первый шаг выполняется тогда, когда в теле класса встретилось определение функции-члена, то имя index_type должно быть объявлено до определения operator[]().
Обратите внимание, что член _string объявлен в теле класса после определения operator[](). Это правильно, и _string не является в теле operator[]() необъявленным именем. Имена в телах функций-членов просматриваются на втором шаге разрешения имен в определениях встроенных функций-членов. Этот этап выполняется во всей области видимости класса, как если бы тела функций-членов обрабатывались последними, прямо перед закрытием тела класса, когда все его члены уже объявлены.
Аргументы по умолчанию также разрешаются на втором шаге. Например, в объявлении функции-члена clear() используется имя статического члена bkground, который определен позже:
class Screen {
public:
// bkground относится к статическому члену,
// объявленному позже в определении класса
Screen& clear( char = bkground );
private:
static const char bkground = '#';
};
Хотя такие аргументы в объявлениях функций-членов разрешаются во всей области видимости класса, программа будет считаться ошибочной, если он ссылается на нестатический член. Нестатический член должен быть привязан к объекту своего класса или к указателю на такой объект, иначе использовать его нельзя. Употребление подобных членов в качестве аргументов по умолчанию нарушает это ограничение. Если переписать предыдущий пример так:
class Screen {
public:
// ...
// ошибка: bkground - нестатический член
Screen& clear( char = bkground );
private:
const char bkground = '#';
};
то имя аргумента по умолчанию разрешается нестатическим членом bkground, а это считается ошибкой.
Определения членов класса, появляющиеся вне его тела, – это еще один пример части программы, которая находится в области видимости класса. В ней имена членов распознаются несмотря на то, что оператор доступа или оператор разрешения области видимости при обращении к ним не применяется. Как же разрешаются имена в определениях членов?
Как правило, если такое определение появляется вне тела, то часть программы, следующая за именем определяемого члена, считается находящейся в области видимости класса вплоть до конца определения члена. Вынесем определение оператора operator[]() из класса String:
class String {