我发现写文章的逻辑真的很重要。如果只是简单的陈述知识点,那么阅读的体验会很糟糕。要想做到文章逻辑清晰,文笔是其次,作为作者首先就要对这个问题有比较顺畅的思路,绕后再把内容呈现出来。话不多说,开始本篇的内容。
本篇主要介绍了C++中类中名字的查找机制。
名字查找(name lookup)指的是解析一个程序中出现的名字,并且寻找到与之相匹配的声明,这是在程序的编译阶段完成的。
对于一个类外的普通名字,例如一个类型名或一个变量名,其名字查找过程比较直接。编译器会先在当前作用域内、该名字使用之前的部分查找有无该名字的声明,若无,则会去外层作用域寻找依次类推。如果最终没有找到该名字的有效声明,则判定为这是一个未定义的名字。
然而,类中名字的查找方式与方才所描述的有所不同。因为一个类定义的代码是分为两部分来编译的。编译器会先编译类成员声明,再编译类内成员函数的代码块(无论成员函数的定义在类内还是类外)。
类声明中需要进行查找的名字,包括成员函数声明中的返回类型与形参类型名,以及成员变量中的类型名。这种名字大体可以分为两种,一种是自定义类型名,另一种是类型别名。
在名字查找时,会先使用该名字的当前声明语句,在类定义内,之前是否出现过该名字的声明。若无,会在外层作用域该类定义之前的部分查找,依次类推向外拓展,直至找到匹配的声明,该名字解析成功。否则,这是一个未声明的名字。
下面是一个例子:
class NAME; // 外层作用域1
class People{ // 作用域People,类内作用域
private:
using MONEY = double;
MARRIED isMarried = false; // 4. 错误,未声明的名字MARRIED
public:
People(NAME name); // 1. 正确
People(AGE age); // 2. 错误,未声明的名字AGE
People(MONEY money); // 3. 正确
typedef bool MARRIED;
};
// 外层作用域1,但是在类定义(使用该名字的位置)之后
using AGE = int;
上面的例子中描述了4种情况。
People
。在第一个构造函数的形参列表中,我们使用了自定义类型NAME
。NAME
的声明虽然不在类内,但是位于People
类定义所处作用域外层作用域1
的前面,因此类内的名字NAME
解析成功。AGE
是在People
类定义之后定义的一个类型别名,因此解析失败。MONEY
是在第三个构造函数声明之前,定义的一个类型别名,因此查找成功。isMarried
是一个类型为MARRIED
的成员变量。因为MARRIED
本身是一个定义在该成员变量之后的一个类型别名,因此类内查找失败,外层作用域又没有MARRIED
的声明,因此该名字解析失败。此外,需要注意类中类型成员的特殊性:尽管内层作用域中对名字重新声明可以覆盖外层作用域,但是类外定义的类型别名,在成员声明中一经使用,之后的声明禁止重新定义该别名。哪怕重新声明前后,该别名表示的类型相同。(编译器不会检查这种错误,但是前后两次MONEY的类型取决于哪个,C++标准并未指定,这属于编译器具体实现问题)下面是一个例子:
using MONEY = double;
class People{ // 作用域People,类内作用域
private:
public:
People(MONEY money);
using MONEY = double; // 不推荐这么做
};
尽量将类型成员的声明放在类定义的一开始,有利于代码语义清晰。
成员函数代码体的编译是在成员声明编译之后,因此无论成员函数的定义在类内与类外,均可以使用类成员声明中的所有名字。
成员函数定义在类内的,其名字查找机制与成员声明相同。
成员函数定义在类外的,若函数体中出现类定义中没有声明过的名字,则会在成员函数定义所在作用域进行名字查找。与类中成员声明一样,仅在成员函数定义前声明的名字可以被成功搜寻,之后的依旧无效。当前作用域若查找失败,则会向外层作用域依次扩大搜寻范围,直至找到或者判定为名字未定义。(最大找到全局作用域)