Язык программирования C++. Вводный курс. С для начинающих
Скачать 5.41 Mb.
|
646 }; Если бы ListItem не был объявлен перед определением класса Ref, то объявление члена pli было бы ошибкой. Вложенный класс не может напрямую обращаться к нестатическим членам объемлющего, даже если они открыты. Любое такое обращение должно производиться через указатель, ссылку или объект объемлющего класса. Например: }; При использовании нестатических членов класса компилятор должен иметь возможность идентифицировать объект, которому принадлежит такой член. Внутри функции-члена класса ListItem указатель this неявно применяется лишь к его членам. Благодаря неявному this мы знаем, что член value относится к объекту, для которого вызван конструктор. Внутри конструктора ListItem указатель this имеет тип ListItem*. Для доступа же к функции-члену init() нужен объект типа List или указатель типа List*. Следующая функция-член mf() обращается к init() с помощью параметра-ссылки. Таким образом, init() вызывается для объекта, переданного в аргументе функции: class List { public: // ... private: // объявление List::ListItem class ListItem; class Ref { // pli имеет тип List::ListItem* ListItem *pli; }; определение List::ListItem class ListItem { // pref имеет тип List::Ref* Ref *pref; }; 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 С++ для начинающих 647 } Хотя для доступа к нестатическим членам объемлющего класса нужен объект, указатель или ссылка, к статическим его членам, именам типов и элементам перечисления вложенный класс может обращаться напрямую (если, конечно, эти члены открыты). Имя типа – это либо имя typedef, либо имя перечисления, либо имя класса. Например: }; pFunc , ListStatus и ListItem – все это вложенные имена типов в области видимости объемлющего класса List. К ним, а также к элементам перечисления ListStatus можно обращаться в области видимости класса ListItem даже без квалификации: } Вне области видимости ListItem и List при обращении к статическим членам, именам типов и элементам перечисления объемлющего класса требуется оператор разрешения области видимости: List::ListStatus stat = List::Empty; // правильно При обращении к элементам перечисления мы не пишем: List::ListStatus::Empty void List::ListItem::mf( List &i1 ) { memb = i1.init(); // правильно: обращается к init() по ссылке class List { public: typedef int (*pFunc)(); enum ListStatus { Good, Empty, Corrupted }; //... private: class ListItem { public: void check_status(); ListStatus status; // правильно pFunc action; // правильно // ... }; // ... void List::ListItem::check_status() { ListStatus s = status; switch ( s ) { case Empty: ... case Corrupted: ... case Good: ... } List::pFunc myAction; // правильно С++ для начинающих 648 поскольку они доступны непосредственно в той области видимости, в которой определено само перечисление. Почему? Потому что с ним, в отличие от класса, не связана отдельная область. 13.10.1. Разрешение имен в области видимости вложенного класса Посмотрим, как разрешаются имена в определениях вложенного класса и его членов. Имя, встречающееся в определении вложенного класса (кроме тех, которые употребляются во встроенных функциях-членах и аргументах по умолчанию) разрешается следующим образом: 1. Просматриваются члены вложенного класса, расположенные перед употреблением имени. 2. Если шаг 1 не привел к успеху, то просматриваются объявления членов объемлющего класса, расположенные перед употреблением имени. 3. Если и этого недостаточно, то просматриваются объявления, расположенные в области видимости пространства имен перед определением вложенного класса. Например: }; Сначала компилятор ищет объявление ListStatus в области видимости класса ListItem . Поскольку его там нет, поиск продолжается в области видимости List, а затем в глобальной. При этом во всех трех областях просматриваются только объявления, предшествующие использованию ListStatus. В конце концов находится глобальное объявление перечисления ListStatus – оно и будет типом, использованным в объявлении status. Если вложенный класс ListItem определен в глобальной области видимости, вне тела объемлющего класса List, то все члены List уже были объявлены: enum ListStatus { Good, Empty, Corrupted }; class List { public: // ... private: class ListItem { public: // Смотрим в: // 1) List::ListItem // 2) List // 3) глобальной области видимости ListStatus status; // относится к глобальному перечислению // ... }; // ... С++ для начинающих 649 }; При разрешении имени ListStatus сначала просматривается область видимости класса ListItem . Поскольку там его нет, поиск продолжается в области видимости List. Так как полное определение класса List уже встречалось, просматриваются все члены этого класса. Вложенное перечисление ListStatus найдено несмотря даже на то, что оно объявлено после объявления ListItem. Таким образом, status объявляется как указатель на данное перечисление в классе List. Если бы в List не было члена с таким именем, поиск был бы продолжен в глобальной области видимости среди тех объявлений, которые предшествуют определению класса ListItem. Имя, встретившееся в определении функции-члена вложенного класса, разрешается следующим образом: 1. Сначала просматриваются локальные области видимости функции-члена. 2. Если шаг 1 не привел к успеху, то просматриваются объявления всех членов вложенного класса. 3. Если имя еще не найдено, то просматриваются объявления всех членов объемлющего класса. 4. Если и этого недостаточно, то просматриваются объявления, появляющиеся в области видимости пространства имен перед определением функции-члена. Какое объявление относится к имени list в определении функции-члена check_status() в следующем фрагменте кода: class List { private: class ListItem { //... public: enum ListStatus { Good, Empty, Corrupted }; // ... }; class List::ListItem { public: // Смотрим в: // 1) List::ListItem // 2) List // 3) глобальной области видимости ListStatus status; // относится к глобальному перечислению // ... С++ для начинающих 650 } Весьма вероятно, что при использовании 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 следует использовать оператор разрешения области видимости: } Если бы функция-член check_status() была определена как встроенная в теле класса ListItem , то последнее объявление привело бы к выдаче сообщения об ошибке из-за того, что имя list не объявлено в глобальной области видимости: 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? void List::ListItem::check_status() { int value = ::list; // правильно С++ для начинающих 651 int list = 0; Глобальный объект list объявлен после определения класса List. Во встроенной функции-члене, определенной внутри тела класса, рассматриваются только те глобальные объявления, которые были видны перед определением объемлющего класса. Если же определение check_status() следует за определением List, то рассматриваются глобальные объявления, расположенные перед ним, поэтому будет найдено глобальное определение объекта list. Упражнение 13.21 В главе 11 был приведен пример программы, использующей класс iStack. Измените его, объявив классы исключений pushOnFull и popOnEmpty открытыми вложенными в iStack . Модифицируйте соответствующим образом определение класса iStack и его функций-членов, а также определение main(). 13.11. Классы как члены пространства имен A Представленные до сих пор классы определены в области видимости глобального пространства имен. Но их можно определять и в объявленных пользователем пространствах. Имя класса, определенного таким образом, доступно только в области видимости этого пространства, т.е. оно не конфликтует с именами, объявленными в других пространствах имен. Например: Node another; // cplusplus_primer::Node class List { public: // ... private: class ListItem { public: // ошибка: нет видимого объявления для ::list void check_status() { int value = ::lis; } //... }; ListItem *list; // ... }; namespace cplusplus_primer { class Node { /* ... */ }; } namespace DisneyFeatureAnimation { class Node { /* ... */ }; } Node *pnode; // ошибка: Node не видно в глобальной области видимости // правильно: объявляет nodeObj как объект // квалифицированного типа DisneyFeatureAnimation::Node DisneyFeatureAnimation::Node nodeObj; // using- объявление делает Node видимым в глобальной области видимости using cplusplus_primer::Node; С++ для начинающих 652 Как было показано в двух предыдущих разделах, член класса (функция-член, статический член или вложенный класс) может быть определен вне его тела. Если мы реализуем библиотеку и помещаем определения наших классов в объявленное пользователем пространство имен, то где расположить определения членов, находящиеся вне тел своих классов? Их можно разместить либо в пространстве имен, которое содержит определение самого внешнего класса, либо в одном из объемлющих его пространств. Это дает возможность организовать код библиотеки следующим образом: int cplusplus_primer::List::ListItem::action() { } Члены вложенного класса ListItem можно определить в пространстве имен cplusplus_primer , которое содержит определение List, или в глобальном пространстве, включающем определение cplusplus_primer. В любом случае имя члена в определении должно быть квалифицировано именами объемлющих классов и объявленных пользователем пространств, вне которых находится объявление члена. Как происходит разрешение имени в определении члена, которое находится в объявленном пользователем пространстве? Например, как будет разрешено someVal: } Сначала просматриваются локальные области видимости в определении функции-члена, затем поиск продолжается в области видимости ListItem, затем – в области видимости List . До этого момента все происходит так же, как в процессе разрешения имен, описанном в разделе 13.10. Далее просматриваются объявления из пространства cplusplus_primer и наконец объявления в глобальной области видимости, причем во // --- 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() { int local = someVal; // ... С++ для начинающих 653 внимание принимаются только те, которые расположены до определения функции-члена action() : } Определение пространства имен cplusplus_primer не является непрерывным. Определения класса List и объекта someVal размещены в первом его разделе, который находится в заголовочном файле primer.h. Определение функции calc() появляется в определении пространства имен, расположенном в файле реализации primer.C. Использование calc() внутри action() ошибочно, так как она объявлена после использования. Если calc() – часть интерфейса cplusplus_primer, ее следовало бы объявить в той части данного пространства, которая находится в заголовочном файле: } Если же calc() используется только в action() и не является частью интерфейса пространства имен, то ее нужно объявить перед action(), чтобы можно было ссылаться на нее внутри определения 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) { } // ... // --- primer.h --- namespace cplusplus_primer { class List { // ... } const int someVal = 365; double calc(int); С++ для начинающих 654 Здесь прослеживается аналогия с процессом поиска объявлений в глобальной области видимости, о котором мы говорили в предыдущих разделах: объявления, предшествующие определению члена, принимаются во внимание, тогда как следующие за ним игнорируются. Довольно просто запомнить, в каком порядке просматриваются области видимости при поиске имени из определения функции, расположенного вне определения класса. Имена, которыми квалифицировано имя члена, указывают порядок рассмотрения пространств. Например, имя 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: } а сам iStack – членом пространства имен Container. Модифицируйте соответствующим образом определение данного класса и его функций-членов, а также определение main(). namespace LibException { class pushOnFull{ }; class popOnEmpty{ }; С++ для начинающих 655 13.12. Локальные классы A Класс, определенный внутри тела функции, называется локальным. Он виден только в той локальной области, где определен. Не существует синтаксиса, позволяющего обратиться к члену такого класса, в отличие от вложенного, извне локальной области видимости, содержащей его определение. Поэтому функции-члены локального класса должны определяться внутри определения самого класса. На практике это ограничивает их сложность несколькими строками кода; помимо всего прочего, такой код становится трудно читать. Поскольку невозможно определить член локального класса в области видимости пространства имен, то в таком классе не бывает статических членов. Класс, вложенный в локальный, может быть определен вне определения объемлющего класса, но только в локальной области видимости, содержащей это определение. Имя вложенного класса в таком определении должно быть квалифицировано именем объемлющего класса. Объявление вложенного класса в объемлющем нельзя опускать: } У объемлющей функции нет никаких специальных прав доступа к закрытым членам локального класса. Разумеется, это можно обойти, объявив ее другом данного класса. Однако необходимость делать его члены закрытыми вообще сомнительна, поскольку часть программы, из которой разрешается обратиться к нему, весьма ограничена. Локальный класс инкапсулирован в своей локальной области видимости. Дальнейшая инкапсуляция путем сокрытия информации не требуется: вряд ли на практике найдется причина, по которой не все члены локального класса должны быть открыты. У локального класса, как и у вложенного, ограничен доступ к именам из объемлющей области видимости. Он может обратиться только к именам типов, статических переменных и элементов перечислений, определенных в объемлющих локальных областях. Например: void foo( int val ) { class Bar { public: int barVal; class nested; // объявление вложенного класса обязательно }; // определение вложенного класса class Bar::nexted { // ... }; |