【C++11 笔记】关键字剖析 —— this
目录
一、this 和 Python-self
在 Python 中定义类时,无论是显式创建类的构造方法,还是向类中添加实例方法,都要求将 self 参数作为方法的第一个参数。当然,之所以命名为 self,只是程序员间约定俗成的一种习惯,使代码更具可读性。同一个类可实例化为多个对象,当某个类实例对象调用方法 (即“成员函数”) 时,它会把自身的引用作为第一个参数自动传给该方法,换言之,Python 会自动绑定类方法的第一个参数 self 指向调用该方法的对象。如此,Python 解释器就能知道到底要操作哪个对象的方法了。因此,程序在调用类实例方法和构造方法时,无需手动为第一个参数传值。抛砖引玉,Python 类方法中的 self 参数就相当于 C++ 中的 this 指针。更具体地:
在 C++ 中,每一个类实例对象都能通过 this 指针来访问自己的地址。this 指针是所有成员函数 (即“方法”) 的隐含参数。因此,在成员函数内部,this 指针 可用于指向调用该成员函数的类实例对象。
注意,友元函数没有 this 指针,因为友元不是类的成员,只有成员函数才有 this 指针。
二、this 指针
2.1 this 与 类成员
首先,看个例子:
#include <iostream>
using namespace std;
class Banana
{
public:
// const member function
int show_num_banana() const { return num_banana; }
private:
// data member
int num_banana = 0;
};
int main()
{
Banana obj1;
cout << obj1.show_num_banana() << endl;
system("pause");
return 0;
}
0
在上例中,先实例化一个 Banana 类对象 obj1,然后通过点运算符来访问 obj1 对象的常成员函数(即“访问函数”)show_num_banana,然后调用它来访问私有数据成员 num_banana。
其实,当调用类的成员函数时,实质是替某个类的实例对象调用它。如果 show_num_banana 指向 Banana 的成员(例如 num_banana),则它 隐式地指向调用该成员函数的类实例对象的成员。在上例中,当 show_num_banana 返回 num_banana 时,实际上它隐式地返回了 obj1.num_banana。
事实上,成员函数通过一个名为 this 的额外隐式参数来访问调用它的那个类实例对象,当调用一个成员函数时,用请求该成员函数的类实例对象的地址初始化 this。例如,若调用 obj1.show_num_banana(),则编译器负责把 obj1 的地址传递给 show_num_banana 的隐式形参 this,可等价地认为编译器将该调用重写成了以下形式:
// 伪代码, 用于说明调用成员函数的实际执行过程
Banana::show_num_banana(&obj1)
// 其中, 调用 Banana 的 show_num_banana 成员时传入了类实例 obj1 的地址
在成员函数内部,则可直接使用调用该成员函数的对象的成员,而无须通过成员访问运算符来做到这一点,因为 this 所指的正是这个对象。任何对类成员的直接访问都被看作是对 this 的隐式引用。换言之,当 show_num_banana 使用 num_banana 时,它 隐式地使用 this 所指向的成员(即 调用 show_num_banana 的类实例对象的成员),就像是书写了 this->num_banana 一样。
对用户而言,this 形参是隐式定义的。实际上,任何自定义名为 this 的参数或变量的行为都是非法的。成员函数体内,用户可显式地使用 this。因此,尽管没必要,还是能把 show_num_banana 定义成如下形式:
int show_num_banana() const { return this->num_banana; }
因为 this 的目的是 总指向 调用类成员的类实例对象,所以 this 是一个 (顶层) 常量指针,不允许改变 this 中保存的地址。
2.2 this 与 const 成员函数
注意,常成员函数中,紧随形参列表之后的 const 的关键字,其作用是 修改隐式 this 指针的类型。
默认情况下,this 的类型是 指向非常量类类型的常量指针(即顶层常量指针,指针本身是常量,指向非常量)。例如,Banana 中,若 show_num_banana 是非常成员函数,则其 this 的类型是 Banana *const(切记不是 Banana const* !!! 二者完全不同)。尽管 this 是隐式的,也仍需遵循初始化规则,因此默认情况下,不能把 this 绑定/指向到一个常量对象上,导致 无法在一个常量对象上调用普通的成员函数(注意,只有常成员函数可以操作常量/常对象)。
为此,需要把 this 声明成 const Banana *const 类型 以便于 this 可以指向常量。然而,this 不会出现在成员函数的参数列表中。于是,C++ 允许令 const 紧随成员函数的参数列表之后,以表明 this 是一个指向常量的指针,此时的 const 成员函数即为 常成员函数 (const member function)。这也是常成员函数声明中,形参列表后 const 关键字的由来。
此时,不妨把 show_num_banana 的函数体想象成如下形式以便理解:
// 伪代码, 用于说明隐式 this 指针如何使用
// 以下代码非法, 因为用户无法显式自定义 this 指针, 当然也不会出现在成员函数的参数列表中
// 谨记此处的 this 是一个指向常量的指针,因为 show_num_banana 是一个常量成员
int Banana::show_num_banana(const Banana *const this) { return this->show_num_banana; } // 形参列表中, 靠右的是顶层 const, 靠左的是底层 const
可见,因为常成员函数的 this 是指向常量的指针(底层 const),所以常成员函数不能改变调用它的类实例对象的内容,这也是常成员函数又称为 “访问函数” 的原因(与之相对的是 “修改函数”)。相应地,上例的 show_num_coil 可读取调用它的类实例对象如 obj1 的数据成员,但不能更新或写入新值。
总之,常量对象、常量对象的引用、常量对象的指针,都只能调用常量成员函数。
2.3 在成员函数中返回 this
现在,再看一个内容稍丰富些的例子:
#include <iostream>
using namespace std;
class Banana
{
public:
// const member function
int show_num_banana() const { return num_banana; }
// common member function decleration
Banana& combine(const Banana&);
public:
// data member
int num_banana = 0;
double revenue_banana = 0.0;
};
Banana& Banana::combine(const Banana& rhs)
{
// 把 rhs 的数据成员加到 this 对象的数据成员上
num_banana += rhs.num_banana;
revenue_banana += rhs.revenue_banana;
// 返回调用该成员函数的对象
return *this;
}
int main()
{
Banana obj1;
obj1.num_banana = 2;
obj1.revenue_banana = 4.2;
Banana obj2;
obj2.num_banana = 3;
obj2.revenue_banana = 8.3;
obj1.combine(obj2); // 相当于 in-place 操作
cout << obj1.num_banana << " " << obj1.revenue_banana << endl;
system("pause");
return 0;
}
5 12.5
可见,成员函数 combine 的设计类似于 += 的用途,但针对的是类实例对象(的数据成员)。当执行 obj1.combine(obj2); 时,obj1 的地址被绑定到隐式的 this 参数上,而 rhs 绑定到了 obj2 上。因此,当成员函数内执行 num_banana += rhs.num_banana; 时,效果等同于 obj1.num_banana += obj2.num_banana。
其实,成员函数 combine 最值得关注的部分是其 返回类型 和 返回语句。
一方面,当定义的函数类似某个内置运算符时,应令其行为尽量模仿该运算符。内置的赋值运算符将其左侧运算对象当成左值返回,因此,为保持一致,combine 必须返回引用类型,对应为 Banana&。
另一方面,无需使用隐式的 this 指针访问函数调用者的某个具体成员,而是需要把调用函数的对象当成一个整体来访问 —— return *this; 其中,return 解引用 this 指针以获取调用和执行本函数的类实例对象。换言之,上述语句返回 obj1 的引用。
三、小结
3.1 this 指针再叙
- this 实际是成员函数的一个形参,在调用成员函数时将类实例对象的地址作为实参传递给 this。但 this 形参是隐式的,并不出现在代码中,而是在编译阶段由编译器默默地将其添加到成员函数参数列表中。
- this 作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数内,且只有在通过类实例对象调用成员函数时才给 this 赋值。此外,用户无法显式定义 this 指针。
- this 默认为指向非常量的常量指针(顶层 const),其保存的之(所指向的类实例对象的地址)不可修改。
- 只有当类实例对象被创建后 this 才有意义,因此不能在 static 成员函数中使用,因为 static 成员函数与类相关联,而与具体的类实例对象无关。
- 在《C++函数编译原理和成员函数的实现》一节中讲到:成员函数最终被编译成与对象无关的普通函数,会丢失除成员变量外的所有信息,所以编译时要在成员函数中添加一个额外的参数,把当前对象的首地址传入,以此来关联成员函数和成员变量。而这个额外的参数就是 this,它是成员函数和成员变量关联的桥梁。
3.2 this 指针用途
- 一个类实例对象的 this 指针并非该对象本身的一部分,不会影响 sizeof(对象) 的结果。
- this 作用域限于类内部,当 在类的非静态成员函数中 访问类的非静态成员 时,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。换言之,即使没有显式写上 this 指针,编译器编译时也会加上 this。this 作为非静态成员函数的隐式形参,对各成员的访问均通过它进行。this 在成员函数的开始执行前构造,在成员的执行结束后清除。
3.3 this 指针使用
- 在类的非静态成员函数中返回类实例对象本身时,直接使用 return *this。
- 当参数与成员变量同名时,如 this->n = n (不能写成 n = n)。
参考文献
《C++ Primer 5th》
https://light-city.club/sc/basic_content/static/