一个类可以在另一个类中定义这样的类被称为嵌套类(nested class)。 嵌套类是其外
围类的一个成员,嵌套类的定义可以出现在其外围类的公有、私有或保护区中。
嵌套类的名字在其外围类域中是可见的但是在其他类域或名字空间中是不可见的。这
意味着嵌套类的名字不会与外围域中声明的相同名字冲突。例如
class Node { /* ... */ };
class Tree {
public:
// Node 被封装在 Tree 的域中
// 在类域中 Tree::Node 隐藏了 ::Node
class Node {...};
// ok: 被解析为嵌套类: Tree::Node
Node *tree;
};
// Tree::Node 在全局域中不可见
// Node 被解析为全局的 Node 声明
Node *pnode;
class List {
public:
// Node 被封装在 List 的域中
// 在类域 List::Node 中隐藏了 ::Node
class Node {...};
// ok: 解析为: List::Node
Node *list;
};{
public:
class ListItem {
friend class List; // 友元声明
ListItem( int val = 0 ); // 构造函数
ListItem *next; // 指向自己类的指针
int value;
};
// ...
private:
ListItem *list;
ListItem *at_end;
};
私有成员是指这样的成员:它只能在该类的成员或友元定义中被访问,除非外围类被声
明为嵌套类的友元,否则它没有权利访问嵌套类的私有成员,这就是为什么ListItem 把List
声明为友元的原因:为了允许类List 的成员定义访问ListItem 的私有成员。嵌套类也没有任
何特权访问其外围类的私有成员,如果我们想授权ListItem 允许它访问类List 的私有成员
那么该外围类List 必须把嵌套类ListItem 声明为友元。在前面的例子中ListItem 不是List 的
友元所以它不能引用List 的私有成员。
把类ListItem 声明为List 类的公有成员意味着该嵌套类可以在整个程序中在类List
的友元和成员定义之外用作类型例。如
// ok: 全局域中的声明
List::ListItem *headptr;
这超出了我们的本意。嵌套类ListItem 支持List 类的抽象我们不希望让ListItem 类型
在整个程序中都可以被访问,那么比较好的设计是把ListItem 嵌套类定义为类List 的私有
成员
// 不理想的配置: 要改进的类定义
class List {
public:
// ...
private:
class ListItem {
// ...
};
ListItem *list;
ListItem *at_end;
};
现在只有List 的成员和友元的定义可以访问类型ListItem 。把类ListItem 的所有成员都声明为公有的也不再有任何坏处,因为ListItem 类是List 的私有成员所以只有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 的定义中没有把构造函数定义为inline, 内联的构造函数必须在类定义
之外,被定义在哪儿可以定义它呢?ListItem 的构造函数不是类List 的成员所以我们不能
在类List 的体内定义ListItem 的构造函数,必须被定义在全局域中——该域含有其外围类的
定义。当我们没有在嵌套类体内以inline 形式定义嵌套类的成员函数时我们就必须在最外
围的类之外定义这些成员函数
下面是ListItem 构造函数的一种可能的定义,但是对于全局域定义的语法来说这是不
正确的
class List {
public:
// ...
private:
class ListItem {
public:
ListItem( int val = 0 );
// ...
};
};
// 错误: ListItem 不在全局域中
ListItem::ListItem( int val ) { ... }
问题在于名字ListItem 在全局域中是不可见的。在全局域中使用ListItem 必须指明
ListItem 是类List 中嵌套的类。可以通过用其外围类名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 的私有成员也可以被定义在全局域中
嵌套类也可以被定义在其外围类之外,例如Lisiltem 的定义也可以在全局域中被给出
如下
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 的名字限定修饰。注意在
类List 体内的ListItem 的声明不能省略,如果嵌套类没有先被声明为其外围类的一个成员
则全局域中的定义不能被指定为嵌套类,在全局域中定义的嵌套类不一定是其外围类的公有
成员,
在嵌套类的定义被看到之前我们只能声明嵌套类的指针和引用,即使类ListItem 是在
全局域中被定义的,List 的数据成员list 和at_end 仍然是有效的因为这两个成员都是指针。
如果这两个成员中有一个是对象而不是指针,那么类List 的成员声明将会引发一个编译错误
例如
class List {
public:
// ...
private:
// 这个声明是必需的
与非嵌套类一样嵌套类可以有与自身同样类型的成员
// Not ideal configuration: evolving class definition
class List
class ListItem;
ListItem *list;
ListItem at_end; // 错误: 未定义嵌套类 ListItem
};
为什么会希望在类定义之外定义嵌套类呢?或许嵌套类支持外围类的实现细节,我们不
想让List 类的用户看到ListItem 的细节,因此我们不愿把嵌套类的定义放在含有List 类接
口的头文件中,于是我们只能在含有List 类及其成员实现的文本文件中给出嵌套类ListItem
的定义。
嵌套类可以先被声明然后再在外围类体中被定义。这允许多个嵌套类具有互相引用的
成员。例如
class List {
public:
// ...
private:
// List::ListItem 的声明
class ListItem;
class Ref {
ListItem *pli; // pli 类型为: List::ListItem*
};
// List::ListItem 的定义
class ListItem {
Ref *pref; // pref 的类型为: List::Ref*
};
};
如果类ListItem 没有在类Ref 之前先被声明,那么成员pli 的声明就是错的。因为名字
ListItem 没有被声明。
嵌套类不能直接访问其外围类的非静态成员,即使这些成员是公有的。任何对外围类的
非静态成员的访问都要求通过外围类的指针引用或对象来完成。例如
class List {
public:
int init( int );
private:
class ListItem {
public:
ListItem( int val = 0 );
void mf( const List & );
int value;
int memb;
};
};
List::ListItem::ListItem( int val )
{
// List::init() 是类 List 的非静态成员
// 必须通过 List 类型的对象或指针来使用
value = init( val ); // 错误: 非法使用 init
}
使用类的非静态成员时编译器必须能够识别出非静态成员属于哪个对象,在类ListItem
的成员函数中this 指针只能被隐式地应用在类ListItem 的成员上,而不是外围类的成员上。
由于隐式的this 指针,我们知道数据成员value 指向被凋用构造函数的对象。在ListItem 的
构造函数中的this 指针的类型是ListItem* ,而要访问成员init()所需的是List 类型的对象或
List*类型的指针
下面是成员函数mf()通用引用参数引用init() 从这里我们能够知道成员init()是针对
函数实参指定的对象而被调用的。
void List::ListItem::mf( const List &il ) {
memb = il.init(); // ok: 通过引用调用 init()
}
尽管访问外围类的非静态数据成员需要通过对象指针或引用才能完成,但是嵌套类可
以直接访问外围类的静态成员类型名、枚举值。假定这些成员是公有的,类型名是一个
typedef 名字,枚举类型名或是一个类名。例如
class List {
public:
typedef int (*pFunc)();
enum ListStatus { Good, Empty, Corrupted };
// ...
private:
class ListItem {
public:
void check_status();
ListStatus status; // ok
pFunc action; // ok
// ...
};
// ...
};
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; // ok
List::ListStatus stat = List::Empty; // ok
当引用一个枚举值时我们不能写
List::ListStatus::Empty
这是因为枚举值可以在定义枚举的域内被直接访问。为什么?因为枚举定义并不像类定
义一样维护了自己相关的域。
13.10.1 在嵌套类域中的名字解析
让我们来看看在嵌套类的定义及其成员定义中的名字解析是怎样进行的。
被用在嵌套类的定义中的名字除了inline 成员函数定义中的名字和缺省实参的名字之
外其解析过程如下
1 考虑出现在名字使用点之前的嵌套类的成员声明
2 如果第1 步没有成功则考虑出现在名字使用点之前的外围类的成员声明
3 如果第2 步没有成功则考虑出现在嵌套类定义之前的名字空间域中的声明
例如
enum ListStatus { Good, Empty, Corrupted };
class List {
public:
// ...
private:
class ListItem {
public:
// 查找:
// 1) 在 List::ListItem 中
// 2) 在 List 中
// 3) 在全局域中
ListStatus status; // 引用全局枚举
// ...
};
// ...
};
编译器首先在类ListItem 的域中查找ListStatus 的声明,因为没有找到成员声明,所以编
译器接着在类List 的域中查找ListStatus 的声明,因为在List 类中也没有找到声明,于是编
译器在全局域中查找ListStatus 的声明,在这三个域中只有位于ListStatus 使用点之前的声
明才会被编译器考虑,编译器找到了全局枚举ListStatus 的声明它是被用在Status 声明中的
类型
如果在全局域中在外围域List 之外定义嵌套类ListItem 则List 类的所有成员都已经
被声明完毕因而编译器将考虑其所有声明
class List {
private:
class ListItem;
// ...
public:
enum ListStatus { Good, Empty, Corrupted };
// ...
};
class List::ListItem {
public:
// 查找: // 1) 在 List::ListItem 中
// 2) 在 List 中
// 3) 在全局域中
ListStatus status; // List::ListStatus
// ...
};
ListItem的名字解析过程首先在类ListItem 的域中开始查找,因为没有找到成员声明
所以编译器在类List 的域内查找ListStatus 的声明,因为类List 的完整定义都已经能够看得
到,所以这一步查找考虑List 的所有成员,于是找到List 中嵌套的enumListStatus 尽管它
是在ListItem 之后被声明的,status 是List 的ListStatus 类型的一个枚举对象,如果List 没有
名为ListStatus 的成员则名字查找过程会在全局域中在嵌套类ListItem 定义之前查找声明
被用在嵌套类的成员函数定义中的名字其解析过程如下:
1 首先考虑在成员函数局部域中的声明
2 如果第1 步没有成功则考虑所有嵌套类成员的声明
3 如果第2 步没有成功则考虑所有外围类成员的声明
4 如果第3 步没有成功则考虑在成员函数定义之前的名字空间域中出现的声明
在下面的代码段中成员函数check_status()定义中的list 引用了哪个声明
class List {
public:
enum ListStatus { Good, Empty, Corrupted };
// ...
private:
class ListItem {
public:
void check_status();
ListStatus status; // ok
// ...
};
ListItem *list;
// ...
};
int list = 0;
void List::ListItem::check_status()
{
int value = list; // 哪个 list?
}
很有可能程序员想让check_status()中的List 引用全局对象。
value 和全局对象List 的类型都是int List::list 成员是指针类型在没有显式转换的
情况它不能被赋给value
不允许ListItem 访问其外围类的私有数据成员如List
list 是一个非静态数据成员,在ListItem 的成员函数中必须通过对象指针或引用
来访问它。
但是尽管有这些原因,在成员check_status()中用到的名字List 仍被解析为类List 的数
据成员list 。记住如果在联套类ListItem 的域中没有找到该名字则在查找全局域之前下一个要查找的是其外围类的域外围类List 的成员list 隐藏了全局域中的对象,于是产生一
个错误消息,因为在check_status()中使用指针list 是无效的
只有在名字解析成功之后编译器才会检查访问许可和类型兼容性如果名字的用法本
身就是错误的则名字解析过程将不会再去查找更适合于该名字用法的声明而是产生一个
错误消息
为了访问全局对象list 必须使用全局域解析操作符
void List::ListItem:: check_status() {
value = ::list; // ok
}
如果成员函数check_status()被定义成位于ListItem 类体中的内联函数,则上面所讲到的
最后一步修改会使编译器产生一个错误消息报告说全局域中的list 没有被声明。
class List {
public:
// ...
private:
class ListItem {
public:
// 错误: 没有可见的 ::list 声明
void check_status() { int value = ::list; }
// ...
};
ListItem *list;
// ...
};
int list = 0;
全局对象list 是在类List 定义之后被声明的,对于在类体中内联定义的成员函数只考
虑在外围类定义之前可见的全局声明,如果check_status()的定义出现在List 的定义之后则
编译器考虑在check_status()定义之前可见的全局声明于是找到对象list 的全局声明。