每一个类都会定义它本身的做用域。在类的做用域以外,普通的数据和函数成员只能由对象、引用或者指针使用成员访问运算符来访问。对于类类型成员则使用做用域运算符访问。不论哪一种状况,跟在运算符以后的名字都必须是对应类的成员:函数
Screen::pos ht=24,wd=80; //使用Screen定义的pos类型this
Screen scr(ht,wd,' ');spa
Screen *p=&scr;指针
char c=scr.get(); //访问scr对象的get成员code
c=p->get(); //访问p所指对象的get成员对象
做用域和定义在类外部的成员blog
一个类就是一个做用域的事实可以很好地解释为何当咱们在类的外部定义成员函数时必须同时提供类名和函数名。在类的外部,成员的名字被隐藏起来了。作用域
一旦遇到了类名,定义的剩余部分就在类的做用域以内了,这里的剩余部分包括参数列表和函数体。结果就是,咱们能够直接使用类的其余成员而无须再次受权了。get
例如,Window_mgr类的clear成员,该函数的参数用到了Window_mgr类定义的一种类型:编译器
void Window_mgr::clear(ScreenIndex i) { Screen &s=screens[i]; s.contents=string(s.height*s.width,' '); }
由于编译器在处理参数列表以前已经明确了咱们当前正位于Window_mgr类的做用域中,全部咱们没必要再专门说明ScreenIndex是Window_mgr类定义的。出于一样的缘由,编译器也能知道函数体中用到的screens也是在Window_mgr类中定义的。
另外一方面,函数的返回类型一般出如今函数名以前。所以当成员函数定义在类的外部时,返回类型中使用的名字都位于类的做用域以外。这时,返回类型必须指明它是哪一个类的成员。例如,咱们可能想Window_mgr类添加一个新的名为addScreen的函数,它负责先显示器添加一个新的屏幕。这个成员的返回类型将是ScreenIndex,用户能够经过它定位到指定的Screen:
class Window_mgr{ public: //向窗口添加一个Screen,返回它的编号 SceenIndex addScreen(const Screen&); }; //首先处理返回类型,以后咱们才进入Window_mgr的做用域 Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s) { screens.push_back(s); return screens.size()-1; }
由于返回类型出如今类名以前,全部事实上它是位于Window_mgr类的做用域以外的。在这种状况下,要想使用ScreenIndex做为返回类型,咱们必须明确指定哪一个类定义了它。
名字查找与类的做用域
到目前为止,咱们编写的程序中,名字查找的过程比较直截了当:
对于定义的类内部的成员函数来讲,解析其中名字的方式与上述的查找规则有所区别。类的定义分两步处理:
编译器处理完类中的所有声明后才会处理成员函数的定义。
按照这种两阶段的方式处理类能够简化类代码的组织方式。由于成员函数体直到整个类能够后才会被处理,因此它能使用类中定义的任何名字。相反,若是函数的定义和成员的声明被同时处理,那么咱们将不得不在成员函数中只使用那些已经出现的名字。
用于类成员声明的名字查找
这种两阶段的处理方式只适用于成员函数中使用的名字。声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见。若是某个成员的声明使用了类中还没有出现的名字,则编译器将会在定义该类的做用域中继续查找。例如:
typedef double Money; string bal; class Account{ public: Money balance() { return bal;} private: Money bal; //... };
当编译器看到balance函数的声明语句时,它将在Account类的范围内寻找对Money的声明。编译器只考虑Account中在使用Money前出现的声明,由于没找到匹配的成员,因此编译器会接着到Account的外层做用域中查找。在这个例子中,编译器会找到Money的typedef语句,该类型被这样balance函数的返回类型以及数据成员bal的类型。另外一方面,balance函数体在整个类可见后才被处理,所以,该函数的return语句返回名为bal的成员,而非外层做用域的string对象。
类型名要特殊处理
通常来讲,内层做用域能够从新定义外层做用域中的名字,即便该名字已经在内层做用域中使用过。然而在类中,若是成员使用了外层做用域中的某个名字,而该名字表明一种类型,则类不能在以后从新定义该名字:
typedef double Money; string bal; class Account{ public: Money balance() { return bal;} //使用外层做用域的Money private: typedef double Money; //错误:不能从新定义Money Money bal; //... };
须要特别注意的是,即便Account中定义的Money类型与外层做用域一致,上述代码仍然是错误的。
尽管从新定义类型名字是一种错误的行为,可是编译器并不为此负责。一些编译器仍将顺序经过这样的代码,而忽略代码有错的事实。
类型名的定义一般出如今类的开始处,这样就能确保全部使用该类型的成员都出如今类名的定义以后。
成员定义中的普通块做用域的名字查找
成员函数中使用的名字按照以下方式解析:
通常来讲,不建议使用其余成员的名字做为某个成员函数的参数。不过为了更好的理解名字的解析过程,咱们不妨在dummy_fcn函数中暂时违反一下这约定:
//一般状况下不建议为参数和成员使用一样的名字
int height; //定义了一个名字,稍后将在Screen中使用
class Screen{
public:
typedef string::size_type pos;
void dump_fcn(pos height){
cursor=width*height; //哪一个height?是哪一个参数
}
private:
pos cursor=0;
pos height=0,width=0;
};
当编译器处理dummy_fcn中的乘法表达式时,它首先在函数做用域内查找表达式中用到的名字,函数的参数位于函数做用域内,所以,dummy_fcn函数体内用到的名字height指的是参数声明。
在此例中,height参数隐藏了同名的成员。若是想绕开上面的查找规则,应该将代码变为:
//不建议的写法:成员函数中的名字不该该隐藏同名的成员
void Screen::dummy_fcn (pos height){
cursor=width*this->height; //成员height
//另一种表示该成员的方式
cursor=width*Screen::height; //成员height
}
尽管类的成员被隐藏了,但咱们仍然能够经过加上类的名字或显式地使用this指针来强制访问成员。
其实最好的确保咱们使用height成员的方法是给参数起个其余的名字:
//建议的写法:不要把成员名字做为参数或其余局部变量使用
void Screen::dummy_fcn(pos ht){
cursor=width*height; //成员height
}
在此例中,当编译器查找名字height时,显然在dummy_fcn函数内部是找不到的。编译器接着会在Screen内查找匹配的声明,即便height的声明出如今dummy_fcn使用它以后,编译器也能正确地解析函数使用的是名为height的成员。
类做用域以后,在外围的做用域中查找
若是编译器在函数和类的做用域中都没有找到名字,它将接着在外围的做用域中查找。在咱们的例子中,名字height定义在外层的做用域中,且位于Screen的定义以前。然而,外层做用域中的对象被名为height的成员隐藏了。所以,若是咱们须要的是外层做用域中的名字,能够显式地经过做用域运算符来进行请求:
//不建议的写法:不要隐藏外层做用域中可能被用到的名字
void Screen::dummy_fcn(pos ht){
cursor=width*::height; //成员height
}
尽管外层的对象被隐藏了,但咱们仍然能够用做用域运算符访问它。
在文件中名字的出现处对其进行解析
当成员定义在类的外部时,名字查找的第三步不只要考虑类定义以前的全局做用域中的声明,还须要考虑在成员函数定义以前的全局做用域中的声明。例如:
int height; //定义了一个名字,稍后将在Screen中使用 class Screen{ public: typedef string::size_type pos; void setHeight(pos); pos height=0; //隐藏了外层做用域中的height }; Screen::pos verify(Screen::pos); void Screen::setHeight(pos var) { //var: 参数 //height:类的成员 //verify:全局函数 height=verify(var); }
请注意,全局函数verify的声明在Screen类的定义以前是不可见的。然而,名字查找的第三步包括了成员函数出现以前的全局做用域。在此例中,verify的声明位于setHeight的定义以前,所以能够被正常使用。