Меню сайта |
|
|
Категория |
|
|
Развлечение |
|
|
ON - LINE |
|
|
Опрос |
|
|
Оbserver Ward |
Онлайн всего: 2 Гостей: 2 Пользователей: 0
|
|
Друзья сайта |
|
|
Hаша кнопка |
Для обмена банерами , наша кнопка для размещения у вас на сайте
|
|
|
13. Классы (3)
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 { 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 ]; }
Обратите внимание, что в списке параметров встречается 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 может быть обращение к члену класса nameSize:
class Account: // ... private: static const int nameSize = 16; static const char name[nameSize]; // nameSize не квалифицировано именем класса Account const char Account::name[nameSize] = "Savins Account";
Хотя член nameSize не квалифицирован именем класса Account, определение name не является ошибкой, так как оно находится в области видимости своего класса и может ссылаться на его члены после того, как компилятор прочитал Account::name.
В определении члена, которое появляется вне тела, часть программы перед определяемым именем не находится в области видимости класса. При обращении к члену в этой части следует пользоваться оператором разрешения области видимости. Например, если типом статического члена является typedef Money, определенный в классе Account, то имя Money должно быть квалифицировано, когда статический член данных определяется вне тела класса:
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().
Имя, встретившееся в определении функции-члена класса, разрешается следующим образом:
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 скрыто объявлением параметра конструктора, но его можно использовать в теле конструктора, если квалифицировать имя члена именем его класса или явно использовать указатель this:
int _height;
class Screen { public: Screen( long _height ) { this->_height = 0; // относится к Screen::_height // тоже правильно: // Screen::_height = 0; } private: short _height; };
Если бы не были найдены ни объявление параметра, ни объявление члена, компилятор стал бы искать их в объемлющих областях видимости пространств имен. В нашем примере в глобальной области видимости просматриваются объявления, которые расположены перед определением класса Screen. В результате было бы найдено объявление глобального объекта _height. Говорят, что такой объект скрыт за объявлением члена класса, однако его можно использовать в теле конструктора, если квалифицировать оператором разрешения глобальной области видимости:
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
Назовите те части программы, которые находятся в области видимости класса.
Упражнение 13.19
Назовите те части программы, которые находятся в области видимости класса и для которых при разрешении имен просматривается полная область (т.е. принимаются во внимание все члены, объявленные в теле класса).
Упражнение 13.20
К каким объявлениям относится имя Type при использовании в теле класса Exersise и в определении его функции-члена setVal()? (Напоминаем, что разные вхождения могут относиться к разным объявлениям.) К каким объявлениям относится имя initVal при употреблении в определении функции-члена setVal()?
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() ошибочно. Можете ли вы сказать, почему? Внесите необходимые изменения, чтобы в классе Exercise использовался глобальный typedef Type и глобальная функция initVal(). 13.10. Вложенные классы A
Класс, объявленный внутри другого класса, называется вложенным. Он является членом объемлющего класса, а его определение может находиться в любой из секций public, private или protected объемлющего класса.
Имя вложенного класса известно в области видимости объемлющего класса, но ни в каких других областях. Это означает, что оно не конфликтует с таким же именем, объявленным в объемлющей области видимости. Например:
class Node { /* ... */ } class Tree { public: // Node инкапсулирован внутри области видимости класса Tree // В этой области Tree::Node скрывает ::Node class Node {...}; // правильно: разрешается в пользу вложенного класса: Tree::Node Node *tree; };
// Tree::Node невидима в глобальной области видимости // Node разрешается в пользу глобального объявления Node Node *pnode; class List { public: // Node инкапсулирован внутри области видимости класса List // В этой области List::Node скрывает ::Node class Node {...}; // правильно: разрешается в пользу вложенного класса: List::Node Node *list; };
Для вложенного класса допустимы такие же виды членов, как и для невложенного:
// Не идеально, будем улучшать class List { public: class ListItem { friend class List; // объявление друга ListItem( int val=0 ); // конструктор ListItem *next; // указатель на собственный класс int value; }; // ... private: ListItem *list; ListItem *at_end; };
Закрытым называется член, который доступен только в определениях членов и друзей класса. У объемлющего класса нет права доступа к закрытым членам вложенного. Чтобы в определениях членов List можно было обращаться к закрытым членам ListItem, класс ListItem объявляет List как друга. Равно и вложенный класс не имеет никаких специальных прав доступа к закрытым членам объемлющего класса. Если бы нужно было разрешить ListItem доступ к закрытым членам класса List, то в объемлющем классе List следовало бы объявить вложенный класс как друга. В приведенном выше примере этого не сделано, поэтому ListItem не может обращаться к закрытым членам List.
Объявление ListItem открытым членом класса List означает, что вложенный класс можно использовать как тип во всей программе, в том числе и за пределами определений членов и друзей класса. Например:
// правильно: объявление в глобальной области видимости List::ListItem *headptr;
Это дает более широкую область видимости, чем мы планировали. Вложенный ListItem поддерживает абстракцию класса List и не должен быть доступен во всей программе. Поэтому лучше объявить вложенный класс ListItem закрытым членом List:
// Не идеально, будем улучшать class List { public: // ... private: class ListItem { // ... }; ListItem *list; ListItem *at_end; };
Теперь тип ListItem доступен только из определений членов и друзей класса List, поэтому все члены класса ListItem можно сделать открытыми. При таком подходе объявление List как друга ListItem становится ненужным. Вот новое определение класса List:
// так лучше class List { public: // ... private: // Теперь ListItem закрытый вложенный тип class ListItem { // а его члены открыты public: ListItem( int val=0 ); ListItem *next; int value; }; ListItem *list; ListItem *at_end; };
Конструктор ListItem не задан как встроенный внутри определения класса и, следовательно, должен быть определен вне него. Но где именно? Конструктор класса ListItem не является членом List и, значит, не может быть определен в теле последнего; его нужно определить в глобальной области видимости – той, которая содержит определение объемлющего класса. Когда функция-член вложенного класса не определяется как встроенная в теле, она должна быть определена вне самого внешнего из объемлющих классов.
Вот как могло бы выглядеть определение конструктора ListItem. Однако показанный ниже синтаксис в глобальной области видимости некорректен:
class List { public: // ... private: class ListItem { public: ListItem( int val=0 ); // ... }; }; // ошибка: ListItem вне области видимости ListItem:: ListItem( int val ) { ... }
Проблема в том, что имя ListItem отсутствует в глобальной области видимости. При использовании его таким образом следует указывать, что ListItem – вложенный класс в области видимости List. Это делается путем квалификации имени ListItem именем объемлющего класса. Следующая конструкция синтаксически правильна:
// имя вложенного класса квалифировано именем объемлющего List::ListItem::ListItem( int val ) { value = val; next = 0; }
Заметим, что квалифицировано только имя вложенного класса. Первый квалификатор List:: именует объемлющий класс и квалифицирует следующее за ним имя вложенного ListItem. Второе вхождение ListItem – это имя конструктора, а не вложенного класса. В данном определении имя члена некорректно:
// ошибка: конструктор называется ListItem, а не List::ListItem List::ListItem::List::ListItem( int val ) { value = val; next = 0; }
Если бы внутри ListItem был объявлен статический член, то его определение также следовало бы поместить в глобальную область видимости. Имя этого члена могло бы выглядеть так:
int List::ListItem::static_mem = 1024;
Обратите внимание, что функции-члены и статические данные-члены не обязаны быть открытыми членами вложенного класса для того, чтобы их можно было определить вне его тела. Закрытые члены ListItem также определяются в глобальной области видимости.
Вложенный класс разрешается определять вне тела объемлющего. Например, определение ListItem могло бы находиться и в глобальной области видимости:
class List { public: // ... private: // объявление необходимо class ListItem; ListItem *list; ListItem *at_end; }; // имя вложенного класса квалифицировано именем объемлющего класса class List::ListItem { public: ListItem( int val=0 ); ListItem *next; int value; };
В глобальном определении имя вложенного ListItem должно быть квалифицировано именем объемлющего класса List. Заметьте, что объявление ListItem в теле List опустить нельзя. Определение вложенного класса не может быть задано в глобальной области видимости, если предварительно оно не было объявлено членом объемлющего класса. Но при этом вложенный класс не обязательно должен быть открытым членом объемлющего.
Пока компилятор не увидел определения вложенного класса, разрешается объявлять лишь указатели и ссылки на него. Объявления членов list и at_end класса List правильны несмотря на то, что ListItem определен в глобальной области видимости, поскольку оба члена – указатели. Если бы один из них был объектом, то его объявление в классе List привело бы к ошибке компиляции:
class List { public: // ... private: // объявление необходимо class ListItem; ListItem *list; ListItem at_end; // ошибка: неопределенный вложенный класс ListItem };
Зачем определять вложенный класс вне тела объемлющего? Возможно, он поддерживает некоторые детали реализации ListItem, а нам нужно скрыть их от пользователей класса List. Поэтому мы помещаем определение вложенного класса в заголовочный файл, содержащий интерфейс List. Таким образом, определение ListItem может находиться лишь внутри исходного файла, включающего реализацию класса List и его членов.
Вложенный класс можно сначала объявить, а затем определить в теле объемлющего. Это позволяет иметь во вложенных классах члены, ссылающиеся друг на друга:
class List { public: // ... private: // объявление List::ListItem class ListItem; class Ref { // pli имеет тип List::ListItem* ListItem *pli; }; определение List::ListItem class ListItem { // pref имеет тип List::Ref* Ref *pref; }; };
Если бы ListItem не был объявлен перед определением класса Ref, то объявление члена pli было бы ошибкой.
Вложенный класс не может напрямую обращаться к нестатическим членам объемлющего, даже если они открыты. Любое такое обращение должно производиться через указатель, ссылку или объект объемлющего класса. Например:
class List { public: int init( int ); private: class List::ListItem { public: ListItem( int val=0 ); void mf( const List & ); int value; }; }; List::ListItem::ListItem { int val ) { // List::init() - нестатический член класса List // должен использоваться через объект или указатель на тип List value = init( val ); // ошибка: неверное использование init };
При использовании нестатических членов класса компилятор должен иметь возможность идентифицировать объект, которому принадлежит такой член. Внутри функции-члена класса ListItem указатель this неявно применяется лишь к его членам. Благодаря неявному this мы знаем, что член value относится к объекту, для которого вызван конструктор. Внутри конструктора ListItem указатель this имеет тип ListItem*. Для доступа же к функции-члену init() нужен объект типа List или указатель типа List*.
Следующая функция-член mf() обращается к init() с помощью параметра-ссылки. Таким образом, init() вызывается для объекта, переданного в аргументе функции:
void List::ListItem::mf( List &i1 ) { memb = i1.init(); // правильно: обращается к init() по ссылке }
Хотя для доступа к нестатическим членам объемлющего класса нужен объект, указатель или ссылка, к статическим его членам, именам типов и элементам перечисления вложенный класс может обращаться напрямую (если, конечно, эти члены открыты). Имя типа – это либо имя typedef, либо имя перечисления, либо имя класса. Например:
class List { public: typedef int (*pFunc)(); enum ListStatus { Good, Empty, Corrupted }; //... private: class ListItem { public: void check_status(); ListStatus status; // правильно pFunc action; // правильно // ... }; // ... };
pFunc, ListStatus и ListItem – все это вложенные имена типов в области видимости объемлющего класса List. К ним, а также к элементам перечисления ListStatus можно обращаться в области видимости класса ListItem даже без квалификации:
void List::ListItem::check_status() { ListStatus s = status; switch ( s ) { case Empty: ... case Corrupted: ... case Good: ... } }
Вне области видимости ListItem и List при обращении к статическим членам, именам типов и элементам перечисления объемлющего класса требуется оператор разрешения области видимости:
List::pFunc myAction; // правильно List::ListStatus stat = List::Empty; // правильно
При обращении к элементам перечисления мы не пишем:
List::ListStatus::Empty
поскольку они доступны непосредственно в той области видимости, в которой определено само перечисление. Почему? Потому что с ним, в отличие от класса, не связана отдельная область. 13.10.1. Разрешение имен в области видимости вложенного класса
Посмотрим, как разрешаются имена в определениях вложенного класса и его членов.
Имя, встречающееся в определении вложенного класса (кроме тех, которые употребляются во встроенных функциях-членах и аргументах по умолчанию) разрешается следующим образом:
1. Просматриваются члены вложенного класса, расположенные перед употреблением имени. 2. Если шаг 1 не привел к успеху, то просматриваются объявления членов объемлющего класса, расположенные перед употреблением имени. 3. 4. Если и этого недостаточно, то просматриваются объявления, расположенные в области видимости пространства имен перед определением вложенного класса.
Например:
enum ListStatus { Good, Empty, Corrupted }; class List { public: // ... private: class ListItem { public: // Смотрим в: // 1) List::ListItem // 2) List // 3) глобальной области видимости ListStatus status; // относится к глобальному перечислению // ... }; // ... };
Сначала компилятор ищет объявление ListStatus в области видимости класса ListItem. Поскольку его там нет, поиск продолжается в области видимости List, а затем в глобальной. При этом во всех трех областях просматриваются только объявления, предшествующие использованию ListStatus. В конце концов находится глобальное объявление перечисления ListStatus – оно и будет типом, использованным в объявлении status.
Если вложенный класс ListItem определен в глобальной области видимости, вне тела объемлющего класса List, то все члены List уже были объявлены:
class List { private: class ListItem { //... public: enum ListStatus { Good, Empty, Corrupted }; // ... }; class List::ListItem { public: // Смотрим в: // 1) List::ListItem // 2) List // 3) глобальной области видимости ListStatus status; // относится к глобальному перечислению // ... };
При разрешении имени ListStatus сначала просматривается область видимости класса ListItem. Поскольку там его нет, поиск продолжается в области видимости List. Так как полное определение класса List уже встречалось, просматриваются все члены этого класса. Вложенное перечисление ListStatus найдено несмотря даже на то, что оно объявлено после объявления ListItem. Таким образом, status объявляется как указатель на данное перечисление в классе List. Если бы в List не было члена с таким именем, поиск был бы продолжен в глобальной области видимости среди тех объявлений, которые предшествуют определению класса ListItem.
Имя, встретившееся в определении функции-члена вложенного класса, разрешается следующим образом:
1. Сначала просматриваются локальные области видимости функции-члена. 2. Если шаг 1 не привел к успеху, то просматриваются объявления всех членов вложенного класса. 3. Если имя еще не найдено, то просматриваются объявления всех членов объемлющего класса. 4. Если и этого недостаточно, то просматриваются объявления, появляющиеся в области видимости пространства имен перед определением функции-члена.
Какое объявление относится к имени list в определении функции-члена check_status() в следующем фрагменте кода:
class List { public: enum ListStatus { Good, Empty, Corrupted }; // ... private: class ListItem { public: void check_status(); ListStatus status; // правильно //... }; ListItem *list; }; int list = 0; void List::ListItem::check_status() { int value = list; // какой list? }
Весьма вероятно, что при использовании list внутри check_status() программист имел в виду глобальный объект:
* и value, и глобальный объект list имеют тип int. Член List::list объявлен как указатель и не может быть присвоен value без явного приведения типа; * ListItem не имеет прав доступа к закрытым членам объемлющего класса, в частности list; * list – это нестатический член, и обращение к нему в функциях-членах ListItem должно производиться через объект, указатель или ссылку.
Однако, несмотря на все это, имя list, встречающееся в функции-члене check_status(), разрешается в пользу члена list класса List. Напоминаем, что если имя не найдено в области видимости вложенного ListItem, то далее просматривается область видимости объемлющего класса, а не глобальная. Член list в List скрывает глобальный объект. А так как использование указателя list в check_status() недопустимо, то выводится сообщение об ошибке.
Права доступа и совместимость типов проверяются только после того, как имя разрешено. Если при этом обнаруживается ошибка, то выдается сообщение о ней и дальнейший поиск объявления, которое было бы лучше согласовано с именем, уже не производится. Для доступа к глобальному объекту list следует использовать оператор разрешения области видимости:
void List::ListItem::check_status() { int value = ::list; // правильно }
Если бы функция-член check_status() была определена как встроенная в теле класса ListItem, то последнее объявление привело бы к выдаче сообщения об ошибке из-за того, что имя list не объявлено в глобальной области видимости:
class List { public: // ... private: class ListItem { public: // ошибка: нет видимого объявления для ::list void check_status() { int value = ::lis; } //... }; ListItem *list; // ... }; int list = 0;
Глобальный объект list объявлен после определения класса List. Во встроенной функции-члене, определенной внутри тела класса, рассматриваются только те глобальные объявления, которые были видны перед определением объемлющего класса. Если же определение check_status() следует за определением List, то рассматриваются глобальные объявления, расположенные перед ним, поэтому будет найдено глобальное определение объекта list.
Упражнение 13.21
В главе 11 был приведен пример программы, использующей класс iStack. Измените его, объявив классы исключений pushOnFull и popOnEmpty открытыми вложенными в iStack. Модифицируйте соответствующим образом определение класса iStack и его функций-членов, а также определение main(). 13.11. Классы как члены пространства имен A
Представленные до сих пор классы определены в области видимости глобального пространства имен. Но их можно определять и в объявленных пользователем пространствах. Имя класса, определенного таким образом, доступно только в области видимости этого пространства, т.е. оно не конфликтует с именами, объявленными в других пространствах имен. Например:
namespace cplusplus_primer { class Node { /* ... */ }; } namespace DisneyFeatureAnimation { class Node { /* ... */ }; } Node *pnode; // ошибка: Node не видно в глобальной области видимости // правильно: объявляет nodeObj как объект // квалифицированного типа DisneyFeatureAnimation::Node DisneyFeatureAnimation::Node nodeObj; // using-объявление делает Node видимым в глобальной области видимости using cplusplus_primer::Node; Node another; // cplusplus_primer::Node
Как было показано в двух предыдущих разделах, член класса (функция-член, статический член или вложенный класс) может быть определен вне его тела. Если мы реализуем библиотеку и помещаем определения наших классов в объявленное пользователем пространство имен, то где расположить определения членов, находящиеся вне тел своих классов? Их можно разместить либо в пространстве имен, которое содержит определение самого внешнего класса, либо в одном из объемлющих его пространств. Это дает возможность организовать код библиотеки следующим образом:
// --- primer.h --- namespace cplusplus_primer { class List { // ... private: class ListItem { public: void check_status(); int action(); // ... }; }; } // --- primer.C --- #include "primer.h" namespace cplusplus_primer { // правильно: check_status() определено в том же пространстве имен, // что и List void List::ListItem::check_status() { } } // правильно: action() определена в глобальной области видимости // в пространстве имен, объемлющем определение класса List // Имя члена квалифицировано именем пространства int cplusplus_primer::List::ListItem::action() { }
Члены вложенного класса ListItem можно определить в пространстве имен cplusplus_primer, которое содержит определение List, или в глобальном пространстве, включающем определение cplusplus_primer. В любом случае имя члена в определении должно быть квалифицировано именами объемлющих классов и объявленных пользователем пространств, вне которых находится объявление члена.
Как происходит разрешение имени в определении члена, которое находится в объявленном пользователем пространстве? Например, как будет разрешено someVal:
int cplusplus_primer::List::ListItem::action() { int local = someVal; // ... }
Сначала просматриваются локальные области видимости в определении функции-члена, затем поиск продолжается в области видимости ListItem, затем – в области видимости List. До этого момента все происходит так же, как в процессе разрешения имен, описанном в разделе 13.10. Далее просматриваются объявления из пространства cplusplus_primer и наконец объявления в глобальной области видимости, причем во внимание принимаются только те, которые расположены до определения функции-члена action():
// --- primer.h --- namespace cplusplus_primer { class List { // ... private: class ListItem { public: int action(); // ... }; }; const int someVal = 365; } // --- primer.C --- #include "primer.h" namespace cplusplus_primer { int List::ListItem::action() { // правильно: cplusplus_primer::someVal int local = someVal;
// ошибка: calc() еще не объявлена double result = calc( local ); // ... } double calc(int) { } // ... }
Определение пространства имен cplusplus_primer не является непрерывным. Определения класса List и объекта someVal размещены в первом его разделе, который находится в заголовочном файле primer.h. Определение функции calc() появляется в определении пространства имен, расположенном в файле реализации primer.C. Использование calc() внутри action() ошибочно, так как она объявлена после использования. Если calc() – часть интерфейса cplusplus_primer, ее следовало бы объявить в той части данного пространства, которая находится в заголовочном файле:
// --- primer.h --- namespace cplusplus_primer { class List { // ... } const int someVal = 365; double calc(int); }
Если же calc() используется только в action() и не является частью интерфейса пространства имен, то ее нужно объявить перед action(), чтобы можно было ссылаться на нее внутри определения action().
Здесь прослеживается аналогия с процессом поиска объявлений в глобальной области видимости, о котором мы говорили в предыдущих разделах: объявления, предшествующие определению члена, принимаются во внимание, тогда как следующие за ним игнорируются.
Довольно просто запомнить, в каком порядке просматриваются области видимости при поиске имени из определения функции, расположенного вне определения класса. Имена, которыми квалифицировано имя члена, указывают порядок рассмотрения пространств. Например, имя action() в предыдущем примере квалифицируется так:
cplusplus_primer::List::ListItem::action()
Квалификаторы cplusplus_primer::List::ListItem:: записаны в порядке, обратном тому, в котором просматриваются имена областей видимости классов и пространств имен. Сначала поиск ведется в области ListItem, затем продолжается в объемлющем классе List и наконец в пространстве cplusplus_primer, предшествующем той области, в которой находится определение action(). Во время поиска в любой области видимости класса просматриваются все объявления членов, а в любом пространстве имен – только те объявления, которые встречались перед определением члена.
Класс, определенный в области видимости пространства имен, потенциально виден во всей программе. Если заголовочный файл primer.h включен в несколько исходных файлов, то имя cplusplus_primer::List везде относится к одному и тому же классу. Класс – это сущность, для которой в программе может быть более одного определения. Определение класса должно присутствовать один раз в каждом исходном файле, где определяются или используются сам класс или его члены. Однако оно должно быть одинаковым во всех файлах, где встречается, поэтому его следует помещать в заголовочный файл, например primer.h. Затем такой файл можно включать в любой исходный, где определяются или используются члены класса. Это предотвратит несоответствия в случае, когда определение класса записывается более одного раза.
Невстроенные функции-члены и статические данные-члены класса в пространстве имен – это также программные сущности. Однако они могут быть определены лишь один раз во всей программе. Поэтому их определения помещаются не в заголовочный, а в отдельный исходный файл типа primer.C.
Упражнение 13.22
Используя класс iStack, определенный в упражнении 13.21, объявите классы исключений pushOnFull и popOnEmpty как члены пространства имен LibException:
namespace LibException { class pushOnFull{ }; class popOnEmpty{ }; }
а сам iStack – членом пространства имен Container. Модифицируйте соответствующим образом определение данного класса и его функций-членов, а также определение main(). 13.12. Локальные классы A
Класс, определенный внутри тела функции, называется локальным. Он виден только в той локальной области, где определен. Не существует синтаксиса, позволяющего обратиться к члену такого класса, в отличие от вложенного, извне локальной области видимости, содержащей его определение. Поэтому функции-члены локального класса должны определяться внутри определения самого класса. На практике это ограничивает их сложность несколькими строками кода; помимо всего прочего, такой код становится трудно читать.
Поскольку невозможно определить член локального класса в области видимости пространства имен, то в таком классе не бывает статических членов.
Класс, вложенный в локальный, может быть определен вне определения объемлющего класса, но только в локальной области видимости, содержащей это определение. Имя вложенного класса в таком определении должно быть квалифицировано именем объемлющего класса. Объявление вложенного класса в объемлющем нельзя опускать:
void foo( int val ) { class Bar { public: int barVal; class nested; // объявление вложенного класса обязательно };
// определение вложенного класса class Bar::nexted { // ... }; }
У объемлющей функции нет никаких специальных прав доступа к закрытым членам локального класса. Разумеется, это можно обойти, объявив ее другом данного класса. Однако необходимость делать его члены закрытыми вообще сомнительна, поскольку часть программы, из которой разрешается обратиться к нему, весьма ограничена. Локальный класс инкапсулирован в своей локальной области видимости. Дальнейшая инкапсуляция путем сокрытия информации не требуется: вряд ли на практике найдется причина, по которой не все члены локального класса должны быть открыты.
У локального класса, как и у вложенного, ограничен доступ к именам из объемлющей области видимости. Он может обратиться только к именам типов, статических переменных и элементов перечислений, определенных в объемлющих локальных областях. Например:
int a, val; void foo( int val ) { static int si; enum Loc { a = 1024, b }; class Bar { public: Loc locVal; // правильно int barVal; void fooBar ( Loc l = a ) { // правильно: Loc::a barVal = val; // ошибка: локальный объект barVal = ::val; // правильно: глобальный объект barVal = si; // правильно: статический локальный объект locVal = b; // правильно: элемент перечисления } }; // ... }
Имена в теле локального класса разрешаются лексически путем поиска в объемлющих областях видимости объявлений, предшествующих определению такого класса. При разрешении имен, встречающихся в телах его функций-членов, сначала просматривается область видимости класса, а только потом – объемлющие области,
Как всегда, если первое найденное объявление таково, что употребление имени оказывается некорректным, поиск других объявлений не производится. Несмотря на то что использование val в fooBar() выше является ошибкой, глобальная переменная val не будет найдена, если только ее имени не предшествует оператор разрешения глобальной области видимости.
|
Категория: С++ | Добавил: r2d2 (29.09.2011)
|
Просмотров: 963
| Рейтинг: 0.0/0 |
Добавлять комментарии могут только зарегистрированные пользователи. [ Регистрация | Вход ]
|
|
Born in Ussr |
|
|
Залогиниться |
|
|
Турниры |
|
|
|