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

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

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

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

642

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 означает, что вложенный класс можно использовать как тип во всей программе, в том числе и за пределами определений членов и друзей класса. Например:

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

643

// правильно: объявление в глобальной области видимости

List::ListItem *headptr;

Это дает более широкую область видимости, чем мы планировали. Вложенный ListItem поддерживает абстракцию класса List и не должен быть доступен во всей программе.

// Не идеально, будем улучшать class List {

public: // ...

private:

class ListItem { // ...

};

ListItem *list; ListItem *at_end;

Поэтому лучше объявить вложенный класс ListItem закрытым членом List:

};

Теперь тип ListItem доступен только из определений членов и друзей класса List, поэтому все члены класса ListItem можно сделать открытыми. При таком подходе объявление List как друга ListItem становится ненужным. Вот новое определение

// так лучше class List { public:

//...

private:

//Теперь ListItem закрытый вложенный тип class ListItem {

//а его члены открыты

public:

ListItem( int val=0 ); ListItem *next;

int value;

};

ListItem *list; ListItem *at_end;

класса List:

};

Конструктор ListItem не задан как встроенный внутри определения класса и, следовательно, должен быть определен вне него. Но где именно? Конструктор класса ListItem не является членом List и, значит, не может быть определен в теле последнего; его нужно определить в глобальной области видимости той, которая содержит определение объемлющего класса. Когда функция-член вложенного класса не определяется как встроенная в теле, она должна быть определена вне самого внешнего из объемлющих классов.

Вот как могло бы выглядеть определение конструктора ListItem. Однако показанный ниже синтаксис в глобальной области видимости некорректен:

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

644

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 могло бы находиться и в глобальной области видимости:

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

645

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 и его членов.

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

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

646

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() вызывается для объекта, переданного в аргументе функции:

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

647

 

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

// ...

};

// ...

типа это либо имя typedef, либо имя перечисления, либо имя класса. Например:

};

pFunc, ListStatus и ListItem все это вложенные имена типов в области видимости объемлющего класса List. К ним, а также к элементам перечисления ListStatus можно

void List::ListItem::check_status()

{

ListStatus s = status;

switch ( s ) {

case Empty: ...

case Corrupted: ...

case Good: ...

}

обращаться в области видимости класса ListItem даже без квалификации:

}

Вне области видимости ListItem и List при обращении к статическим членам, именам

типов и элементам перечисления объемлющего класса требуется оператор разрешения

List::pFunc myAction; // правильно

области видимости:

List::ListStatus stat = List::Empty; // правильно

При обращении к элементам перечисления мы не пишем:

List::ListStatus::Empty

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

648

поскольку они доступны непосредственно в той области видимости, в которой определено само перечисление. Почему? Потому что с ним, в отличие от класса, не связана отдельная область.

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

Посмотрим, как разрешаются имена в определениях вложенного класса и его членов.

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

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

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

3.Если и этого недостаточно, то просматриваются объявления, расположенные в области видимости пространства имен перед определением вложенного класса.

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 уже были объявлены:

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

649

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() в следующем фрагменте кода:

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

650

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 не объявлено в глобальной области видимости:

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

651

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 не видно в глобальной области видимости

Node *pnode;

//правильно: объявляет nodeObj как объект

//квалифицированного типа DisneyFeatureAnimation::Node DisneyFeatureAnimation::Node nodeObj;

//using-объявление делает Node видимым в глобальной области видимости using cplusplus_primer::Node;

других пространствах имен. Например:

Node another;

// cplusplus_primer::Node