章节目录

隐藏的“this”指针和成员函数调用

本节阅读量:

新程序员经常会问一个关于类的问题:“调用成员函数时,C++如何知道要操作哪个对象?”。

首先,先定义一个简单类。该类封装一个整数值,并提供一些访问函数来获取和设置该值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

class Simple
{
private:
    int m_id{};
 
public:
    Simple(int id)
        : m_id{ id }
    {
    }

    int getID() const { return m_id; }
    void setID(int id) { m_id = id; }

    void print() const { std::cout << m_id; }
};

int main()
{
    Simple simple{1};
    simple.setID(2);

    simple.print();

    return 0;
}

如您所料,该程序会产生以下结果:

1
2

当调用 simple.setID(2); 时,C++知道函数 setID() 应该作用于对象simple,并且m_id实际上指的是simple.m_id。

C++使用了一个名为this的隐藏指针!本课将更详细地介绍这一点。


隐藏的this指针

在每个成员函数内部,关键字this都是一个保存当前隐式对象地址的const指针。

大多数时候,我们不会显式写出this,下面是一个说明示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

class Simple
{
private:
    int m_id{};

public:
    Simple(int id)
        : m_id{ id }
    {
    }

    int getID() const { return m_id; }
    void setID(int id) { m_id = id; }

    void print() const { std::cout << this->m_id; } // 使用 `this` 访问隐式对象,并使用操作符->访问成员 m_id
};

int main()
{
    Simple simple{ 1 };
    simple.setID(2);
    
    simple.print();

    return 0;
}

这与前面的示例相同,并打印:

1
2

请注意,上述两个示例中的print()成员函数执行的操作完全相同:

1
2
    void print() const { std::cout << m_id; }       // 隐式使用 this
    void print() const { std::cout << this->m_id; } // 显示使用 this

实际上,前者是后者的简写。编译程序时,编译器会隐式地为引用隐式对象的成员添加this->前缀。前一种写法有助于保持代码简洁,避免反复显式编写this->。


this 如何生效?

让我们仔细看看这个函数调用:

1
    simple.setID(2);

尽管对函数 setID(2) 的调用看起来只有一个参数,但实际上有两个!编译时,编译器会把表达式 simple.setID(2); 重写为类似下面的形式:

1
    Simple::setID(&simple, 2); // simple由前缀变换为函数参数!

注意,这现在只是一个标准函数调用,对象simple(原来的对象前缀)的地址会作为函数参数传递。

但这只是答案的一半。由于函数调用现在多了一个参数,因此还需要修改成员函数定义,以接收并使用该参数。下面是setID()的原始成员函数定义:

1
    void setID(int id) { m_id = id; }

编译器如何重写函数是特定于实现的细节,但最终结果如下:

1
    static void setID(Simple* const this, int id) { this->m_id = id; }

注意,setID函数多了一个最左侧的参数this,它是一个const指针(这意味着它不能重新指向其他对象,但指向的内容可以修改)。m_id成员也会使用this指针,被重写为this->m_id。

组合在一起:

  1. 当调用 simple.setID(2) 时,编译器实际会调用 Simple::setID(&simple, 2),simple 的地址会传递给函数。
  2. 函数有一个隐藏的 this 参数来接收 simple。
  3. setID() 函数中的成员变量,被改写为带 this-> 前缀的形式,这指向传入的 simple。所以当执行到 this->m_id 时,实际计算的是 simple.m_id。

好消息是,所有这些都会自动发生,是否记得具体转换过程并不重要。需要记住的是,所有非静态成员函数都有一个this指针,该指针指向调用该函数的对象。


this总是指向正在操作的对象

新程序员有时会困惑到底存在多少个this指针。每个成员函数都有一个指向隐式对象的this指针参数。考虑:

1
2
3
4
5
6
7
8
9
int main()
{
    Simple a{1}; // this = &a 在 Simple 的构造函数里
    Simple b{2}; // this = &b 在 Simple 的构造函数里
    a.setID(3); // this = &a 在成员函数 setID() 里
    b.setID(4); // this = &b 在成员函数 setID() 里

    return 0;
}

注意,this指针保存对象a还是对象b的地址,取决于当前调用的是a还是b的成员函数。

因为this是函数参数(而不是成员),所以它不会增加类实例的内存占用。


明确使用this

大多数情况下,不需要显式引用this指针。然而,在一些情况下,这样做是有用的:

首先,如果成员函数中有与数据成员同名的参数,则可以通过下面的写法消除歧义:

1
2
3
4
5
6
7
8
9
struct Something
{
    int data{}; // 这个结构体里没有使用 m_ 前缀

    void setData(int data)
    {
        this->data = data; // this->data 是成员变量, data 是参数
    }
};

这个Something类有一个名为data的成员。setData() 的函数参数也命名为data。在 setData() 函数中,data指的是函数参数(因为函数参数遮挡了成员变量data),因此如果想要使用data成员,需要采用 this->data 的形式。

一些开发人员喜欢显式地给所有类成员加上 this->,以明确表示正在引用成员变量。建议您避免这样做,因为它通常会降低代码可读性,却没有带来多少收益。使用“m_”前缀是区分私有成员变量和非成员(局部)变量的更简洁方法。


返回 *this

其次,有时让成员函数返回隐式对象本身会很有用。这样做的主要原因是允许成员函数“链接”在一起,从而在单个表达式中对同一对象连续调用多个成员函数!这称为函数链接(或方法链接)。

考虑这个常见示例,其中使用std::cout 连续输出几段文本:

1
std::cout << "Hello, " << userName;

编译器对上述代码段的求值如下:

1
(std::cout << "Hello, ") << userName;

首先,操作符«使用std::cout和字符串文字“Hello”,将“Hello”打印到控制台。然而,由于这是表达式的一部分,运算符«还需要返回某个值(或void)。如果运算符«返回void,则部分求值后的表达式会变成:

1
void{} << userName;

这显然没有任何意义(编译器会报错)。相反,操作符«会返回传入的流对象,在本例中就是std::cout。这样,在计算完第一个运算符«后,我们得到:

1
(std::cout) << userName;

然后打印用户名。

这样,只需要指定一次std::cout,就可以使用操作符«将任意多段文本链接在一起。每次调用operator«都会返回std::cout,因此下一次调用operator«时仍使用std::cout作为左操作数。

也可以在成员函数中实现这种行为。考虑以下类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Calc
{
private:
    int m_value{};

public:

    void add(int value) { m_value += value; }
    void sub(int value) { m_value -= value; }
    void mult(int value) { m_value *= value; }

    int getValue() const { return m_value; }
};

如果你想先加5,再减3,最后乘以4,必须这样写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <iostream>

int main()
{
    Calc calc{};
    calc.add(5); // 返回 void
    calc.sub(3); // 返回 void
    calc.mult(4); // 返回 void

    std::cout << calc.getValue() << '\n';

    return 0;
}

然而,如果让每个函数通过引用返回*this,就可以将调用链接在一起。下面是带有“可链接”函数的Calc新版本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Calc
{
private:
    int m_value{};

public:
    Calc& add(int value) { m_value += value; return *this; }
    Calc& sub(int value) { m_value -= value; return *this; }
    Calc& mult(int value) { m_value *= value; return *this; }

    int getValue() const { return m_value; }
};

注意,add()、sub() 和mult() 现在都会通过引用返回*this。因此,我们可以这样写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <iostream>

int main()
{
    Calc calc{};
    calc.add(5).sub(3).mult(4); // 函数链接

    std::cout << calc.getValue() << '\n';

    return 0;
}

这有效地把三行代码压缩成了一个表达式!让我们仔细看看它是如何工作的。

首先,调用calc.add(5),将5与m_value相加。然后,add() 返回对*this的引用,这是对隐式对象calc的引用,因此calc将是后续求值中使用的对象。接下来,calc.sub(3) 求值,它从m_value中减去3,然后再次返回calc。最后,calc.mult(4) 将m_value乘以4,并返回calc,它不再使用,因此被忽略。

由于每个函数在执行时都会修改calc,因此calc的m_value现在包含值(((0+5)-3)*4),即8。

这可能是this最常见的显式用法。只要成员函数需要支持链式调用,就应该考虑返回 *this。

因为this总是指向隐式对象,所以在解引用它时,不需要检查它是否为空指针。


将类重置回默认状态

如果您的类具有默认构造函数,您可能会对提供一种将现有对象重置到其默认状态的方法感兴趣。

在前面讲解委托构造函数时,我们知道构造函数只用于初始化新对象,不应直接调用。直接调用构造函数会导致意外行为。

将类重置回默认状态的最佳方法是创建 reset() 成员函数,让该函数创建一个新对象(使用默认构造函数),然后将该新对象赋值给当前隐式对象,如下所示:

1
2
3
4
    void reset()
    {
        *this = {}; // 值初始化一个新对象,并赋值给当前的隐式对象
    }

下面是一个完整的程序,演示了此reset() 函数的实际使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>

class Calc
{
private:
    int m_value{};

public:
    Calc& add(int value) { m_value += value; return *this; }
    Calc& sub(int value) { m_value -= value; return *this; }
    Calc& mult(int value) { m_value *= value; return *this; }

    int getValue() const { return m_value; }

    void reset() { *this = {}; }
};


int main()
{
    Calc calc{};
    calc.add(5).sub(3).mult(4);

    std::cout << calc.getValue() << '\n'; // 打印 8

    calc.reset();
    
    std::cout << calc.getValue() << '\n'; // 打印 0

    return 0;
}

this和const对象

对于非const成员函数,this是指向非常量值的const指针(这意味着它不能指向其他对象,但指向的对象可以修改)。对于const成员函数,这是指向常量值的const指针(意味着指针不能指向其他对象,也不能修改被指向的对象)。

尝试调用常量对象上的非常量成员时生成的错误可能有点神秘:

1
2
error C2662: 'int Something::getValue(void)': cannot convert 'this' pointer from 'const Something' to 'Something &'
error: passing 'const Something' as 'this' argument discards qualifiers [-fpermissive]

当对常量对象调用非常量成员函数时,隐式this函数参数需要的是指向非常量对象的const指针。但传入的值是指向常量对象的const指针。将指向常量对象的指针转换为指向非常量对象的指针需要丢弃常量限定符,这不能隐式完成。某些编译器生成的错误正是反映了编译器无法执行这种转换。


为什么 this 是指针而不是引用

由于this指针始终指向隐式对象(并且永远不能是空指针,除非我们做了一些导致未定义行为的事情),因此您可能想知道为什么this是指针而不是引用。答案很简单:当它被加入C++时,引用还不存在。

如果今天将 this 添加到C++语言中,它无疑将是引用而不是指针。在其他更现代的类C++语言(如Java和C#)中,this 被实现为引用。


14.16 第14章总结

上一节

15.1 类和头文件

下一节