关于类,初学者最常提出的疑问之一便是:“当调用某个成员函数时,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_id
即simple.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);
逐步求值:
calc.add(5)
→m_value = 5
,返回calc
引用;calc.sub(3)
→m_value = 2
,返回calc
引用;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
均以引用形式实现。