15-1 隐藏的“this”指针与成员函数链式调用

初学编程者常会问到关于类的疑问之一是:“当调用成员函数时,C++ 如何追踪该函数是在哪个对象上被调用的?”

首先,我们定义一个简单的类来演示。该类封装了一个整数值,并提供若干访问函数用于获取和设置该值:

#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;
}

正如您所期望的,该程序产生的结果是:

image

不知何故,当我们调用 simple.setID(2); 时,C++ 知道函数 setID() 应该对对象 simple 进行操作,并且 m_id 实际上引用 simple.m_id。

答案是 C++ 使用了一个名为 this 的隐藏指针!在本课中,我们将更详细地讨论这一点。


隐藏的this指针

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

大多数时候,我们不会明确提及这一点,只是为了证明我们可以:

#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; } // use `this` pointer to access the implicit object and operator-> to select member m_id
};

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

    simple.print();

    return 0;
}

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

image

请注意,前两个示例中的 print() 成员函数完全执行相同的功能:

void print() const { std::cout << m_id; }       // implicit use of this
void print() const { std::cout << this->m_id; } // explicit use of this

原来前者是后者的简写形式。当我们编译程序时,编译器会自动为引用隐式对象的成员添加 this-> 前缀。这有助于保持代码简洁,避免重复显式书写 this-> 的冗余。

重要提示:
我们使用->从对象指针中选择成员。this->m_id等同于(*this).m_id。
第13.12节《指针与引用中的成员选择》将详细讲解->运算符。


这是如何设置的?

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

simple.setID(2);

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

Simple::setID(&simple, 2); // note that simple has been changed from an object prefix to a function argument!

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

但这只是答案的一半。由于函数调用现在增加了参数,成员函数定义也需要修改以接受(并使用)该参数。以下是我们原始的 setID() 成员函数定义:

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

编译器如何重写函数属于实现细节,但最终结果大致如下:

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

请注意,我们的 setId 函数新增了一个最左侧参数 this,它是一个 const 指针(意味着该指针不可重新指向,但指针内容可被修改)。m_id 成员也已重写为 this->m_id,利用了 this 指针。

进阶说明:
在此语境中,static 关键字表示该函数不与类对象相关联,而是被视为类作用域内的普通函数。静态成员函数将在第 15.7 节——静态成员函数中详细讲解。

综合说明:

  • 当我们调用 simple.setID(2) 时,编译器实际上调用的是 Simple::setID(&simple, 2),其中 simple 通过地址传递给函数。
  • 该函数有一个名为 this 的隐藏参数,用于接收 simple 的地址。
  • setID()内部的成员变量前缀为this->,该指针指向simple。因此当编译器解析this->m_id时,实际解析为simple.m_id。

好消息是这一切都自动完成,你是否理解其工作原理并不重要。你只需记住:所有非静态成员函数都拥有一个 this 指针,该指针指向调用该函数的对象。

关键要点:
所有非静态成员函数都拥有一个 this const 指针,该指针保存着隐式对象的地址。


this 始终指向被操作的对象

新手程序员有时会对 this 指针的数量感到困惑。每个成员函数都拥有一个 this 指针参数,该参数指向隐含的对象。请考虑以下情况:

int main()
{
    Simple a{1}; // this = &a inside the Simple constructor
    Simple b{2}; // this = &b inside the Simple constructor
    a.setID(3); // this = &a inside member function setID()
    b.setID(4); // this = &b inside member function setID()

    return 0;
}

请注意,this指针会根据我们调用对象a还是对象b的成员函数,交替持有对象a或对象b的地址。

由于this只是函数参数(而非成员),因此不会增加类实例在内存中的占用空间。


显式引用 this

大多数情况下,您无需显式引用 this 指针。但某些情况下这样做会很有用:

首先,若成员函数的参数与数据成员同名,可通过 this 消除歧义:

struct Something
{
    int data{}; // not using m_ prefix because this is a struct

    void setData(int data)
    {
        this->data = data; // this->data is the member, data is the local parameter
    }
};

该Something类包含一个名为data的数据成员。setData()函数的参数同样命名为data。在setData()函数内部,data指代函数参数(因为函数参数遮蔽了数据成员),因此若需引用数据成员,需使用this->data。

部分开发者倾向于在所有类成员前显式添加 this-> 以明确成员引用关系。我们建议避免此做法,因其往往降低代码可读性却收效甚微。使用 “m_” 前缀是区分私有成员变量与非成员(局部)变量更为简洁的方式。


返回 *this

其次,让成员函数将隐式对象作为返回值有时会很有用。这样做的主要目的是允许成员函数进行“链式调用”,从而能在单个表达式中对同一个对象调用多个成员函数!这被称为函数链function chaining 式调用(或方法链method chaining式调用)。

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

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

编译器将上述代码片段解析如下:

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

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

void{} << userName;

这显然毫无意义(编译器也会报错)。相反,<< 运算符会返回传入的流对象,本例中即为 std::cout。因此在第一个 << 运算符求值后,我们得到:

(std::cout) << userName;

然后打印用户的姓名。

这样,我们只需指定一次 std::cout,随后就能使用 operator<< 将任意数量的文本片段串联起来。每次调用 operator<< 都会返回 std::cout,因此下一次调用时,std::cout 将作为左操作数被使用。

我们同样可以在成员函数中实现这种行为。请看以下类:

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的运算,你需要这样操作:

#include <iostream>

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

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

    return 0;
}

image

然而,如果让每个函数通过引用返回*this,我们就能将调用串联起来。以下是具有“可链式”函数的新版Calc:

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。因此,这使我们能够执行以下操作

#include <iostream>

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

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

    return 0;
}

image

我们已将三行代码有效地浓缩为一个表达式!让我们深入解析其工作原理。

首先调用 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。

这可能是最常见的显式用法,当需要实现可链式成员函数时应优先考虑此方案。

由于该指针始终指向隐式对象,在解引用前无需检查其是否为空指针。


将类重置为默认状态

如果您的类具有默认构造函数,您可能需要提供一种方法,将现有对象恢复到其默认状态。

正如先前课程(14.12——委托构造函数)所述,构造函数仅用于初始化新对象,不应直接调用。直接调用将导致意外行为。

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

#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'; // prints 8

    calc.reset();

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

    return 0;
}

image


this 与 const 对象

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

在常量对象上尝试调用非常量成员函数时,生成的错误信息可能有些晦涩:

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]

image

当我们在常量对象上调用非常量成员函数时,隐含的 this 函数参数是一个指向非常量对象的常量指针。但该参数的类型是常量对象的常量指针。将常量对象指针转换为非常量对象指针需要舍弃常量限定符,而这种转换无法隐式完成。某些编译器生成的错误信息反映了编译器对被要求执行此类转换的抗议。


为什么是指针而非引用

由于 this 指针始终指向隐式对象(除非我们做了导致未定义行为的操作,否则它永远不会是空指针),你可能会疑惑为什么 it's a pointer 而不是引用instead of a reference。答案很简单:当 this 被添加到 C++ 时,引用还未存在。

若在当今C++语言中新增this,它必然会以引用而非指针的形式实现。在其他更现代的C++类语言(如Java和C#)中,this正是通过引用机制实现的。

posted @ 2025-12-31 17:35  游翔  阅读(12)  评论(0)    收藏  举报