【问题标题】:Is the "this" pointer just a compile time thing?“this”指针只是编译时的东西吗?
【发布时间】:2026-02-21 04:15:02
【问题描述】:

我问自己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 顺便说一句。

【问题讨论】:

标签: c++ gcc this this-pointer


【解决方案1】:

this 确实是一个运行时指针(尽管编译器隐含地提供),正如大多数答案中所迭代的那样。它用于指示给定成员函数在调用时要操作的类的哪个实例;对于C 类的任何给定实例c,当调用任何成员函数cf() 时,将提供c.cf() 等于&cthis 指针(这自然也适用于任何结构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 统一了x.f(y)f(x, y) 的调用语法,这会使这种隐含的行为成为一个显式的语言规则。不幸的是,有人担心它可能会给库开发人员带来一些意外的惊喜,因此尚未实施;据我所知,最近的提议是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 是实例的地址,正如该实例所见;虽然指针本身是一个运行时的东西,但它并不总是具有您认为它具有的值。当查看具有更复杂继承层次结构的类时,这变得相关。具体来说,当查看一个或多个包含成员函数的基类与派生类本身的地址不同的情况时。特别想到三个案例:

请注意,这些是使用 MSVC 演示的,类布局通过 undocumented -d1reportSingleClassLayout compiler parameter 输出,因为我发现它比 GCC 或 Clang 等价物更容易阅读。

  1. 非标准布局:当类是标准布局时,实例的第一个数据成员的地址与实例本身的地址完全相同;因此,this 可以说等价于第一个数据成员的地址。即使所述数据成员是基类的成员,只要派生类继续遵循标准布局规则,这也将成立。 ...相反,这也意味着如果派生类不是标准布局,则不再保证。

    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() 提供的 this 指针与 NonStandardDerived::address() 不同,即使在同一个实例上调用也是如此。这是因为后者使用 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
    
  2. 虚拟基类:由于虚拟基在最派生类之后,提供给从虚拟基继承的成员函数的this 指针将不同于提供给成员的指针派生类本身。

    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
    

    由于VDerived 继承的VBaseVDerived 本身的起始地址不同,基类的成员函数再次提供了不同的this 指针。

    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
    
  3. 多重继承:正如所料,多重继承很容易导致传递给一个成员函数的this指针与传递给另一个成员函数的this指针不同的情况,即使两个函数都使用相同的实例调用。这可能出现在除第一个基类之外的任何基类的成员函数中,类似于使用非标准布局类时(第一个基类之后的所有基类都从与派生类本身不同的地址开始)......但它对于 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() 将返回与Derived::raw_address() 不同的值。但是既然我们知道派生函数总是会调用最派生的形式,那么当从对Base2 的引用中调用address() 时如何正确?这是由于一个称为“调整器 thunk”的编译器小技巧造成的,它是一个帮助器,它获取基类实例的 this 指针并在必要时将其调整为指向最派生的类。

    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,看看如果您多次运行地址会发生什么变化,或者它的值可能与您预期的不同。

【讨论】:

    【解决方案2】:

    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();
    

    【讨论】:

      【解决方案3】:

      如果编译器内联了一个使用静态而不是动态绑定调用的成员函数,它可能能够优化掉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 &lt;&lt; e.foo() 编译为三个指令:

      movl    $10, %esi
      leaq    _ZSt4cout(%rip), %rdi
      call    _ZNSolsEi@PLT
      

      这是对std::ostream::operator&lt;&lt; 的呼叫。请记住cout &lt;&lt; e.foo();std::ostream::operator&lt;&lt; (cout, e.foo()); 的语法糖。 operator&lt;&lt;(int) 可以写成两种方式:static operator&lt;&lt; (ostream&amp;, int),作为非成员函数,其中左侧的操作数是显式参数,或 operator&lt;&lt;(int),作为成员函数,其中隐式为 this

      编译器能够推断出e.foo() 将始终是常量10。由于 64 位 x86 调用约定是在寄存器中传递函数参数,因此编译为单个 movl 指令,它将第二个函数参数设置为 10leaq 指令将第一个参数(可能是显式的ostream&amp; 或隐式的this)设置为&amp;cout。然后程序向函数发出call

      不过,在更复杂的情况下——例如,如果你有一个函数以 example&amp; 作为参数——编译器需要查找 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 是第一个函数参数,隐含的 this 指针用于调用 e.foo()。将其放在括号中,(%rdi),表示在该位置查找变量。 (由于example 实例中的唯一数据是x,因此在这种情况下&amp;e.x 恰好与&amp;e 相同。)将内容移至%eax 设置返回值。

      在这种情况下,编译器需要 foo(/* example* this */) 的隐式 this 参数才能找到 &amp;e 并因此找到 &amp;e.x。事实上,在成员函数内部(不是static),xthis-&gt;x(*this).x 都是同一个意思。

      【讨论】:

        【解决方案4】:

        “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->鼓励 x 以防止此类修改错误。
        • 是的,不幸的是 -Wshadow 没有与 -Wall 一起启用。 gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
        【解决方案5】:

        因为我通常在每次引用成员变量或函数时都使用它。

        当您引用成员变量或函数时,您总是使用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-&gt;,编译器很聪明,足以在大多数情况下为您插入它。而已。唯一的区别是源中少了 6 个字母。

        当存在歧义时,明确写 this 是有意义的,即另一个变量的名称与您的成员变量一样:

        struct A {
            int x;
        
            A(int x) {
                this->x = x
            }
        };
        

        在某些情况下,例如 __thiscall,OO 和非 OO 代码在 asm 中的结尾可能有所不同,但是每当指针在堆栈上传递然后从一开始就优化到寄存器或 ECX 中时不会不要让它“不是指针”。

        【讨论】:

          【解决方案6】:

          你的机器对类方法一无所知,它们是底层的普通函数。 因此方法必须通过始终传递一个指向当前对象的指针来实现,它只是隐含在 C++ 中,即T Class::method(...) 只是T Class_Method(Class* this, ...) 的语法糖。

          Python 或 Lua 等其他语言选择使其显式化,而 Vulkan 等现代面向对象的 C API(与 OpenGL 不同)使用类似的模式。

          【讨论】:

            【解决方案7】:

            编译后,每个符号都只是一个地址,所以不会是运行时问题。

            无论如何,任何成员符号都会编译为当前类中的偏移量,即使您没有使用this

            name 在 C++ 中使用时,它可以是以下之一。

            • 在全局命名空间中(如::name),或在当前命名空间中,或在使用的命名空间中(当using namespace ...被使用时)
            • 在当前班级中
            • 局部定义,在上块中
            • 本地定义,在当前块中

            因此,当您编写代码时,编译器应该扫描每一个,以查找符号名称的方式,从当前块到全局命名空间。

            使用this-&gt;name 有助于编译器将name 的搜索范围缩小到仅在当前类范围内查找它,这意味着它会跳过本地定义,如果在类范围内找不到,则不要在全局范围。

            【讨论】:

              【解决方案8】:

              它是一个实际的指针,正如标准所指定的(§12.2.2.1):

              在非静态 (12.2.1) 成员函数的主体中,关键字this 是一个prvalue 表达式,其值是调用该函数的对象的地址。类X的成员函数中this的类型是X*

              this 实际上是隐式的,每次您在类自己的代码中引用 非静态 成员变量或成员函数时。它也是需要的(无论是隐式还是显式),因为编译器需要在运行时将函数或变量绑定到实际对象。

              显式使用它很少有用,除非您需要,例如,在成员函数中消除参数和成员变量之间的歧义。否则,没有它,编译器将使用参数 (See it live on Coliru) 隐藏成员变量。

              【讨论】:

              • 从模板成员访问非依赖基类型的成员时,还需要显式编写this-&gt;。不经常需要,一个好的编译器会在你忘记它时准确诊断,但值得一提。
              • 在使用 IDE 开发时,编写“this->”也非常有用,因为 IDE 可以提供成员列表以供选择。 (就我个人而言,我倾向于不使用 IDE,但如果有人选择,利用它似乎是明智的。)
              • “显式使用它很少有用”,从编译器的角度来看,是的;从人类的角度来看,一些团队会将此作为样式规则强制执行,以防止人为错误引入的错误。
              • 我将发表与 Martin Bonner 关于使用 IDE 部分所做的相同的评论。否则,“这个”似乎永远不需要(至少对我来说)
              【解决方案9】:

              this 在非静态方法中始终必须存在。无论你是否明确使用它,你都必须有一个对当前实例的引用,这就是this 给你的。

              在这两种情况下,您都将通过this 指针访问内存。只是在某些情况下可以省略。

              【讨论】:

              • 本质上是语法糖(无论是包含还是省略,都是一种捷径)。
              【解决方案10】:

              这几乎是 How do objects work in x86 at the assembly level? 的复制品,我在其中评论了一些示例的 asm 输出,包括显示 this 指针传入了哪个寄存器。


              在 asm 中,this 的工作方式与隐藏的第一个参数完全相同,因此成员函数 foo::add(int) 和非成员函数 add 都采用 显式 em> 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
              

              我使用 GCC 4.4.3

              那是released in January 2010,因此它缺少对优化器和错误消息近十年的改进。 gcc7 系列已经稳定了一段时间。预计这种旧编译器会错过优化,尤其是对于像 AVX 这样的现代指令集。

              【讨论】:

                【解决方案11】:

                这是一个简单的例子,“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;
                }
                

                【讨论】:

                  【解决方案12】:

                  那么 this 指针只是编译时的东西而不是实际的指针吗?

                  它非常一个运行时的东西。它指的是调用成员函数的对象,自然该对象可以在运行时存在。

                  什么是编译时的事情是名称查找的工作原理。当编译器遇到x = X 时,它必须弄清楚分配的x 是什么。所以它查找它,并找到成员变量。由于this-&gt;xx 指的是同一个东西,自然你会得到相同的汇编输出。

                  【讨论】: