嵌套类 局部类
《c++ primer 3th》
C++嵌套类
1、 嵌套类的名字只在外围类可见。
2、 类的私有成员只有类的成员和友元可以访问,因此外围类不可以访问嵌套类的私有成员。嵌套类可以访问外围类的成员(通过对象、指针或者引用)。
3、 一个好的嵌套类设计:嵌套类应该设成私有。嵌套类的成员和方法可以设为 public 。
4、 嵌套类可以直接访问外围类的静态成员、类型名( typedef )、枚举值。
嵌套类
一个类可以在另一个类中定义,这样的类被称为嵌套类。嵌套类是其外围类的一个成员。嵌套类的定义可以出现在其外围类的公有、私有或保护区中。
嵌套类的名字在其外围类域中是可见的,但是在其他类域或名字空间中是不可见的,这意味着,嵌套类的名字不会与外围域中声明的相同名字冲突。例如:
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;
};
与非嵌套类一样,嵌套类可以有与自身同样类型的成员:
// Not ideal configuration: evolving class definition
class 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:
// 这个声明是必需的
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
这是因为枚举值可以在定义枚举的域内被直接访问。为什么?因为枚举定义并不像类定义一样维护了自己相关的域。
在嵌套类域中的名字解析
让我们来看看在嵌套类的定义,及其成员定义中的名字解析是怎样进行的。被用在嵌套类的定义中的名字(除了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 引用全局对象:
1、value 和全局对象List 的类型都是int。List::list 成员是指针类型,在没有显式转换的情况它不能被赋给value。
2、不允许ListItem 访问其外围类的私有数据成员,如List。
3、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 的全局声明。
局部类
类也可以定义在函数体内,这样的类被称为局部类。局部类只在定义它的局部域内可见。与嵌套类不同的是,在定义该类的局部域外没有语法能够引用局部类的成员。因此,局部类的成员函数必须被定义在类定义中。在实际中,这就把局部类的成员函数的复杂性限制在几行代码中。否则,对读者来说,代码将变得很难理解。
因为没有语法能够在名字空间域内定义局部类的成员,所以也不允许局部类声明静态数据成员。
在局部类中嵌套的类可以在其类定义之外被定义。但是,该定义必须出现在包含外围局部类定义的局部域内。在局部域定义中的嵌套类的名字必须由其外围类名限定修饰。在外围类中,该嵌套类的声明不能被省略,例如:
void foo( int val )
{
class Bar {
public:
int barVal;
class nested; // 嵌套类的声明是必需的
};
// 嵌套类定义
class Bar::nested {
// ...
};
}
外围函数没有特权访问局部类的私有成员。当然,这可以通过使外围函数成为局部类的友元来实现。但是,看起来,局部类几乎从不需要私有成员。能够访问局部类的程序部分只有很少的一部分。局部类被封装在它的局部域中,通过信息隐藏进一步封装好像有点太过了。在实际中,很难找到一个理由不把局部类的所有成员都声明为公有的。
同嵌套类一样,局部类可以访问的外围域中的名字也是有限的。局部类只能访问在外围局部域中定义的类型名、静态变量以及枚举值,例如:
int a, val;
void foo( int val )
{
static int si;
enum Loc { a = 1024, b };
class Bar {
public:
Loc locVal; // ok;
int barVal;
void fooBar( Loc l = a ) { // ok: Loc::a
barVal = val; // 错误: 局部对象
barVal = ::val; // OK: 全局对象
barVal = si; // ok: 静态局部对象
locVal = b; // ok: 枚举值
}
};
// ...
}
在局部类体内(不包括成员函数定义中的)的名字解析过程是:在外围域中查找出现在局部类定义之前的声明。在局部类的成员函数体内的名字的解析过程是:在查找外围域之前,首先直找该类的完整域。
还是一样,如果先找到的声明使该名字的用法无效,则不考虑其他声明。即使在fooBar()中使用val 是错的,编译器也不会找到全局变量val,除非用全局域解析操作符限定修饰val。