"this" 指针只是一个编译时的东西吗?
Is the "this" pointer just a compile time thing?
我问自己 this
指针是否会被过度使用,因为我通常每次引用成员变量或函数时都会使用它。我想知道它是否会对性能产生影响,因为必须有一个指针每次都需要取消引用。所以我写了一些测试代码
struct A {
int x;
A(int X) {
x = X; /* And a second time with this->x = X; */
}
};
int main() {
A a(8);
return 0;
}
令人惊讶的是,即使使用 -O0
,它们也输出完全相同的汇编代码。
此外,如果我使用一个成员函数并在另一个成员函数中调用它,它会显示相同的行为。那么 this
指针只是编译时的东西而不是实际的指针吗?或者是否存在 this
实际上被翻译和取消引用的情况?我使用 GCC 4.4.3 顺便说一句。
当您使用非静态方法时,this
始终必须存在。无论您是否显式使用它,都必须引用当前实例,这就是 this
给您的。
在这两种情况下,您都将通过 this
指针访问内存。只是在某些情况下可以省略。
So is the this pointer just a compile time thing and not an actual pointer?
它非常是一个运行时间的东西。它指的是调用成员函数的对象,自然那个对象可以存在运行时间。
编译时的事情是名称查找的工作原理。当编译器遇到 x = X
时,它必须弄清楚正在分配的 x
是什么。所以它查找它,并找到成员变量。由于 this->x
和 x
指的是同一事物,自然会得到相同的汇编输出。
编译后,每个符号只是一个地址,所以不会是运行时间问题。
无论如何,任何成员符号都会被编译为当前 class 中的一个偏移量,即使您没有使用 this
.
当在C++中使用name
时,它可以是以下之一。
- 在全局命名空间中(如
::name
),或在当前命名空间中,或在使用的命名空间中(当using namespace ...
被使用时)
- 当前class
- 本地定义,在上层
- 本地定义,在当前区块
因此,当您编写代码时,编译器应该以查找符号名称的方式从当前块一直扫描到全局命名空间。
使用 this->name
帮助编译器缩小对 name
的搜索范围,使其仅在当前 class 范围内查找,这意味着它会跳过局部定义,如果在class范围,不要在全局范围内寻找。
它是一个实际的指针,正如标准指定的那样 (§12.2.2.1):
In the body of a non-static (12.2.1) member function, the keyword this
is a prvalue expression whose value is the address of the object for which the function is called. The type of this
in a member function of a class X
is X*
.
每次在 class 自己的代码中引用 非静态 成员变量或成员函数时,this
实际上是隐含的。还需要它(无论是隐式还是显式),因为编译器需要在运行时将函数或变量绑定到实际对象。
显式使用它很少有用,除非您需要,例如,在成员函数中消除参数和成员变量之间的歧义。否则,如果没有它,编译器将使用参数 (See it live on Coliru).
隐藏成员变量
这是一个简单的例子,说明 "this" 在运行时如何发挥作用:
#include <vector>
#include <string>
#include <iostream>
class A;
typedef std::vector<A*> News;
class A
{
public:
A(const char* n): name(n){}
std::string name;
void subscribe(News& n)
{
n.push_back(this);
}
};
int main()
{
A a1("Alex"), a2("Bob"), a3("Chris");
News news;
a1.subscribe(news);
a3.subscribe(news);
std::cout << "Subscriber:";
for(auto& a: news)
{
std::cout << " " << a->name;
}
return 0;
}
这几乎是 的副本,我在其中注释了一些示例的 asm 输出,包括显示 this
指针传入的寄存器。
在 asm 中,this
的工作方式与隐藏的第一个 arg 完全相同,因此成员函数 foo::add(int)
和非成员函数 [=15] =] 它采用 explicit foo*
第一个 arg 编译为完全相同的 asm.
struct foo {
int m;
void add(int a); // not inline so we get a stand-alone definition emitted
};
void foo::add(int a) {
this->m += a;
}
void add(foo *obj, int a) {
obj->m += a;
}
On the Godbolt compiler explorer,使用 System V ABI 为 x86-64 编译(RDI 中的第一个参数,RSI 中的第二个参数),我们得到:
# gcc8.2 -O3
foo::add(int):
add DWORD PTR [rdi], esi # memory-destination add
ret
add(foo*, int):
add DWORD PTR [rdi], esi
ret
I use GCC 4.4.3
那是 released in January 2010,因此它缺少近十年来对优化器和错误消息的改进。 gcc7 系列已经发布并稳定了一段时间。预计会错过这样一个旧编译器的优化,尤其是对于像 AVX 这样的现代指令集。
如果编译器内联一个使用静态而非动态绑定调用的成员函数,它可能能够优化掉 this
指针。举个简单的例子:
#include <iostream>
using std::cout;
using std::endl;
class example {
public:
int foo() const { return x; }
int foo(const int i) { return (x = i); }
private:
int x;
};
int main(void)
{
example e;
e.foo(10);
cout << e.foo() << endl;
}
带有 -march=x86-64 -O -S
标志的 GCC 7.3.0 能够将 cout << e.foo()
编译为三个指令:
movl , %esi
leaq _ZSt4cout(%rip), %rdi
call _ZNSolsEi@PLT
这是对 std::ostream::operator<<
的调用。请记住 cout << e.foo();
是 std::ostream::operator<< (cout, e.foo());
的语法糖。 operator<<(int)
可以有两种写法:static operator<< (ostream&, int)
,作为一个非成员函数,其中左边的操作数是一个显式参数,或 operator<<(int)
,作为一个成员函数,其中它是隐式 this
.
编译器能够推断出 e.foo()
将始终是常量 10
。由于 64 位 x86 调用约定是在寄存器中传递函数参数,因此编译为单个 movl
指令,它将第二个函数参数设置为 10
。 leaq
指令将第一个参数(可能是显式 ostream&
或隐式 this
)设置为 &cout
。然后程序对函数做了一个call
。
不过,在更复杂的情况下——例如,如果你有一个函数将 example&
作为参数——编译器需要查找 this
,因为 this
是什么告诉程序它正在使用哪个实例,因此,要查找哪个实例的 x
数据成员。
考虑这个例子:
class example {
public:
int foo() const { return x; }
int foo(const int i) { return (x = i); }
private:
int x;
};
int bar( const example& e )
{
return e.foo();
}
函数 bar()
被编译成一些样板和指令:
movl (%rdi), %eax
ret
你还记得前面的例子,x86-64 上的 %rdi
是第一个函数参数,调用 e.foo()
的隐式 this
指针。把它放在括号中,(%rdi)
,表示在该位置查找变量。 (因为 example
实例中唯一的数据是 x
,所以 &e.x
在这种情况下恰好与 &e
相同。)将内容移动到 %eax
设置 return 值。
在这种情况下,编译器需要 foo(/* example* this */)
的隐式 this
参数才能找到 &e
,从而找到 &e.x
。事实上,在成员函数(不是 static
)中,x
、this->x
和 (*this).x
都是同一个意思。
您的机器对 class 方法一无所知,它们是底层的正常功能。
因此方法必须通过始终传递指向当前对象的指针来实现,它只是隐含在 C++ 中,即 T Class::method(...)
只是 T Class_Method(Class* this, ...)
.
的语法糖
Python 或 Lua 等其他语言选择显式化,现代面向对象的 C API,如 Vulkan(不同于 OpenGL)使用类似的模式。
since I usually use it every single time I refer to a member variable or function.
当您引用成员变量或函数时,您总是使用this
。没有其他方法可以接触到会员。唯一的选择是隐式与显式符号。
我们回过头来看看this
之前是怎么做的,了解一下this
是什么
没有 OOP:
struct A {
int x;
};
void foo(A* that) {
bar(that->x)
}
使用 OOP 但明确地写 this
struct A {
int x;
void foo(void) {
bar(this->x)
}
};
使用更短的表示法:
struct A {
int x;
void foo(void) {
bar(x)
}
};
但区别仅在于源代码。所有的都被编译成同样的东西。如果你创建一个成员方法,编译器会为你创建一个指针参数,并将其命名为"this"。如果在引用成员时省略 this->
,编译器会很聪明地在大多数情况下为您插入它。而已。唯一的区别是源代码中少了 6 个字母。
当存在歧义时,明确地写 this
是有意义的,即另一个变量的名字就像你的成员变量:
struct A {
int x;
A(int x) {
this->x = x
}
};
有一些实例,例如 __thiscall,其中 OO 和非 OO 代码在 asm 中的结尾可能略有不同,但是只要指针在堆栈上传递然后优化到寄存器或者在 ECX 中从一开始就没有做到 "not a pointer".
"this" 也可以通过函数参数防止阴影,例如:
class Vector {
public:
double x,y,z;
void SetLocation(double x, double y, double z);
};
void Vector::SetLocation(double x, double y, double z) {
this->x = x; //Passed parameter assigned to member variable
this->y = y;
this->z = z;
}
(显然,不鼓励编写这样的代码。)
this
是一个指针。它就像是每个方法的一部分的隐式参数。您可以想象使用纯 C 函数并编写如下代码:
Socket makeSocket(int port) { ... }
void send(Socket *this, Value v) { ... }
Value receive(Socket *this) { ... }
Socket *mySocket = makeSocket(1234);
send(mySocket, someValue); // The subject, `mySocket`, is passed in as a param called "this", explicitly
Value newData = receive(socket);
在 C++ 中,类似的代码可能如下所示:
mySocket.send(someValue); // The subject, `mySocket`, is passed in as a param called "this"
Value newData = mySocket.receive();
this
确实是一个 运行 时间指针(尽管编译器隐式 提供 ),正如大多数答案中所重复的那样。它用于指示给定成员函数在调用时将对 class 的哪个实例进行操作;对于 class C
的任何给定实例 c
,当任何成员函数 cf()
被调用时,c.cf()
将提供一个 this
指针等于&c
(这自然也适用于类型 S
的任何结构 s
,当调用成员函数 s.sf()
时,将用于更清晰的演示)。它甚至可以像任何其他指针一样被 cv 限定,具有相同的效果(但不幸的是,由于特殊,语法不同);这通常用于 const
正确性,而很少用于 volatile
正确性。
template<typename T>
uintptr_t addr_out(T* ptr) { return reinterpret_cast<uintptr_t>(ptr); }
struct S {
int i;
uintptr_t address() const { return addr_out(this); }
};
// Format a given numerical value into a hex value for easy display.
// Implementation omitted for brevity.
template<typename T>
std::string hex_out_s(T val, bool disp0X = true);
// ...
S s[2];
std::cout << "Control example: Two distinct instances of simple class.\n";
std::cout << "s[0] address:\t\t\t\t" << hex_out_s(addr_out(&s[0]))
<< "\n* s[0] this pointer:\t\t\t" << hex_out_s(s[0].address())
<< "\n\n";
std::cout << "s[1] address:\t\t\t\t" << hex_out_s(addr_out(&s[1]))
<< "\n* s[1] this pointer:\t\t\t" << hex_out_s(s[1].address())
<< "\n\n";
示例输出:
Control example: Two distinct instances of simple class.
s[0] address: 0x0000003836e8fb40
* s[0] this pointer: 0x0000003836e8fb40
s[1] address: 0x0000003836e8fb44
* s[1] this pointer: 0x0000003836e8fb44
无法保证这些值,并且可以很容易地从一次执行更改为下一次执行;通过使用构建工具,在创建和测试程序时最容易观察到这一点。
从机制上讲,它类似于添加到每个成员函数参数列表开头的隐藏参数; x.f() cv
可以看作是 f(cv X* this)
的特殊变体,尽管由于语言原因具有不同的格式。实际上,there were recent proposals by both Stroustrup and Sutter to unify the call syntax of x.f(y)
and f(x, y)
, which would've made this implicit behaviour an explicit linguistic rule. It unfortunately was met with concerns that it may cause a few unwanted surprises for library developers, and thus not yet implemented; to my knowledge, the most recent proposal is a joint proposal, for f(x,y)
to be able to fall back on x.f(y)
if no f(x,y)
is found,类似于std::begin(x)
和成员函数x.begin()
.
之间的交互
在这种情况下,this
更类似于普通指针,程序员可以手动指定它。如果找到一个解决方案允许更健壮的形式而不违反最小惊讶原则(或带来任何其他问题),那么也可以隐式生成 this
的等价物作为普通指针非成员函数,以及。
与此相关,需要注意的一件重要事情是 this
是实例的地址,如该实例所见;虽然指针本身是 运行 时间的东西,但它并不总是具有您认为的价值。当查看具有更复杂继承层次结构的 classes 时,这变得很重要。具体来说,在查看包含成员函数的一个或多个基 class 与派生 class 本身的地址不同的情况时。特别想到三个案例:
请注意,这些是使用 MSVC 演示的,通过 undocumented -d1reportSingleClassLayout compiler parameter 输出 class 布局,因为我发现它比 GCC 或 Clang 等价物更容易阅读。
非标准布局:当class为标准布局时,实例的第一个数据成员的地址与地址完全相同实例本身;因此,this
可以说等同于第一个数据成员的地址。即使所述数据成员是基础 class 的成员,只要派生 class 继续遵循标准布局规则,这也适用。 ...相反,这也意味着如果派生的 class 不是 标准布局,则不再保证。
struct StandardBase {
int i;
uintptr_t address() const { return addr_out(this); }
};
struct NonStandardDerived : StandardBase {
virtual void f() {}
uintptr_t address() const { return addr_out(this); }
};
static_assert(std::is_standard_layout<StandardBase>::value, "Nyeh.");
static_assert(!std::is_standard_layout<NonStandardDerived>::value, ".heyN");
// ...
NonStandardDerived n;
std::cout << "Derived class with non-standard layout:"
<< "\n* n address:\t\t\t\t\t" << hex_out_s(addr_out(&n))
<< "\n* n this pointer:\t\t\t\t" << hex_out_s(n.address())
<< "\n* n this pointer (as StandardBase):\t\t" << hex_out_s(n.StandardBase::address())
<< "\n* n this pointer (as NonStandardDerived):\t" << hex_out_s(n.NonStandardDerived::address())
<< "\n\n";
示例输出:
Derived class with non-standard layout:
* n address: 0x00000061e86cf3c0
* n this pointer: 0x00000061e86cf3c0
* n this pointer (as StandardBase): 0x00000061e86cf3c8
* n this pointer (as NonStandardDerived): 0x00000061e86cf3c0
请注意 StandardBase::address()
提供了与 NonStandardDerived::address()
不同的 this
指针,即使在同一实例上调用也是如此。这是因为后者使用了vtable导致编译器插入了隐藏成员
class StandardBase size(4):
+---
0 | i
+---
class NonStandardDerived size(16):
+---
0 | {vfptr}
| +--- (base class StandardBase)
8 | | i
| +---
| <alignment member> (size=4)
+---
NonStandardDerived::$vftable@:
| &NonStandardDerived_meta
| 0
0 | &NonStandardDerived::f
NonStandardDerived::f this adjustor: 0
虚碱基classes:由于虚碱基尾随最衍生的class,this
提供给从虚拟基类继承的成员函数的指针将不同于提供给派生 class 本身成员的指针。
struct VBase {
uintptr_t address() const { return addr_out(this); }
};
struct VDerived : virtual VBase {
uintptr_t address() const { return addr_out(this); }
};
// ...
VDerived v;
std::cout << "Derived class with virtual base:"
<< "\n* v address:\t\t\t\t\t" << hex_out_s(addr_out(&v))
<< "\n* v this pointer:\t\t\t\t" << hex_out_s(v.address())
<< "\n* this pointer (as VBase):\t\t\t" << hex_out_s(v.VBase::address())
<< "\n* this pointer (as VDerived):\t\t\t" << hex_out_s(v.VDerived::address())
<< "\n\n";
示例输出:
Derived class with virtual base:
* v address: 0x0000008f8314f8b0
* v this pointer: 0x0000008f8314f8b0
* this pointer (as VBase): 0x0000008f8314f8b8
* this pointer (as VDerived): 0x0000008f8314f8b0
基 class' 成员函数再次提供了不同的 this
指针,因为 VDerived
继承的 VBase
具有不同的起始地址比 VDerived
本身。
class VDerived size(8):
+---
0 | {vbptr}
+---
+--- (virtual base VBase)
+---
VDerived::$vbtable@:
0 | 0
1 | 8 (VDerivedd(VDerived+0)VBase)
vbi: class offset o.vbptr o.vbte fVtorDisp
VBase 8 0 4 0
多重继承: 正如所料,多重继承很容易导致传递给一个成员函数的this
指针不同的情况比 this
指针传递给不同的成员函数,即使这两个函数是用同一个实例调用的。这可以用于除第一个以外的任何基 class 的成员函数,类似于使用非标准布局 classes 时(其中第一个之后的所有基 classes 开始于与派生的 class 本身不同的地址)...但是在 virtual
函数的情况下,当多个成员提供具有相同签名的虚函数时,这尤其令人惊讶。
struct Base1 {
int i;
virtual uintptr_t address() const { return addr_out(this); }
uintptr_t raw_address() { return addr_out(this); }
};
struct Base2 {
short s;
virtual uintptr_t address() const { return addr_out(this); }
uintptr_t raw_address() { return addr_out(this); }
};
struct Derived : Base1, Base2 {
bool b;
uintptr_t address() const override { return addr_out(this); }
uintptr_t raw_address() { return addr_out(this); }
};
// ...
Derived d;
std::cout << "Derived class with multiple inheritance:"
<< "\n (Calling address() through a static_cast reference, then the appropriate raw_address().)"
<< "\n* d address:\t\t\t\t\t" << hex_out_s(addr_out(&d))
<< "\n* d this pointer:\t\t\t\t" << hex_out_s(d.address()) << " (" << hex_out_s(d.raw_address()) << ")"
<< "\n* d this pointer (as Base1):\t\t\t" << hex_out_s(static_cast<Base1&>((d)).address()) << " (" << hex_out_s(d.Base1::raw_address()) << ")"
<< "\n* d this pointer (as Base2):\t\t\t" << hex_out_s(static_cast<Base2&>((d)).address()) << " (" << hex_out_s(d.Base2::raw_address()) << ")"
<< "\n* d this pointer (as Derived):\t\t\t" << hex_out_s(static_cast<Derived&>((d)).address()) << " (" << hex_out_s(d.Derived::raw_address()) << ")"
<< "\n\n";
示例输出:
Derived class with multiple inheritance:
(Calling address() through a static_cast reference, then the appropriate raw_address().)
* d address: 0x00000056911ef530
* d this pointer: 0x00000056911ef530 (0x00000056911ef530)
* d this pointer (as Base1): 0x00000056911ef530 (0x00000056911ef530)
* d this pointer (as Base2): 0x00000056911ef530 (0x00000056911ef540)
* d this pointer (as Derived): 0x00000056911ef530 (0x00000056911ef530)
我们希望每个 raw_address()
遵循相同的规则,因为每个明确地是一个单独的函数,因此 Base2::raw_address()
将 return 与 Derived::raw_address()
不同的值。但是因为我们知道派生函数总是调用最派生的形式,所以当从对 Base2
的引用调用时 address()
是如何正确的?这是由于一个叫做 "adjustor thunk" 的小编译器技巧造成的,它是一个助手,它接受一个基础 class 实例的 this
指针并将其调整为指向最派生的 class 相反,在必要时。
class Derived size(40):
+---
| +--- (base class Base1)
0 | | {vfptr}
8 | | i
| | <alignment member> (size=4)
| +---
| +--- (base class Base2)
16 | | {vfptr}
24 | | s
| | <alignment member> (size=6)
| +---
32 | b
| <alignment member> (size=7)
+---
Derived::$vftable@Base1@:
| &Derived_meta
| 0
0 | &Derived::address
Derived::$vftable@Base2@:
| -16
0 | &thunk: this-=16; goto Derived::address
Derived::address this adjustor: 0
如果你很好奇,请随意修改 this little program,看看如果你多次 运行 地址会发生什么变化,或者在它可能有的情况下与您预期不同的值。
我问自己 this
指针是否会被过度使用,因为我通常每次引用成员变量或函数时都会使用它。我想知道它是否会对性能产生影响,因为必须有一个指针每次都需要取消引用。所以我写了一些测试代码
struct A {
int x;
A(int X) {
x = X; /* And a second time with this->x = X; */
}
};
int main() {
A a(8);
return 0;
}
令人惊讶的是,即使使用 -O0
,它们也输出完全相同的汇编代码。
此外,如果我使用一个成员函数并在另一个成员函数中调用它,它会显示相同的行为。那么 this
指针只是编译时的东西而不是实际的指针吗?或者是否存在 this
实际上被翻译和取消引用的情况?我使用 GCC 4.4.3 顺便说一句。
this
始终必须存在。无论您是否显式使用它,都必须引用当前实例,这就是 this
给您的。
在这两种情况下,您都将通过 this
指针访问内存。只是在某些情况下可以省略。
So is the this pointer just a compile time thing and not an actual pointer?
它非常是一个运行时间的东西。它指的是调用成员函数的对象,自然那个对象可以存在运行时间。
编译时的事情是名称查找的工作原理。当编译器遇到 x = X
时,它必须弄清楚正在分配的 x
是什么。所以它查找它,并找到成员变量。由于 this->x
和 x
指的是同一事物,自然会得到相同的汇编输出。
编译后,每个符号只是一个地址,所以不会是运行时间问题。
无论如何,任何成员符号都会被编译为当前 class 中的一个偏移量,即使您没有使用 this
.
当在C++中使用name
时,它可以是以下之一。
- 在全局命名空间中(如
::name
),或在当前命名空间中,或在使用的命名空间中(当using namespace ...
被使用时) - 当前class
- 本地定义,在上层
- 本地定义,在当前区块
因此,当您编写代码时,编译器应该以查找符号名称的方式从当前块一直扫描到全局命名空间。
使用 this->name
帮助编译器缩小对 name
的搜索范围,使其仅在当前 class 范围内查找,这意味着它会跳过局部定义,如果在class范围,不要在全局范围内寻找。
它是一个实际的指针,正如标准指定的那样 (§12.2.2.1):
每次在 class 自己的代码中引用 非静态 成员变量或成员函数时,In the body of a non-static (12.2.1) member function, the keyword
this
is a prvalue expression whose value is the address of the object for which the function is called. The type ofthis
in a member function of a classX
isX*
.
this
实际上是隐含的。还需要它(无论是隐式还是显式),因为编译器需要在运行时将函数或变量绑定到实际对象。
显式使用它很少有用,除非您需要,例如,在成员函数中消除参数和成员变量之间的歧义。否则,如果没有它,编译器将使用参数 (See it live on Coliru).
隐藏成员变量这是一个简单的例子,说明 "this" 在运行时如何发挥作用:
#include <vector>
#include <string>
#include <iostream>
class A;
typedef std::vector<A*> News;
class A
{
public:
A(const char* n): name(n){}
std::string name;
void subscribe(News& n)
{
n.push_back(this);
}
};
int main()
{
A a1("Alex"), a2("Bob"), a3("Chris");
News news;
a1.subscribe(news);
a3.subscribe(news);
std::cout << "Subscriber:";
for(auto& a: news)
{
std::cout << " " << a->name;
}
return 0;
}
这几乎是 this
指针传入的寄存器。
在 asm 中,this
的工作方式与隐藏的第一个 arg 完全相同,因此成员函数 foo::add(int)
和非成员函数 [=15] =] 它采用 explicit foo*
第一个 arg 编译为完全相同的 asm.
struct foo {
int m;
void add(int a); // not inline so we get a stand-alone definition emitted
};
void foo::add(int a) {
this->m += a;
}
void add(foo *obj, int a) {
obj->m += a;
}
On the Godbolt compiler explorer,使用 System V ABI 为 x86-64 编译(RDI 中的第一个参数,RSI 中的第二个参数),我们得到:
# gcc8.2 -O3
foo::add(int):
add DWORD PTR [rdi], esi # memory-destination add
ret
add(foo*, int):
add DWORD PTR [rdi], esi
ret
I use GCC 4.4.3
那是 released in January 2010,因此它缺少近十年来对优化器和错误消息的改进。 gcc7 系列已经发布并稳定了一段时间。预计会错过这样一个旧编译器的优化,尤其是对于像 AVX 这样的现代指令集。
如果编译器内联一个使用静态而非动态绑定调用的成员函数,它可能能够优化掉 this
指针。举个简单的例子:
#include <iostream>
using std::cout;
using std::endl;
class example {
public:
int foo() const { return x; }
int foo(const int i) { return (x = i); }
private:
int x;
};
int main(void)
{
example e;
e.foo(10);
cout << e.foo() << endl;
}
带有 -march=x86-64 -O -S
标志的 GCC 7.3.0 能够将 cout << e.foo()
编译为三个指令:
movl , %esi
leaq _ZSt4cout(%rip), %rdi
call _ZNSolsEi@PLT
这是对 std::ostream::operator<<
的调用。请记住 cout << e.foo();
是 std::ostream::operator<< (cout, e.foo());
的语法糖。 operator<<(int)
可以有两种写法:static operator<< (ostream&, int)
,作为一个非成员函数,其中左边的操作数是一个显式参数,或 operator<<(int)
,作为一个成员函数,其中它是隐式 this
.
编译器能够推断出 e.foo()
将始终是常量 10
。由于 64 位 x86 调用约定是在寄存器中传递函数参数,因此编译为单个 movl
指令,它将第二个函数参数设置为 10
。 leaq
指令将第一个参数(可能是显式 ostream&
或隐式 this
)设置为 &cout
。然后程序对函数做了一个call
。
不过,在更复杂的情况下——例如,如果你有一个函数将 example&
作为参数——编译器需要查找 this
,因为 this
是什么告诉程序它正在使用哪个实例,因此,要查找哪个实例的 x
数据成员。
考虑这个例子:
class example {
public:
int foo() const { return x; }
int foo(const int i) { return (x = i); }
private:
int x;
};
int bar( const example& e )
{
return e.foo();
}
函数 bar()
被编译成一些样板和指令:
movl (%rdi), %eax
ret
你还记得前面的例子,x86-64 上的 %rdi
是第一个函数参数,调用 e.foo()
的隐式 this
指针。把它放在括号中,(%rdi)
,表示在该位置查找变量。 (因为 example
实例中唯一的数据是 x
,所以 &e.x
在这种情况下恰好与 &e
相同。)将内容移动到 %eax
设置 return 值。
在这种情况下,编译器需要 foo(/* example* this */)
的隐式 this
参数才能找到 &e
,从而找到 &e.x
。事实上,在成员函数(不是 static
)中,x
、this->x
和 (*this).x
都是同一个意思。
您的机器对 class 方法一无所知,它们是底层的正常功能。
因此方法必须通过始终传递指向当前对象的指针来实现,它只是隐含在 C++ 中,即 T Class::method(...)
只是 T Class_Method(Class* this, ...)
.
Python 或 Lua 等其他语言选择显式化,现代面向对象的 C API,如 Vulkan(不同于 OpenGL)使用类似的模式。
since I usually use it every single time I refer to a member variable or function.
当您引用成员变量或函数时,您总是使用this
。没有其他方法可以接触到会员。唯一的选择是隐式与显式符号。
我们回过头来看看this
之前是怎么做的,了解一下this
是什么
没有 OOP:
struct A {
int x;
};
void foo(A* that) {
bar(that->x)
}
使用 OOP 但明确地写 this
struct A {
int x;
void foo(void) {
bar(this->x)
}
};
使用更短的表示法:
struct A {
int x;
void foo(void) {
bar(x)
}
};
但区别仅在于源代码。所有的都被编译成同样的东西。如果你创建一个成员方法,编译器会为你创建一个指针参数,并将其命名为"this"。如果在引用成员时省略 this->
,编译器会很聪明地在大多数情况下为您插入它。而已。唯一的区别是源代码中少了 6 个字母。
当存在歧义时,明确地写 this
是有意义的,即另一个变量的名字就像你的成员变量:
struct A {
int x;
A(int x) {
this->x = x
}
};
有一些实例,例如 __thiscall,其中 OO 和非 OO 代码在 asm 中的结尾可能略有不同,但是只要指针在堆栈上传递然后优化到寄存器或者在 ECX 中从一开始就没有做到 "not a pointer".
"this" 也可以通过函数参数防止阴影,例如:
class Vector {
public:
double x,y,z;
void SetLocation(double x, double y, double z);
};
void Vector::SetLocation(double x, double y, double z) {
this->x = x; //Passed parameter assigned to member variable
this->y = y;
this->z = z;
}
(显然,不鼓励编写这样的代码。)
this
是一个指针。它就像是每个方法的一部分的隐式参数。您可以想象使用纯 C 函数并编写如下代码:
Socket makeSocket(int port) { ... }
void send(Socket *this, Value v) { ... }
Value receive(Socket *this) { ... }
Socket *mySocket = makeSocket(1234);
send(mySocket, someValue); // The subject, `mySocket`, is passed in as a param called "this", explicitly
Value newData = receive(socket);
在 C++ 中,类似的代码可能如下所示:
mySocket.send(someValue); // The subject, `mySocket`, is passed in as a param called "this"
Value newData = mySocket.receive();
this
确实是一个 运行 时间指针(尽管编译器隐式 提供 ),正如大多数答案中所重复的那样。它用于指示给定成员函数在调用时将对 class 的哪个实例进行操作;对于 class C
的任何给定实例 c
,当任何成员函数 cf()
被调用时,c.cf()
将提供一个 this
指针等于&c
(这自然也适用于类型 S
的任何结构 s
,当调用成员函数 s.sf()
时,将用于更清晰的演示)。它甚至可以像任何其他指针一样被 cv 限定,具有相同的效果(但不幸的是,由于特殊,语法不同);这通常用于 const
正确性,而很少用于 volatile
正确性。
template<typename T>
uintptr_t addr_out(T* ptr) { return reinterpret_cast<uintptr_t>(ptr); }
struct S {
int i;
uintptr_t address() const { return addr_out(this); }
};
// Format a given numerical value into a hex value for easy display.
// Implementation omitted for brevity.
template<typename T>
std::string hex_out_s(T val, bool disp0X = true);
// ...
S s[2];
std::cout << "Control example: Two distinct instances of simple class.\n";
std::cout << "s[0] address:\t\t\t\t" << hex_out_s(addr_out(&s[0]))
<< "\n* s[0] this pointer:\t\t\t" << hex_out_s(s[0].address())
<< "\n\n";
std::cout << "s[1] address:\t\t\t\t" << hex_out_s(addr_out(&s[1]))
<< "\n* s[1] this pointer:\t\t\t" << hex_out_s(s[1].address())
<< "\n\n";
示例输出:
Control example: Two distinct instances of simple class.
s[0] address: 0x0000003836e8fb40
* s[0] this pointer: 0x0000003836e8fb40
s[1] address: 0x0000003836e8fb44
* s[1] this pointer: 0x0000003836e8fb44
无法保证这些值,并且可以很容易地从一次执行更改为下一次执行;通过使用构建工具,在创建和测试程序时最容易观察到这一点。
从机制上讲,它类似于添加到每个成员函数参数列表开头的隐藏参数; x.f() cv
可以看作是 f(cv X* this)
的特殊变体,尽管由于语言原因具有不同的格式。实际上,there were recent proposals by both Stroustrup and Sutter to unify the call syntax of x.f(y)
and f(x, y)
, which would've made this implicit behaviour an explicit linguistic rule. It unfortunately was met with concerns that it may cause a few unwanted surprises for library developers, and thus not yet implemented; to my knowledge, the most recent proposal is a joint proposal, for f(x,y)
to be able to fall back on x.f(y)
if no f(x,y)
is found,类似于std::begin(x)
和成员函数x.begin()
.
在这种情况下,this
更类似于普通指针,程序员可以手动指定它。如果找到一个解决方案允许更健壮的形式而不违反最小惊讶原则(或带来任何其他问题),那么也可以隐式生成 this
的等价物作为普通指针非成员函数,以及。
与此相关,需要注意的一件重要事情是 this
是实例的地址,如该实例所见;虽然指针本身是 运行 时间的东西,但它并不总是具有您认为的价值。当查看具有更复杂继承层次结构的 classes 时,这变得很重要。具体来说,在查看包含成员函数的一个或多个基 class 与派生 class 本身的地址不同的情况时。特别想到三个案例:
请注意,这些是使用 MSVC 演示的,通过 undocumented -d1reportSingleClassLayout compiler parameter 输出 class 布局,因为我发现它比 GCC 或 Clang 等价物更容易阅读。
非标准布局:当class为标准布局时,实例的第一个数据成员的地址与地址完全相同实例本身;因此,
this
可以说等同于第一个数据成员的地址。即使所述数据成员是基础 class 的成员,只要派生 class 继续遵循标准布局规则,这也适用。 ...相反,这也意味着如果派生的 class 不是 标准布局,则不再保证。struct StandardBase { int i; uintptr_t address() const { return addr_out(this); } }; struct NonStandardDerived : StandardBase { virtual void f() {} uintptr_t address() const { return addr_out(this); } }; static_assert(std::is_standard_layout<StandardBase>::value, "Nyeh."); static_assert(!std::is_standard_layout<NonStandardDerived>::value, ".heyN"); // ... NonStandardDerived n; std::cout << "Derived class with non-standard layout:" << "\n* n address:\t\t\t\t\t" << hex_out_s(addr_out(&n)) << "\n* n this pointer:\t\t\t\t" << hex_out_s(n.address()) << "\n* n this pointer (as StandardBase):\t\t" << hex_out_s(n.StandardBase::address()) << "\n* n this pointer (as NonStandardDerived):\t" << hex_out_s(n.NonStandardDerived::address()) << "\n\n";
示例输出:
Derived class with non-standard layout: * n address: 0x00000061e86cf3c0 * n this pointer: 0x00000061e86cf3c0 * n this pointer (as StandardBase): 0x00000061e86cf3c8 * n this pointer (as NonStandardDerived): 0x00000061e86cf3c0
请注意
StandardBase::address()
提供了与NonStandardDerived::address()
不同的this
指针,即使在同一实例上调用也是如此。这是因为后者使用了vtable导致编译器插入了隐藏成员class StandardBase size(4): +--- 0 | i +--- class NonStandardDerived size(16): +--- 0 | {vfptr} | +--- (base class StandardBase) 8 | | i | +--- | <alignment member> (size=4) +--- NonStandardDerived::$vftable@: | &NonStandardDerived_meta | 0 0 | &NonStandardDerived::f NonStandardDerived::f this adjustor: 0
虚碱基classes:由于虚碱基尾随最衍生的class,
this
提供给从虚拟基类继承的成员函数的指针将不同于提供给派生 class 本身成员的指针。struct VBase { uintptr_t address() const { return addr_out(this); } }; struct VDerived : virtual VBase { uintptr_t address() const { return addr_out(this); } }; // ... VDerived v; std::cout << "Derived class with virtual base:" << "\n* v address:\t\t\t\t\t" << hex_out_s(addr_out(&v)) << "\n* v this pointer:\t\t\t\t" << hex_out_s(v.address()) << "\n* this pointer (as VBase):\t\t\t" << hex_out_s(v.VBase::address()) << "\n* this pointer (as VDerived):\t\t\t" << hex_out_s(v.VDerived::address()) << "\n\n";
示例输出:
Derived class with virtual base: * v address: 0x0000008f8314f8b0 * v this pointer: 0x0000008f8314f8b0 * this pointer (as VBase): 0x0000008f8314f8b8 * this pointer (as VDerived): 0x0000008f8314f8b0
基 class' 成员函数再次提供了不同的
this
指针,因为VDerived
继承的VBase
具有不同的起始地址比VDerived
本身。class VDerived size(8): +--- 0 | {vbptr} +--- +--- (virtual base VBase) +--- VDerived::$vbtable@: 0 | 0 1 | 8 (VDerivedd(VDerived+0)VBase) vbi: class offset o.vbptr o.vbte fVtorDisp VBase 8 0 4 0
多重继承: 正如所料,多重继承很容易导致传递给一个成员函数的
this
指针不同的情况比this
指针传递给不同的成员函数,即使这两个函数是用同一个实例调用的。这可以用于除第一个以外的任何基 class 的成员函数,类似于使用非标准布局 classes 时(其中第一个之后的所有基 classes 开始于与派生的 class 本身不同的地址)...但是在virtual
函数的情况下,当多个成员提供具有相同签名的虚函数时,这尤其令人惊讶。struct Base1 { int i; virtual uintptr_t address() const { return addr_out(this); } uintptr_t raw_address() { return addr_out(this); } }; struct Base2 { short s; virtual uintptr_t address() const { return addr_out(this); } uintptr_t raw_address() { return addr_out(this); } }; struct Derived : Base1, Base2 { bool b; uintptr_t address() const override { return addr_out(this); } uintptr_t raw_address() { return addr_out(this); } }; // ... Derived d; std::cout << "Derived class with multiple inheritance:" << "\n (Calling address() through a static_cast reference, then the appropriate raw_address().)" << "\n* d address:\t\t\t\t\t" << hex_out_s(addr_out(&d)) << "\n* d this pointer:\t\t\t\t" << hex_out_s(d.address()) << " (" << hex_out_s(d.raw_address()) << ")" << "\n* d this pointer (as Base1):\t\t\t" << hex_out_s(static_cast<Base1&>((d)).address()) << " (" << hex_out_s(d.Base1::raw_address()) << ")" << "\n* d this pointer (as Base2):\t\t\t" << hex_out_s(static_cast<Base2&>((d)).address()) << " (" << hex_out_s(d.Base2::raw_address()) << ")" << "\n* d this pointer (as Derived):\t\t\t" << hex_out_s(static_cast<Derived&>((d)).address()) << " (" << hex_out_s(d.Derived::raw_address()) << ")" << "\n\n";
示例输出:
Derived class with multiple inheritance: (Calling address() through a static_cast reference, then the appropriate raw_address().) * d address: 0x00000056911ef530 * d this pointer: 0x00000056911ef530 (0x00000056911ef530) * d this pointer (as Base1): 0x00000056911ef530 (0x00000056911ef530) * d this pointer (as Base2): 0x00000056911ef530 (0x00000056911ef540) * d this pointer (as Derived): 0x00000056911ef530 (0x00000056911ef530)
我们希望每个
raw_address()
遵循相同的规则,因为每个明确地是一个单独的函数,因此Base2::raw_address()
将 return 与Derived::raw_address()
不同的值。但是因为我们知道派生函数总是调用最派生的形式,所以当从对Base2
的引用调用时address()
是如何正确的?这是由于一个叫做 "adjustor thunk" 的小编译器技巧造成的,它是一个助手,它接受一个基础 class 实例的this
指针并将其调整为指向最派生的 class 相反,在必要时。class Derived size(40): +--- | +--- (base class Base1) 0 | | {vfptr} 8 | | i | | <alignment member> (size=4) | +--- | +--- (base class Base2) 16 | | {vfptr} 24 | | s | | <alignment member> (size=6) | +--- 32 | b | <alignment member> (size=7) +--- Derived::$vftable@Base1@: | &Derived_meta | 0 0 | &Derived::address Derived::$vftable@Base2@: | -16 0 | &thunk: this-=16; goto Derived::address Derived::address this adjustor: 0
如果你很好奇,请随意修改 this little program,看看如果你多次 运行 地址会发生什么变化,或者在它可能有的情况下与您预期不同的值。