C++ 和 C 的特点是拥有可灵活操作的指针,而 C++ 又拥有面向对象的特性。那么,有关在类成员函数中的 this 指针到底是怎样的指针呢?本文将来探讨这个问题。

1. 引入

试阅读以下程序代码:

#include <iostream>
using namespace std;
class Day {
    int hours_;
public:
    void setAndShow1(int hours);
    void setAndShow2(int hours);
    void setAndShow3(int hours);
};
void Day::setAndShow1(int hours) {
    hours_ = hours;
    cout << hours_ << endl;
}
void Day::setAndShow2(int hours) {
    this->hours_ = hours;
    cout << hours_ << endl;
}
void Day::setAndShow3(int hours) {
    (*this).hours_ = hours;
    cout << hours_ << endl;
}
int main() {
    Day aday;
    aday.setAndShow1(5);
    aday.setAndShow2(6);
    aday.setAndShow3(7);

    return 0;
}

程序主要展示了一个 Day 类,以及它的三个成员函数(setAndShow1,setAndShow2,setAndShow3)。相信,你可以看明白函数 setAndShow1 是在将实参 hours 赋值给成员变量 hours_,并输出 hours_。可能你还不知道函数 setAndShow2 与 setAndShow3 的实现中的 this 是什么意思。不要急,我们接下来就会讲解。

先看一看程序的运行结果:

运行结果:
5
6
7

2. 浅谈

事实上,上文提到的这三条语句是等效的:

hours_ = hours; //[1]
this->hours_ = hours; //[2]
(*this).hours_ = hours; //[3]

this 是一个指向对象本身的指针。事实上,在成员函数中直接调用成员变量(如[1]),在编译器内部也会被解析成 [2] 或 [3] 的形式。

对于在类外调用该类的成员函数的情况,我们一般是这样写的:

aday.setAndShow1(5);
aday.setAndShow2(6);
aday.setAndShow3(7);

调用函数时,编译器会在函数内隐式地定义一个常量指针形参 this,并初始化为指向这个函数所在的对象。可以理解为编译器帮我们把上面的函数调用重写成了这种形式:

Day::setAndShow1(&aday, 5);
Day::setAndShow2(&aday, 6);
Day::setAndShow3(&aday, 7);

到这里,我们便初步理解了 this 指针的作用了。

3. 深入

需要注意的是,this 形参是编译器隐式定义的。我们不能尝试去显式地定义它。

但这并不影响我们使用它,因为他会自动地被编译器处理。

下面讲解 this 指针 的常用场景

(1) 将成员函数的返回值设为 *this,用于层叠式调用函数

尝试阅读以下程序代码:

#include <iostream>
using namespace std;
class Day {
    int hours_;
public:
    Day &setHours(int hours);
    Day &addHours(int hours);
    void showHours();
};
Day &Day::setHours(int hours) {
    hours_ = hours;
    return *this;
}
Day &Day::addHours(int hours) {
    hours_ += hours;
    return *this;
}
void Day::showHours() {
    cout << hours_ << endl;
}
int main() {
    Day aday;
    aday.setHours(5).setHours(6).setHours(7).showHours();
    aday.addHours(1).addHours(1).showHours();

    return 0;
}
运行结果:
7
9

由于函数返回值为类类型的引用,使用句点运算符调用一个成员函数后可以反复使用,随之在一次函数处理后再次调用一个函数。

特别地,我们要对上文的 addHours 函数稍作说明:

该函数的意图是在对象的原有的小时数 (hours_) 中加上几小时。该函数使用了 +=运算符,也就是说,该运算符也是把左侧的运算对象当作左值来返回,恰巧与我们设计的成员函数的用法相仿。而对我们——类设计者而言,我们理应让成员函数的用法它的意图相吻合。如:

//对于 +=运算符
#include <iostream>
using namespace std;
int main() {
    int hours = 3;
    (hours += 1) += 2;
    cout << hours;

    return 0;
}
运行结果:
6
//对于成员函数 addHours
#include <iostream>
using namespace std;
class Day {
    int hours_;
public:
    Day &setHours(int hours);
    Day &addHours(int hours);
    void showHours();
};
Day &Day::setHours(int hours) {
    hours_ = hours;
    return *this;
}
Day &Day::addHours(int hours) {
    hours_ += hours;
    return *this;
}
void Day::showHours() {
    cout << hours_ << endl;
}
int main() {
    Day aday;
    aday.setHours(3).addHours(1).addHours(2).showHours();

    return 0;
}
运行结果:
6

当然,我们不可以对返回值不是类类型的引用的成员函数进行再调用,如:

