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

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

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

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

632

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; // битовое поле

знака:

};

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

633

После идентификатора битового поля следует двоеточие, а за ним константное выражение, задающее число битов. К примеру, modified это поле из одного бита.

Битовые поля, определенные в теле класса подряд, по возможности упаковываются в соседние биты одного целого числа, делая хранение объекта более компактным. Так, в

следующем объявлении пять битовых полей будут содержаться в одном числе типа

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

unsigned int, ассоциированном с первым полем mode:

};

Доступ к битовому полю осуществляется так же, как к прочим членам класса. Скажем, к битовому полю, являющемуся закрытым членом класса, можно обратиться лишь из

void File::write()

{

modified = 1; // ...

}

void File::close()

{

if ( modified )

// ... сохранить содержимое

функций-членов и друзей этого класса:

}

Вот простой пример использования битового поля длиной больше 1 (примененные здесь

enum { READ = 01, WRITE = 02 }; // режимы открытия файла

int main() { File myFile;

myFile.mode |= READ;

if ( myFile.mode & READ )

cout << "myFile.mode is set to READ\n";

побитовые операции рассматривались в разделе 4.11):

}

Обычно для проверки значения битового поля-члена определяются встроенные функции- члены. Допустим, в классе File можно ввести члены isRead() и isWrite():

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

634

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[]() находится

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

635

раньше объявления typedef index_type, то приведенное ниже объявление operator[]() оказывается ошибочным, поскольку в нем используется еще неизвестное

class String { public:

// ошибка: имя index_type не объявлено char &operator[]( index_type );

typedef int index_type;

имя index_type:

};

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

Разрешение имен в определениях встроенных функций-членов происходит в два этапа. Сначала объявление функции (т.е. тип возвращаемого значения и список параметров) обрабатывается в том месте, где оно встретилось в определении класса. Затем тело функции обрабатывается во всей области видимости, сразу после того, как были просмотрены объявления всех членов. Посмотрим на наш пример, в котором оператор

class String { public:

typedef int index_type;

char &operator[]( index_type elem ) { return _string[ elem ]; }

private:

char *_string;

operator[]() определен как встроенный внутри тела класса:

};

На первом этапе просматриваются имена, использованные в объявлении operator[](), чтобы найти имя типа параметра index_type. Поскольку первый шаг выполняется тогда, когда в теле класса встретилось определение функции-члена, то имя index_type должно быть объявлено до определения operator[]().

Обратите внимание, что член _string объявлен в теле класса после определения operator[](). Это правильно, и _string не является в теле operator[]()

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

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

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

636

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, а это считается ошибкой.

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

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

class String { public:

typedef int index_type;

char& operator[]( index_type ); private:

char *_string;

};

// в operator[]() есть обращения к index_type и _string inline char& operator[]( index_type elem )

{

return _string[ elem ];

operator[]() из класса String:

}

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

637

Обратите внимание, что в списке параметров встречается typedef index_type без квалифицирующего имени класса String::.Текст, следующий за именем члена String::operator[] и до конца определения функции, находится в области видимости класса. Объявленные в этой области типы рассматриваются при разрешении имен типов, использованных в списке параметров функции-члена.

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

class Account: // ...

private:

static double _interestRate; static double initInterestRate();

};

// ссылается на Account::initInterest()

операторов, ссылаться на члены класса:

double Account::_interestRate = initInterest();

Инициализатор _interestRate вызывает статическую функцию-член Account::initInterest() несмотря на то, что ее имя не квалифицировано именем класса.

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

class Account: // ...

private:

static const int nameSize = 16; static const char name[nameSize];

// nameSize не квалифицировано именем класса Account

члену класса nameSize:

const char Account::name[nameSize] = "Savins Account";

Хотя член nameSize не квалифицирован именем класса Account, определение name не является ошибкой, так как оно находится в области видимости своего класса и может ссылаться на его члены после того, как компилятор прочитал Account::name.

В определении члена, которое появляется вне тела, часть программы перед определяемым именем не находится в области видимости класса. При обращении к члену в этой части следует пользоваться оператором разрешения области видимости. Например, если типом статического члена является typedef Money, определенный в классе Account, то имя Money должно быть квалифицировано, когда статический член данных определяется вне тела класса:

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

638

class Account {

typedef double Money; //...

private:

static Money _interestRate; static Money initInterest();

};

// Money должно быть квалифицировано именем класса Account::

Account::Money Account::_interestRate = initInterest();

