C++类成员函数中的名字查找

在C++类的成员函数中,名字查找是按照由内到外进行的。首先查找成员函数中的名字,之后再查找类中定义的名字,最后查找类外定义的名字。

查找顺序

自定义类MyClass代码如下

int i = 1;
class MyClass {
  public:
    int i = 2;
    void myFunc() {
      int i = 3;
      int j = i;
   }
};

其中,在MyClass类的成员函数myFunc()中使用了变量i,而在myFunc()函数、MyClass类以及类外都定义了变量i,此时myFunc()成员函数会根据由内向外的顺序对i进行查找,即此时j的值应该是3。

使用指定的名字

使用类的变量

在myFunc()中如果需要使用类myFunc()函数的变量i,可以进行如下定义

int j = MyClass::i;

或者

int j = this->i;

此时,表示使用MyClass中定义的变量i,j的值是2。

使用类外变量

在myFunc()中如果需要使用类外定义的变量i,可以进行如下定义

int j = ::i;

此时,j的值是1。

C++名字查找与类的作用域

每个类都会定义它自己的作用域。在类的作用域之外,普通的数据和函数成员只能由对象、引用或者指针使用成员访问运算符来访问。对于类类型成员则使用作用域运算符访问。不论哪种情况,跟在运算符之后的名字都必须是对应类的成员。

Screen::pos ht = 24, wd = 80;  // 使用 Screen 定义的 pos 类型
Screen scr(ht, wd, ' ');
Screen *p = &scr;
char c = scr.get();  // 访问 scr 对象的 get 成员
c = p->get();  // 访问 p 所指对象的 get 成员

作用域和定义在类外部的成员

类的作用域 (class scope) 每个类定义一个作用域。类作用域比其他作用域更加复杂,类中定义的成员函数甚至有可能使用定义语句之后的名字。

一个类就是一个作用域,我们在类的外部定义成员函数时必须同时提供类名和函数名。在类的外部,成员的名字被隐藏起来了。一旦遇到了类名,定义的剩余部分就在类的作用域之内了,这里的剩余部分包括参数列表和函数体。结果就是,我们可以直接使用类的其他成员而无须再次授权了。

void Window::clear(ScreenIndex i)
{
    Screen &s = screens[i];
    s.contents = string(s.height * s.width, ' ');
}

编译器在处理参数列表之前已经明确了我们当前正位于 Window 类的作用域中,所以不必再专门说明 ScreenIndex 是 Window 类定义的。出于同样的原因,编译器也能知道函数体中用到的 screens 也是在 Window 类中定义的。

函数的返回类型通常出现在函数名之前。因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。这时,返回类型必须指明它是哪个类的成员。我们可能向 Window 类添加一个新的名为 add_screen 的函数,它负责向显示器添加一个新的屏幕。这个成员的返回类型将是 ScreenIndex,用户可以通过它定位到指定的 Screen。

class Window {
public:
    // 向窗口添加一个 Screen,返回它的编号
    ScreenIndex add_screen(const Screen &);
    ......
};

// 首先处理返回类型,之后我们才进入 Window 的作用域
Window::ScreenIndex
Window::add_screen(const Screen &s)
{
    screens.push_back(s);
    return screens.size() - 1;
}

因为返回类型出现在类名之前,所以事实上它是位于 Window 类的作用域之外的。在这种情况下,要想使用 Screenlndex 作为返回类型,我们必须明确指定哪个类定义了它。

名字查找与类的作用域

名字查找 (name lookup) (寻找与所用名字最匹配的声明的过程) 的过程比较直截了当。

  • 首先,在名字所在的块中寻找其声明语句,只考虑在名字的使用之前出现的声明。
  • 如果没找到,继续查找外层作用域。
  • 如果最终没有找到匹配的声明,则程序报错。

对于定义在类内部的成员函数来说,解析其中名字的方式与上述的查找规则有所区别,不过在当前的这个例子中体现得不太明显。

类的定义分两步处理:

  • 首先,编译成员的声明。
  • 直到类全部可见后才编译函数体。

编译器处理完类中的全部声明后才会处理成员函数的定义。按照这种两阶段的方式处理类可以简化类代码的组织方式。因为成员函数体直到整个类可见后才会被处理,所以它能使用类中定义的任何名字。

名字查找 (name lookup) 是根据名字的使用寻找匹配的声明的过程。

用于类成员声明的名字查找

这种两阶段的处理方式只适用于成员函数中使用的名字。声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见。如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续査找。

typedef double Money;
std::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 的成员,而非外层作用域的 std::string 对象。

类型名要特殊处理

内层作用域可以重新定义外层作用域中的名字,即使该名字已经在内层作用域中使用过。然而在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。

typedef double Money;

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 std::string::size_type pos;
    void dummy_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 height) {
    cursor = width * ::height;  // height 是哪个全局的
}

尽管外层的对象被隐藏掉了,但我们仍然可以用作用域运算符访问它。

在文件中名字的出现处对其进行解析

当成员定义在类的外部时,名字査找的第三步不仅要考虑类定义之前的全局作用域中的声明,还需要考虑在成员函数定义之前的全局作用域中的声明。

int height;  // 定义了一个名字,稍后将在 Screen 中使用

class Screen {
public:
    typedef std::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 的定义之前,因此可以被正常使用。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持。

原文地址:https://blog.csdn.net/hou09tian/article/details/109256842

相关文章: