🔷 第十章 类和对象
📄 一句话概括

this 是"当前对象的地址",一个藏在每个普通成员函数里的隐形参数。你写 p.show() 时,编译器实际做的事是 show(&p)——把 p 的地址偷偷传了进去。所以成员函数里的 x 其实就是 this->x,它才知道该读"谁的 x"。三条要点:① this 是指针,不是对象;② 它不能改指向(this = ... 不允许);③ 静态成员函数没有 this(10.4.3 那条限制的根源就在这)。

📝 本节和前面几节的关系

本节回答的是 10.1.1 埋到现在的一个问题:"成员函数里为什么可以直接写 x,不用写对象名?" 以及 10.4.3 那个限制("静态成员函数不能访问普通成员")背后的原因。用到的旧知识:7.4 指针(this 就是指针)、7.4.5 指针常量(this 属于"指针本身不能改"那一类)、7.8.1 引用("当前对象"也可以用引用来表达:*this)。

一、问题从哪来

class Point {
private:
    int x, y;
public:
    void set(int a, int b) { x = a; y = b; }
    void show() { cout << "(" << x << ", " << y << ")" << endl; }
};

int main() {
    Point p1, p2;
    p1.set(1, 2);
    p2.set(3, 4);
    p1.show();        // (1, 2)
    p2.show();        // (3, 4)
}

关键问题:p1.show() 和 p2.show() 调的是同一个函数(同一份代码,同一个地址)——那它怎么知道这次该打印 (1,2) 还是 (3,4)?

同一个函数,凭什么给出两种结果?

二、答案:偷偷多传了一个参数

编译器在编译成员函数时,悄悄给每个普通成员函数加了一个"看不见的第一个参数"——就是 this。

你写的:

void show() { cout << x << endl; }
p.show();

编译器心里的版本(概念上等价):

void show(Point *const this) { cout << this->x << endl; }
show(&p);

所以:

你看到的 实际发生的
p.show() show(&p) —— 把 p 的地址传进去
函数里的 x this->x —— "当前对象"的 x
函数里的 set(a, b) this->set(a, b)
静态成员函数没有这个参数 所以它不知道自己属于谁 → 不能碰普通成员(10.4.3)

一句话理解:this 就是"我这次是在给谁办事"。每次调用成员函数,"谁"都不一样,this 指着不同的对象——同一个函数因此能服务所有对象。

三、this 的三个性质

性质 说明 违反会怎样
① 它是一个指针 类型是 类名 *(如 Point *),所以用 -> 访问 写成 this.x 会报错(this 是指针,得写 this->x)
② 不能改指向 它的类型严格说是 Point * const this——是指针常量(7.4.5 的"锁指针") this = &other; 编译不过:"这一刻我在给谁办事"不能中途换人
③ 不是对象 它只存地址,不占对象的内存(sizeof(Point) 里不含 this) 别以为"每个对象里存了个 this"

关于 ③ 的补充:this 不是成员变量,它只是参数。所以:

cout << sizeof(Point) << endl;   // 8(两个 int),跟 this 无关

在 const 成员函数里,this 会变成"指向常量的指针":

函数 this 的类型 含义
void show() Point * const 可以改自己的数据
void show() const const Point * const 不能改自己的数据(10.4.1 那个承诺的实现方式)

看到没有——const 成员函数的"承诺",就是靠把 this 变成超窄的类型的实现的。

四、this 的三种实用写法

写法 1:解决"参数名和成员名同名"

这是最常见的用途:

class Point {
private:
    int x, y;
public:
    void set(int x, int y) {        // 参数也叫 x、y(很自然)
        this->x = x;                // 左边是成员,右边是参数
        this->y = y;
    }
};

this->x = x; 读作:"把这个对象的 x,设成传进来的那个 x"。

不加 this-> 会怎样:x = x; —— 自己赋值给自己(参数覆盖了参数的视野,成员根本没被碰到)。这个 bug 编译器不报错(3.1.4 讲过的"赋值有值"陷阱的变体),调完发现数据没变,特别容易懵。

写法 2:返回"自己",实现链式调用

class Point {
private:
    int x, y;
public:
    Point& moveX(int dx) {          // 返回"自己的引用"
        x += dx;
        return *this;               // *this 就是"我自己"(7.8.1:*p 是对象本身)
    }
    Point& moveY(int dy) {
        y += dy;
        return *this;
    }
};

Point p;
p.moveX(3).moveX(2).moveY(5);       // 链式调用:一路点下去

为什么能连点:p.moveX(3) 返回的是 p 自己的引用,所以后面还能继续 .moveX(2)。这就是 cout << a << b << c; 那种写法的原理(<< 也返回了流自己的引用)。

注意返回类型是 Point&(引用),不是 Point(值):返回值会复制一个新对象(10.2.2),链式调用改的就不是原件了。这里必须返回引用。

写法 3:判断"是不是同一个对象"

bool isSame(const Point &other) const {
    return this == &other;          // 比地址:同一个对象才有同一个地址
}

比的是地址(7.4.3 的思路),而不是比内容。"是不是同一个东西"和"长得像不像"是两件事。

五、动手 Demo:亲眼看见 this

#include <iostream>
using namespace std;

class Item {
private:
    int  value;
    char name[20];

public:
    // ① 参数与成员同名:必须用 this->
    void set(int value) {
        this->value = value;               // 不加 this-> 就是自赋值(没效果)
    }

    void setName(const char *name) {
        strcpy(this->name, name);          // 同理
    }

    // ② 打印 this,看它是不是"对象的地址"
    void whereAmI() const {
        cout << "  this       = " << this << endl;
    }

    // ③ 返回自己,实现链式调用
    Item& add(int v) {
        value += v;
        return *this;                      // *this = 我自己
    }

    int  get() const { return value; }

    // ④ 判断是不是同一个对象
    bool isSame(const Item &other) const {
        return this == &other;
    }
};

int main() {
    Item a, b;
    a.set(10);
    b.set(20);

    cout << "对象 a 的地址 = " << &a << endl;
    a.whereAmI();                          // this 应该等于 &a

    cout << "对象 b 的地址 = " << &b << endl;
    b.whereAmI();                          // this 应该等于 &b

    cout << "--- 链式调用 ---" << endl;
    a.add(5).add(5).add(10);               // 一路点下去
    cout << "a 的值 = " << a.get() << "(应为 30)" << endl;

    cout << "--- 是不是同一个对象 ---" << endl;
    cout << "a.isSame(a) ? " << (a.isSame(a) ? "是" : "否") << endl;   // 是
    cout << "a.isSame(b) ? " << (a.isSame(b) ? "是" : "否") << endl;   // 否

    cout << "sizeof(Item) = " << sizeof(Item) << "(不含 this)" << endl;
    return 0;
}

看到什么算成功(地址各机器不同,关键是相等关系):

对象 a 的地址 = 0x61ff00
  this       = 0x61ff00      ← 与 &a 相同
对象 b 的地址 = 0x61fef0
  this       = 0x61fef0      ← 与 &b 相同
--- 链式调用 ---
a 的值 = 30(应为 30)
--- 是不是同一个对象 ---
a.isSame(a) ? 是
a.isSame(b) ? 否
sizeof(Item) = 24(不含 this)

读结果四句话:

  1. this 打印出来的地址,和对应对象的 & 完全一样——this 就是"当前对象的地址",实锤。
  2. a.add(5).add(5).add(10) 一次连点三下,最后 a 的值是 30——链式调用靠 return *this; 实现。
  3. a.isSame(b) 是"否"——虽然两个对象长得一样(都是 Item),但地址不同,不是同一个对象。
  4. sizeof(Item) = 24(int 4 + char[20] 20 = 24,正好对齐)——this 不占对象空间。

30 秒实验:把 void set(int value) 里的 this->value = value; 改成 value = value;(去掉 this->),重新运行。a 的值 会变成 0(或垃圾值)——因为那句变成了"参数给自己赋值",成员 value 压根没动。 编译器不报任何错。这个静悄悄的 bug,就是 this-> 存在的意义。

六、常见误区

  1. "this 是对象"——不是,是指针。所以要用 this->x,不能写 this.x。"当前对象"本身要写 *this。
  2. "每个对象内部存了一份 this"——不是。this 是函数调用时传进来的参数,不占对象的内存(sizeof 里没有它)。
  3. "this 可以被赋值、可以指向别的对象"——不行。它是指针常量(Point * const),this = ... 编译不过。"这次在给谁办事"是定死的。
  4. "静态成员函数里也能用 this"——不能,它压根没有 this。这就是 10.4.3 那条"静态成员函数不能访问普通成员"的根本原因。报错 invalid use of 'this' in static member function。
  5. "链式调用返回 Point 和返回 Point& 差不多"——差很多。返回值会复制一个临时对象(10.2.2),你改的是那个临时副本,链式调用就白点了。要链式调用就必须返回引用(Point&),且返回 *this。
自测三问(点开对答案) 1. `p.show()` 在编译器眼里是什么样子?`this` 是什么?——大致等价于 **`show(&p)`**:编译器给成员函数**偷偷加了一个"当前对象地址"的参数**,就是 **`this`**。所以函数里的 `x` 其实是 `this->x`。 2. `this` 能改指向吗?它占对象的内存吗?——**不能改指向**(类型是 `T * const`,指针常量,7.4.5 的"锁指针");**不占对象内存**(它是参数,不是成员)。 3. 什么时候必须显式写 `this->`?举两个例子。——① **参数名和成员名同名**时(`this->x = x;`,否则变成自己给自己赋值,还静悄悄不报错);② **返回自身做链式调用**(`return *this;`)时;③ 判"是不是同一个对象"(`this == &other`)。

相关笔记