【C++】类(四):类的作用域
7.4 类的作用域
每个类都会定义它自己的作用域。在类的作用域之外,普通的数据和函数成员只能由对象、引用或者指针使用成员访问运算符来访问。对于类类型成员则使用作用域运算符访问。无论哪种情况,跟在运算符之后的名字都必须是对应类的成员:
Screen::pos ht = 24, wd = 80; // 使用 Screen 定义的 pos 类型
Screen scr(ht, wd, ' ');
Screen *p = &scr;
char c = scr.get(); // 访问 src 对象的 get 成员
c = p -> get(); // 访问 p 所指对象的 get 成员
作用域和定义在类外部的成员
在类的外部,成员的名字被隐藏起来了。
而一旦遇到了类的类名,并使用了作用域访问符,定义在类外的成员函数定义的剩余部分则又回到了类的作用域当中,比如之前我们在类外定义的 Window_mgr 类的 clear 成员函数:
void Window_mgr::clear(ScreenIndex i) {
Screen &s = screens[i];
s.contents = string(s.height * s.width, ' ');
}
编译器在处理参数列表之前已经明确了我们当前正位于 Window_mgr 类的作用域当中(因为施加了作用域访问符),因此后续 clear 成员函数的定义的作用域和 Window_mgr 类的作用域是相同的。
此外,函数的返回类型通常出现在函数名之前。因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。此时,返回类型必须指明它是哪个类的成员(也很好理解,更恰当的描述是,如果我们在类内使用typedef
或using
定义了某个类型的别名,并且恰好有若干个成员函数以类内自定义类型作为返回值的类型,由于类内定义的类型对于类外是不可见的,如果成员函数定义在类外,则应该在返回值之前施加对应类的类名和访问符,否则将会编译失败)。一个例子如下,我们想要为 Window_mgr 类添加一个名为 addScreen 的函数:
class Window_mgr {
public:
// 向窗口添加一个 Screen, 返回它的编号
ScreenIndex addScreen(const Screen&);
}
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s) {
Screens.push_back(s);
retunr screens.size() - 1;
} // 使用类类型 + 作用域访问符来指定函数头的返回类型
7.4.1 名字查找与类的作用域
直到目前,我们编写的程序当中的**名字查找(name lookup)**还较为直截了当:
- 首先,在名字所在的块中寻找其声明语句,只考虑在名字的使用之前出现的声明;
- 如果没找到,继续查找外层作用域;
- 如果没有找到对应的匹配,程序报错;
对于定义在类内部的成员函数而言,解析其中名字的方式与上述查找规则有所出入。类的定义分为两步处理:
- 首先,编译成员的声明;
- 直到类全部可见后才编译函数体;
编译器处理完类中的全部声明后才会处理成员函数的定义。
用于类成员声明的名字查找
上述两阶段的处理方式只适用于成员函数中使用的名字。如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续查找。
例如:
typedef double Money;
string bal;
class Account {
public:
Money balance() { return bal; }
private:
Money bal;
};
在上述例子当中,编译器会找到 Money 的 typedef 语句,该类型被用作 balance 成员函数的返回类型以及数据成员 bal 的类型。此外,balance 函数体在整个类可见后才会被处理,因此该函数的 return 语句返回名为 bal 的成员,而非类定义外部的类型为 string 的对象。
类型名要特殊处理
在类中,如果成员使用了外部作用域当中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字:
typedef double Money;
class Account {
public:
Money balance() { return bal; }
private:
typedef double Money; // 错误❌, 不能重新定义外部已经定义过且在类当中使用的Money类型
Money bal;
};
注意,某些编译器仍然可以顺利编译上述的代码,但根据 C++ Primer 所提到的,代码本身实际上是错误的。
类型名的定义通常出现在类的开始处,这样能确保所有使用该类型的成员都出现在类名的定义之后。
成员定义中的普通块作用域的名字查找
成员函数当中使用的名字按如下方式解析:
- 首先,在成员函数内查找该名字的声明。和之前类似,只有在函数使用之前出现的声明才会被考虑;
- 如果在成员函数内没有找到,则继续在类内查找,这时类内的所有成员都考虑;
- 如果类内也没找到对应的声明,则在成员函数定义之前的作用域继续查找;
一般不建议使用其它成员的名字作为某个成员函数的参数。(即,必要把成员的名字作为参数或其它局部变量来使用)
类作用域之后,在外围的作用域中查找
如果恰好在我们的成员函数定义中需要使用一个与当前类中某个成员名字相同的外部变量,由于类内的名字查找会覆盖掉外部作用域中相应的对象,因此我们需要显式地使用作用域运算符来指定我们想要使用的外部变量。
在文件中名字的出现处对其进行解析
名字查找的第三步不仅要考虑类定义之前的全局作用域中的声明,还需要考虑在成员函数定义之前的(而不是在类定义之前,因为类的定义中可能只包含成员函数的声明,而成员函数的定义出现在类外的某个位置)全局作用域中的声明:
int height;
class Screen {
public:
typedef std::string::size_type pos;
void setHeight(pos);
pos height = 0;
};
Screen::pos Verify(Screen::pos);
void Screen::setHeight(pos var) {
height = verify(var);
// height: 类的成员
// verify: 全局函数
// var: 参数
}