我刚学 C 的时候。
函数很“老实”。
你要它改谁。
你就把谁的地址传进去。
void inc(int* p) {
++*p;
}
int x = 0;
inc(&x);
你看。
谁被改。
一眼就知道。
后来我第一次写 C++。
看到这种代码。
struct Counter {
int x = 0;
void inc() { ++x; }
};
Counter c;
c.inc();
新手总会问一句。
“inc() 里没参数。
它怎么知道该改谁?”
这句问得很好。
因为你一旦把这个问题搞明白。
你就会对 C++ 的“对象”少一点敬畏。
多一点底气。
那些年,C++ 还叫 C with Classes
C++ 的早年。
其实挺土。
1979 年。
Stroustrup 在贝尔实验室折腾的东西。
名字就叫 “C with Classes”。
意思很直白。
给 C 加一层“类”的皮。
早期的编译器也很直白。
最出名的那个叫 CFront。
它不直接生成机器码。
它把 C++ 翻译成 C。
再交给 C 编译器去干苦活。
这就逼着所有“面向对象的优雅”。
都得落到 C 的那几个原语上。
结构体。
函数。
指针。
于是成员函数这件事。
从一开始就不神秘。
它就是一个普通函数。
只不过编译器偷偷帮你塞了一个参数。
这个参数。
后来大家叫它 this。
你以为你在“调用成员函数”。
编译器以为你在“调用一个函数,并把对象地址顺手递过去”。
把 obj.f(a, b) 翻译成“人话”
我们写一段稍微像样的代码。
struct Vec {
int x = 0;
int y = 0;
void move(int dx, int dy) {
x += dx;
y += dy;
}
};
Vec v;
v.move(1, 2);
你写的是 v.move(1, 2)。
编译器心里想的更像这样。
struct Vec { int x; int y; };
void Vec_move(Vec* self, int dx, int dy) {
self->x += dx;
self->y += dy;
}
Vec v;
Vec_move(&v, 1, 2);
注意这行。
&v。
它就是那根“线”。
把函数和对象绑在一起。
你在成员函数里写的每一次 x。
其实都是 this->x 的省略写法。
所以 this 是什么。
它首先就是。
“当前对象的地址”。
this 的类型:T* const
很多人把 this 当成“指针”。
对。
但还不完整。
更准确地说。
在非 const 成员函数里。
它是 T* const。
指针指向的对象可以改。
但指针本身不能改指向。
也就是你可以写。
struct Counter {
int x = 0;
void inc() {
++x;
}
};
但你不能在函数里干这种事。
struct Counter {
int x = 0;
void bad() {
this = nullptr;
}
};
编译器会直接把你拦下来。
因为 “我是谁”。
这件事在函数入口就已经签好合同了。
你进来就得对这个对象负责。
const 成员函数:不是“更礼貌”,是“换了一张身份证”
C++ 里的 const。
经常被讲成“承诺不改”。
听着像道德。
其实是类型。
我们在成员函数后面加个 const。
struct Vec {
int x = 0;
int y = 0;
int sum() const {
return x + y;
}
};
你写的是 sum() const。
编译器做的事很硬。
它把 this 的类型改了。
从 Vec* const。
改成 Vec const* const。
然后一切就顺理成章了。
你试图修改成员。
int sum() const {
++x;
return x + y;
}
它不会跟你辩论“你是不是好人”。
它只会说。
不行。
因为 x 现在要通过 Vec const* 去访问。
改不了。
这也是为什么。
“const 成员函数能不能调用非 const 成员函数”。
本质上不是礼貌问题。
是类型不匹配。
你手里拿的是 const Vec*。
你就只能调用那些承认自己接受 const Vec* 的函数。
return *this;:链式调用其实是“把自己递回去”
链式调用这种写法。
你肯定见过。
struct Counter {
int x = 0;
Counter& inc() {
++x;
return *this;
}
};
Counter c;
c.inc().inc().inc();
新手喜欢背。
“返回引用就能链式”。
但你把它放回 this 的语境。
就很朴素。
*this 就是“当前对象本体”。
你把它作为引用返回。
下一次调用。
还是同一个对象地址。
它跟这句很像。
Counter& r = c;
r.inc();
只不过你把中间变量省掉了。
所以链式调用的要害。
不是“链”。
是“你返回的到底是谁”。
你要是返回一个临时对象。
链就会变成事故现场。
静态成员函数:它为什么没有 this
有时候你会看到这种写法。
struct Counter {
int x = 0;
static int twice(int v) {
return v * 2;
}
};
int n = Counter::twice(21);
它也写在类里。
但它跟“对象”没关系。
所以它没有 this。
这点很像早期 CFront 的翻译风格。
静态成员函数基本就等价于。
“放在一个命名空间里”。
只是名字长一点。
一点更老的经验:别把 this 当成永远不变的地址
我前面说。
this 是对象地址。
大多数时候。
这句话够用了。
但写到继承。
尤其是多重继承。
你会看到另一层现实。
this 有时会被编译器“调一下”。
因为派生类对象里。
可能塞着不止一块基类子对象。
你把 Derived* 转成 Base2* 的那一刻。
指针值就可能发生偏移。
你以为你只是换了类型。
编译器其实顺手给你挪了地址。
这个故事我们后面讲继承布局时会展开。
你先把这句记在心里就行。
this 不是魔法。
但它也不是一句“永远等于对象起始地址”就完事的。
把这篇文章的结论说得更直白一点
成员函数。
不是对象里藏着的一段代码。
代码在别处。
对象里只有数据。
你调用 obj.f()。
真正发生的是。
你调用一个函数。
并且把 &obj 交给它。
这根隐形的参数。
就是 this。
等你接受了这件事。
很多看似“高级”的语法。
就会自动变得好推导。
也更好调试。