С каждым классом ассоциируется отдельная область видимости, причем у разных классов эти области различны. К членам одного класса нельзя напрямую обращаться в определениях членов другого класса, если только один из них не является для второго базовым. (Наследование и базовые классы рассматриваются в главах 17 и 18.)

13.9.1. Разрешение имен в области видимости класса

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

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

1.Просматриваются объявления членов класса, появляющиеся перед употреблением имени.

2.Если на шаге 1 разрешение не привело к успеху, то просматриваются объявления в пространстве имен перед определением класса. Напомним, что глобальная область видимости это тоже область видимости пространства имен. (О пространствах имен речь шла в разделе 8.5.)

typedef double Money; class Account {

//...

private:

static Money _interestRate; static Money initInterest();

//...

Например:

};

Сначала компилятор ищет объявление Money в области видимости класса Account. При этом учитываются только те объявления, которые встречаются перед использованием Money. Поскольку таких объявлений нет, далее поиск ведется в глобальной области видимости. Объявление глобального typedef Money найдено, именно этот тип и используется в объявлениях _interestRate и initInterest().

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

639

Имя, встретившееся в определении функции-члена класса, разрешается следующим образом:

1.Сначала просматриваются объявления в локальных областях видимости функции- члена. (О локальных областях видимости и локальных объявлениях говорилось в разделе 8.1.)

2.Если шаг 1 не привел к успеху, то просматриваются объявления для всех членов класса.

3.Если и этого оказалось недостаточно, просматриваются объявления в пространстве имен перед определением функции-члена.

int _height;

class Screen { public:

Screen( int _height ) {

_height = 0; // к чему относится _height? К параметру

}

private:

short _height;

Имена, встречающиеся в теле встроенной функции-члена, разрешаются так:

};

В поисках объявления имени _height, которое встретилось в определении конструктора Screen, компилятор просматривает локальную область видимости функции и находит его там. Следовательно, это имя относится к объявлению параметра.

Если бы такое объявление не было найдено, компилятор начал бы поиск в области видимости класса Screen, просматривая все объявления его членов, пока не встретится объявление члена _height. Говорят, что имя члена _height скрыто объявлением параметра конструктора, но его можно использовать в теле конструктора, если

int _height;

class Screen { public:

Screen( long _height ) {

this->_height = 0; // относится к Screen::_height

//тоже правильно:

//Screen::_height = 0;

}

private:

short _height;

квалифицировать имя члена именем его класса или явно использовать указатель this:

};

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

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

640

объявлением члена класса, однако его можно использовать в теле конструктора, если

int _height;

class Screen { public:

Screen( long _height ) {

::_height = 0; // относится к глобальному объекту

}

private:

short _height;

квалифицировать оператором разрешения глобальной области видимости:

};

Если конструктор объявлен вне определения класса, то на третьем шаге разрешения имени просматриваются объявления в глобальной области видимости, которые встретились перед определением класса Screen, а также перед определением функции-

class Screen { public:

// ...

void setHeight( int ); private:

short _height;

};

int verify(int);

void Screen::setHeight( int var ) {

//var: относится к параметру

//_height: относится к члену класса

//verify: относится к глобальной функции

_height = verify( var );

члена:

}

Обратите внимание, что объявление глобальной функции verify() невидимо до определения класса Screen. Однако на третьем шаге разрешения имени просматриваются объявления в областях видимости пространств имен, видимые перед определением члена, поэтому нужное объявление обнаруживается.

Имя, встретившееся в определении статического члена класса, разрешается следующим образом:

1.Просматриваются объявления всех членов класса.

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

Упражнение 13.18

Назовите те части программы, которые находятся в области видимости класса.

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

641

Упражнение 13.19

Назовите те части программы, которые находятся в области видимости класса и для которых при разрешении имен просматривается полная область (т.е. принимаются во внимание все члены, объявленные в теле класса).

Упражнение 13.20

К каким объявлениям относится имя Type при использовании в теле класса Exersise и в определении его функции-члена setVal()? (Напоминаем, что разные вхождения могут относиться к разным объявлениям.) К каким объявлениям относится имя initVal при

typedef int Type; Type initVal();

class Exercise { public:

// ...

typedef double Type; Type setVal( Type ); Type initVal();

private: int val;

};

Type Exercise::setVal( Type parm ) { val = parm + initVal();

употреблении в определении функции-члена setVal()?

}

Определение функции-члена setVal() ошибочно. Можете ли вы сказать, почему? Внесите необходимые изменения, чтобы в классе Exercise использовался глобальный typedef Type и глобальная функция initVal().

13.10. Вложенные классы A

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

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