隐藏的this指针与成员函数链式调用:C++ 对象操作的核心机制

关于类,初学者最常提出的疑问之一便是:“当调用某个成员函数时,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;
}

程序输出符合预期:

2

当执行 simple.setID(2); 时,C++ 必须让 setID() 作用于对象 simple,并且使 m_id 实际上指的是 simple.m_id

答案在于 C++ 使用了一个隐藏的指针——this!本课将深入探讨 this 的细节。


隐藏的 this 指针

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

通常我们不必显式提及 this,但下面示例证明我们可以这么做:

#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 指针访问隐式对象,并通过 operator-> 选择成员 m_id
};

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

    simple.print();

    return 0;
}

输出仍为:

2

实际上,下面两种 print() 实现完全等价:

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

编译器在编译时会自动在所有访问隐式对象的成员前加上 this->,既简化代码,又避免反复书写 this-> 的冗余。


提醒

使用 -> 可通过指向对象的指针选择成员。this->m_id(*this).m_id 等价。
相关内容见第 13.12 课《使用指针与引用进行成员选择》。


this 如何被设置?

再审视以下函数调用:

simple.setID(2);

尽管调用 setID(2) 看似只有一个实参,实际上有两个!编译器将该表达式改写为:

Simple::setID(&simple, 2); // 注意:simple 由对象前缀变为函数实参!

至此,这只是一次普通的函数调用,对象 simple 通过地址传递给函数。

但这只是故事的一半。由于调用时增加了一个形参,成员函数定义也必须相应调整以接收并使用该实参。原始定义如下:

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

编译器如何改写函数属于实现细节,但最终效果类似:

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

注意:

  • 新增最左侧形参 this,类型为 Simple* const(指针自身不可改,但可修改所指内容)。
  • 成员 m_id 被改写为 this->m_id,利用 this 指针访问。

进阶提示
此处 static 表示该函数不与类对象关联,而视为位于该类作用域内的普通函数。详见第 15.7 课《静态成员函数》。


总结

  • 当我们写 simple.setID(2) 时,编译器实际调用 Simple::setID(&simple, 2),将 simple 的地址传入。
  • 函数含隐藏形参 this,接收 simple 的地址。
  • 函数内部所有成员变量前均隐式加 this->,因此 this->m_idsimple.m_id

好消息:上述过程完全自动,无需刻意记忆。只需记住:所有非静态成员函数均拥有一个指向被调用对象的 this 指针。


关键洞见

所有非静态成员函数均含有一个指向隐式对象的 const 指针 this


this 始终指向被操作对象

初学者常疑惑存在多少个 this 指针。答案是:每个成员函数调用只有一个 this 形参,指向当前隐式对象。例如:

int main()
{
    Simple a{1}; // 在 Simple 构造函数中 this = &a
    Simple b{2}; // 在 Simple 构造函数中 this = &b
    a.setID(3);  // 在 setID() 中 this = &a
    b.setID(4);  // 在 setID() 中 this = &b

    return 0;
}

this 指针在调用 a 的成员函数时指向 a,在调用 b 的成员函数时指向 b

由于 this 仅是函数形参(非数据成员),并不会增加类实例的内存占用。


显式引用 this

多数情况下无需显式使用 this,但在以下场景有用:

1. 解决命名冲突

若成员函数的形参与数据成员同名,可用 this 区分:

struct Something
{
    int data{}; // 未使用 m_ 前缀,因为是 struct

    void setData(int data)
    {
        this->data = data; // this->data 为成员,data 为局部参数
    }
};

某些开发者喜欢在所有成员前加 this-> 以显式表明其为成员。我们建议避免这种做法,因其降低可读性而收益甚微;使用“m_”前缀能更简洁地区分私有成员与局部变量。

2. 返回 *this 以实现函数链式调用

有时需要让成员函数返回隐式对象本身,以便在同一表达式中连续调用多个成员函数,称为函数链式调用(method chaining)。

std::cout 为例:

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

编译器按如下顺序求值:

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

operator<< 返回 void,则后续表达式 void{} << userName 非法。因此 operator<< 返回所传入的流对象(此处即 std::cout),从而实现链式输出。

我们可在自定义类中模拟此行为。以下类 Calc 初始版本:

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

调用:

calc.add(5);
calc.sub(3);
calc.mult(4);

若让各函数返回 *this 的引用,则可链式调用:

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

于是:

calc.add(5).sub(3).mult(4);

逐步求值:

  1. calc.add(5)m_value = 5,返回 calc 引用;
  2. calc.sub(3)m_value = 2,返回 calc 引用;
  3. calc.mult(4)m_value = 8,返回 calc(未再使用)。

最终 m_value 为 8。
这是 this 最常见的显式用法之一,每当需要链式调用时应考虑采用。


将类重置为默认状态

若类拥有默认构造函数,可提供 reset() 以将现有对象恢复默认状态。
注意:构造函数仅用于初始化新对象,不得直接调用,否则行为未定义。

最佳做法是在 reset() 内创建临时默认对象,再赋值给当前隐式对象:

void reset()
{
    *this = {}; // 值初始化新对象并覆盖隐式对象
}

完整示例:

#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 成员函数,this 是“指向 const 对象的 const 指针”(指针与对象均不可改)。

若试图在 const 对象上调用非 const 成员函数,编译器报错信息可能晦涩:

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]

原因在于:
非 const 成员函数的隐式 this 形参是“指向非 const 对象的 const 指针”;而实参类型却是“指向 const 对象的 const 指针”。将“指向 const 的指针”转换为“指向非 const 的指针”需要丢弃 const 限定符,而这是不允许的隐式转换。


为何 this 是指针而非引用

既然 this 始终指向隐式对象(且除非触发未定义行为,否则不会为 null),为何它被设计为指针而非引用?
答案很简单:引入 this 时,C++ 尚无引用。若今天才添加 this,它无疑会是引用。在现代类 C++ 语言(如 Java、C#)中,this 均以引用形式实现。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"