#include <iostream>
using namespace std;
class Day {
    int hours_;
public:
    Day &setHours(int hours);
    Day &addHours(int hours);
    void showHours();
};
Day &Day::setHours(int hours) {
    hours_ = hours;
    return *this;
}
Day &Day::addHours(int hours) {
    hours_ += hours;
    return *this;
}
void Day::showHours() {
    cout << hours_ << endl;
}
int main() {
    Day aday;
    aday.setHours(5).showHours().addHours(1); //错误!

    return 0;
}

如上文,成员函数 showHours 的返回值为空值,故不可使用句点运算符在其基础上继续调用其他函数。

(2) 函数的形参名与成员变量名相同时,必须使用 this-> 以区分二者

如:

#include <iostream>
using namespace std;
class Day {
    int hours;
public:
    Day &setHours(int hours);
    Day &addHours(int hours);
    void showHours();
};
Day &Day::setHours(int hours) { //形参名与成员变量名相同。
    this->hours = hours; //必须使用 this-> 以示区分
    return *this;
}
Day &Day::addHours(int hours) {
    this->hours += hours; //必须使用 this-> 以示区分
    return *this;
}
void Day::showHours() {
    cout << hours << endl;
}
int main() {
    Day aday;
    aday.setHours(3).addHours(1).addHours(2).showHours();

    return 0;
}

4. 总结

本文介绍了类成员函数中 this 指针 的相关知识,回顾全文,我们:

  • 讲解了 this 指针 的定义——指向对象本身的常量指针
  • 介绍了 this 指针 的常用情况(两种)

对于 C++ 的普通应用,了解了上文提到的东西已经足够了。

最后,为了满足少数人的好奇心,此处总结了前文未曾提及到的一些 Q&A(引用自 CSDN 不知道起啥昵称):

Q1. this 指针 是什么时候被创建的?

A1. this 在成员函数的开始执行前构造,在成员的执行结束后清除。

但是如果 class 或者 struct 里面没有方法的话,它们是没有构造函数的,只能当做C的 struct 使用。采用 TYPE xx 的方式定义的话,在栈里分配内存,这时候,this 指针 的值就是这块内存的地址。采用 new 的方式创建对象的话,在堆里分配内存,new 操作符通过 eax 返回分配 的地址,然后设置给指针变量。之后去调用构造函数(如果有构造函数的话),这时将这个内存块的地址传给 ecx,在构造函数中再被编译器进一步处理。

Q2. this 指针 存放在何处?堆、栈、全局变量,还是其他地方?

A2. this 指针 会因编译器不同而有不同的放置位置。 可能是栈,也可能是寄存器,甚至全局变量。在汇编级别里面,一个值只会以3种形式出现:立即数、寄存器值和内存变量值。不是存放在寄存器就是存放在内存中,它们并不是与高级语言的变量相对应的。

Q3. this 指针 是如何传递类中的函数的?绑定?还是在函数参数的首参数就是 this 指针?那么,this 指针 又是如何找到“类实例后函数的”?

A3. 大多数编译器通过 ecx 寄存器传递 this 指针。 事实上,这也是一个潜规则。一般来说,不同编译器都会遵从一致的传参规则,否则不同编译器产生的 obj 就无法匹配了。

在调用之前,编译器会把对应的对象地址放到 eax 中。this 是通过函数参数的首参来传递的。this 指针 在调用之前生成,至于“类实例后函数”,没有这个说法。类在实例化时,只分配类中的变量空间,并没有为函数分配空间。自从类的函数定义完成后,它就在那里,并不会自动消失。

Q4. 我们只有获得一个对象后,才能通过对象使用 this 指针。如果我们知道一个对象 this 指针 的位置,可以直接使用吗?

A4. this 指针 只有在成员函数中才有定义。因此,你获得一个对象后,也不能通过对象使用 this 指针。 所以,我们无法知道一个对象的 this 指针 的位置(只有在成员函数里才有 this 指针 的位置)。当然,在成员函数里,你是可以知道 this 指针 的位置的(可以通过 &this 获得),也可以直接使用它。

Q5. 每个类编译后,是否创建一个类中函数表保存函数指针,以便用来调用函数?

A5. 普通的类函数(不论是成员函数,还是静态函数)都不会创建一个函数表来保存函数指针。只有虚函数才会被放到函数表中。

但是,即使是虚函数,如果编译器能明确知道调用的是哪个函数,编译器就不会通过函数表中的指针来间接调用,而是会直接调用该函数。


本文作者:以成
本文链接:
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 。转载请注明本文作者与链接!

MySQL考试复习总结 上一篇
C++深复制与浅复制 下一篇