明确一点,一个类就是一个独立的作用域,这也是为什么在类外定义函数或静态变量需要加上类名和作用域符号(类似void Complex::get() {}
)。如果不加,那么这个get
函数默认就是定义在全局范围中,如果你在类中使用了get
函数,那么编译器看到Complex::get()
没有被定义,从而会调用::get()
。这样破坏了类的封装性,同时,如果get
中使用了Complex
类中的成员变量还会报错。
作用域中重定义类型
在一个作用域内是可以自己去重定义一些类型的,比如下面这样:
for (int i = 0; i < 3; i++) {
typedef double D;
for (int j = 0; j < 3; j++) {
typedef char C;
}
}
类中重定义类型
那么,类作为一个独立的作用域也就可以在类中定义一些自己的类型。
struct Node {
typedef int Height;
typedef int Width;
};
typename Node::Height h = 3;
这种写法在标准库中很常见,很多仿函数都是这样写的:
可以看到plus
这个仿函数继承了binary_function
,也就是两个形参的函数,那么一个形参的函数就要继承unary_function
,这是题外话了,我们来看下binary_function
这个模板类到底是什么?
可以看到,这个模板类里面就自己定义了三个类型:第一个形参的类型、第二个形参的类型,返回值的类型。
还有就是使用类中的类型时,前面要加上typename
,当然,有些编译器不加也可以过,不过,加上了能使程序避免二义性问题,也就是说,在使用类中类型时都显示加上typename
,来告诉编译器,这是一个类型,不是变量或函数。
在类中定义成员函数,这个函数的返回类型和形参类型都是类中的类型,如果类中没有指定的类型,会在全局中去寻找,因此如果一个成员函数在类的外部定义,返回值类型或者形参类型是类中的类型,一定也要加上类名和域作用符号,如果不写,那么就是使用全局范围中的类型。
一个类的编译顺序
- 从上到下依次编译变量,类型别名,函数声明及其返回值和形参;
- 当上一步完成时,再编译成员函数的函数体。
这也就解释了为什么成员函数可以访问成员变量,即使这个变量是放在类的最末尾(后于这个成员函数定义)。原因就是编译器先编译整个类(除成员函数的函数体),然后再编译成员函数体。
这还解释了为什么在类中重命名一些类型尽量放到类的最开始处。因为类中的除成员函数体之外都是顺序编译的,如果不把类型重命名放到使用该重命名的类型之前,那么编译器就不会认识这个重命名的类型。
成员函数中变量名的查找顺序
看下面代码:
typedef int Height;
Height h = 5;
struct Node {
void func(Height h) {
h = 3; // 1. 成员函数func 中的形参 h = 3
this->h = 3; // 2. 成员变量 h = 3
Node::h = 3; // 3. 成员变量 h = 3
::h = 3; // 4. 全局范围中 h = 3
}
private:
Height h;
};
变量名的查找都是先到包含它最小的作用域查找,如果找不到,再到上一层的作用域查找,最后直到找到全局作用域。
因此,类中成员函数变量名的查找顺序先后为:
- 成员函数中的局部变量
- 类的成员变量
- 全局变量
那么,在成员函数中要显式使用类的成员变量的做法(成员函数默认可以访问成员变量)是2、3,分别通过类作用域符号和this
指针显示指定;在成员函数中要显式使用全局变量的做法是4,通过全局域作用符号::
。