web-dev-qa-db-de.com

Ist der Zeiger "this" nur eine Sache zur Kompilierungszeit?

Ich habe mich gefragt, ob der Zeiger this überbeansprucht werden kann, da er normalerweise jedes Mal verwendet wird, wenn ich auf eine Membervariable oder -funktion verweise. Ich fragte mich, ob es Auswirkungen auf die Leistung haben könnte, da es einen Zeiger geben muss, der jedes Mal dereferenziert werden muss. Also habe ich einen Testcode geschrieben

struct A {
    int x;

    A(int X) {
        x = X; /* And a second time with this->x = X; */
    }
};

int main() {
    A a(8);

    return 0;
}

und überraschenderweise sogar mit -O0 Sie geben den exakt gleichen Assembler-Code aus.

Auch wenn ich eine Member-Funktion verwende und sie in einer anderen Member-Funktion aufrufe, zeigt sie dasselbe Verhalten. Ist der Zeiger this also nur eine Sache zur Kompilierungszeit und kein tatsächlicher Zeiger? Oder gibt es Fälle, in denen this tatsächlich übersetzt und dereferenziert wird? Ich benutze GCC 4.4.3 übrigens.

47
Yastanub

Ist dieser Zeiger also nur eine Sache zur Kompilierungszeit und kein tatsächlicher Zeiger?

Es ist sehr viel ist eine Laufzeitsache. Es bezieht sich auf das Objekt, für das die Mitgliedsfunktion aufgerufen wird. Natürlich kann dieses Objekt zur Laufzeit existieren.

Was ist eine Sache zur Kompilierungszeit ist, wie die Namenssuche funktioniert. Wenn ein Compiler auf x = X Stößt, muss er herausfinden, was diesem x zugewiesen wird. Also sucht es und findet die Mitgliedsvariable. Da sich this->x Und x auf dasselbe beziehen, erhalten Sie natürlich dieselbe Assembly-Ausgabe.

81
StoryTeller

Es ist ein tatsächlicher Zeiger, wie es in der Norm festgelegt ist (§12.2.2.1):

Im Hauptteil einer nicht statischen (12.2.1) Memberfunktion ist das Schlüsselwort this ein Wertausdruck, dessen Wert die Adresse des Objekts ist, für das die Funktion aufgerufen wird. Der Typ von this in einer Mitgliedsfunktion einer Klasse X ist X*.

this ist jedes Mal implizit, wenn Sie auf eine nicht statische Elementvariable oder Elementfunktion in einem eigenen Klassencode verweisen. Sie wird auch benötigt (entweder implizit oder explizit), da der Compiler die Funktion oder die Variable zur Laufzeit an ein tatsächliches Objekt binden muss.

Die explizite Verwendung ist nur selten sinnvoll, es sei denn, Sie müssen beispielsweise innerhalb einer Mitgliedsfunktion zwischen einem Parameter und einer Mitgliedsvariablen unterscheiden. Andernfalls spiegelt der Compiler die Member-Variable mit dem Parameter ( Live auf Colir ).

28
JBL

this muss immer existieren, wenn Sie sich in einer nicht statischen Methode befinden. Unabhängig davon, ob Sie es explizit verwenden oder nicht, müssen Sie einen Verweis auf die aktuelle Instanz haben, und das gibt Ihnen this.

In beiden Fällen greifen Sie über den Zeiger this auf den Speicher zu. Es ist nur so, dass Sie es in einigen Fällen weglassen können.

17

Dies ist fast ein Duplikat von Wie funktionieren Objekte in x86 auf Assembly-Ebene? , wobei ich die asm-Ausgabe einiger Beispiele kommentiere, einschließlich der Angabe, in welchem ​​Register der Zeiger this übergeben wurde .


In asm funktioniert this genauso wie ein verstecktes erstes Argument , also sowohl die Member-Funktion foo::add(int) als auch die Nichtmitglied add, das ein explizitfoo* first arg kompiliere genau das gleiche wie 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;
}

Im Godbolt-Compiler-Explorer Beim Kompilieren für x86-64 mit dem System V ABI (erstes Argument in RDI, zweites in RSI) erhalten wir:

# gcc8.2 -O3
foo::add(int):
        add     DWORD PTR [rdi], esi   # memory-destination add
        ret
add(foo*, int):
        add     DWORD PTR [rdi], esi
        ret

Ich benutze GCC 4.4.3

Das war veröffentlicht im Januar 201 , es fehlen also fast ein Jahrzehnt Verbesserungen am Optimierer und an Fehlermeldungen. Die gcc7-Serie ist seit einiger Zeit nicht mehr erhältlich und stabil. Erwarten Sie mit einem so alten Compiler verpasste Optimierungen, insbesondere für moderne Befehlssätze wie AVX.

16
Peter Cordes

Nach der Kompilierung ist jedes Symbol nur eine Adresse, daher kann es sich nicht um ein Laufzeitproblem handeln.

Jedes Mitgliedssymbol wird trotzdem zu einem Offset in der aktuellen Klasse kompiliert, auch wenn Sie this nicht verwendet haben.

Wenn name in C++ verwendet wird, kann es einer der folgenden sein.

  • Im globalen Namensraum (wie ::name) oder im aktuellen Namespace oder im verwendeten Namespace (wenn using namespace ... verwendet worden)
  • In der aktuellen Klasse
  • Lokale Definition im oberen Block
  • Lokale Definition im aktuellen Block

Wenn Sie Code schreiben, sollte der Compiler daher jeden so durchsuchen, dass er nach dem Symbolnamen sucht, und zwar vom aktuellen Block bis zum globalen Namespace.

Mit this->name hilft dem Compiler, die Suche nach name einzugrenzen, um nur im aktuellen Klassenbereich danach zu suchen. Dies bedeutet, dass lokale Definitionen übersprungen werden. Wenn sie nicht im Klassenbereich gefunden werden, suchen Sie sie nicht im globalen Bereich .

10
SHR

Hier ist ein einfaches Beispiel, wie "dies" zur Laufzeit nützlich sein kann:

#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;
}
7
Helmut Zeisel

Ihre Maschine weiß nichts über Klassenmethoden, sie sind normale Funktionen unter der Haube. Daher müssen Methoden implementiert werden, indem immer ein Zeiger auf das aktuelle Objekt übergeben wird. Dies ist nur in C++ implizit, d. H. T Class::method(...) ist nur syntaktischer Zucker für T Class_Method(Class* this, ...).

Andere Sprachen wie Python oder Lua machen es explizit und moderne objektorientierte C-APIs wie Vulkan (im Gegensatz zu OpenGL) verwenden ein ähnliches Muster.

7
Trass3r

da ich es normalerweise jedes Mal verwende, wenn ich auf eine Mitgliedsvariable oder -funktion verweise.

Sie immer verwenden this, wenn Sie auf eine Mitgliedsvariable oder -funktion verweisen. Es gibt einfach keinen anderen Weg, um Mitglieder zu erreichen. Die einzige Möglichkeit ist implizite oder explizite Notation.

Lassen Sie uns zurückgehen, um zu sehen, wie es vor this gemacht wurde, um zu verstehen, was this ist.

Ohne OOP:

struct A {
    int x;
};

void foo(A* that) {
    bar(that->x)
}

Mit OOP aber this explizit schreiben

struct A {
    int x;

    void foo(void) {
        bar(this->x)
    }
};

mit kürzerer Notation:

struct A {
    int x;

    void foo(void) {
        bar(x)
    }
};

Der Unterschied liegt jedoch nur im Quellcode. Alle sind zu derselben Sache zusammengestellt. Wenn Sie eine Member-Methode erstellen, erstellt der Compiler ein Zeigerargument für Sie und nennt es "this". Wenn Sie this-> Weglassen, wenn Sie auf ein Mitglied verweisen, ist der Compiler so clever, dass er es die meiste Zeit für Sie einfügt. Das ist es. Der einzige Unterschied sind 6 Buchstaben weniger in der Quelle.

Das Schreiben von this ist explizit sinnvoll, wenn eine Mehrdeutigkeit vorliegt, nämlich eine andere Variable, die genau wie Ihre Mitgliedsvariable benannt ist:

struct A {
    int x;

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

Es gibt einige Fälle wie __thiscall, in denen OO und Nicht-OO-Code möglicherweise in asm ein anderes Bit haben, aber immer dann, wenn der Zeiger auf stack und dann übergeben wird Von Anfang an auf ein Register oder in ECX optimiert, bedeutet dies nicht, dass es kein Zeiger ist.

5
Agent_L

"this" kann auch vor Abschattung durch einen Funktionsparameter schützen, zum Beispiel:

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;
}

(Offensichtlich wird davon abgeraten, solchen Code zu schreiben.)

3
Szak1

wenn der Compiler eine Memberfunktion einfügt, die mit statischer statt dynamischer Bindung aufgerufen wird, kann er möglicherweise den Zeiger this optimieren. Nehmen Sie dieses einfache Beispiel:

#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;
}

GCC 7.3.0 mit dem Flag -march=x86-64 -O -S Kann cout << e.foo() zu drei Befehlen kompilieren:

movl    $10, %esi
leaq    _ZSt4cout(%rip), %rdi
call    [email protected]

Dies ist ein Aufruf von std::ostream::operator<<. Denken Sie daran, dass cout << e.foo(); syntaktischer Zucker für std::ostream::operator<< (cout, e.foo()); ist. Und operator<<(int) kann auf zwei Arten geschrieben werden: static operator<< (ostream&, int) als Nicht-Member-Funktion, wobei der linke Operand ein expliziter Parameter ist, oder operator<<(int) als eine Mitgliedsfunktion, bei der es sich implizit um this handelt.

Der Compiler konnte ableiten, dass e.foo() immer die Konstante 10 Sein wird. Da die 64-Bit-x86-Aufrufkonvention darin besteht, Funktionsargumente in Registern zu übergeben, wird dies bis zur einzelnen Anweisung movl kompiliert, die den zweiten Funktionsparameter auf 10 Setzt. Die Anweisung leaq setzt das erste Argument (das ein explizites ostream& Oder das implizite this sein kann) auf &cout. Dann macht das Programm ein call zur Funktion.

In komplexeren Fällen, zum Beispiel wenn Sie eine Funktion haben, die einen example& Als Parameter verwendet, muss der Compiler this nachschlagen, da this das ist, was das angibt Programmieren Sie, mit welcher Instanz es arbeitet und daher, mit welchem ​​x -Datenelement die Instanz gesucht werden soll.

Betrachten Sie dieses Beispiel:

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

Die Funktion bar() wird zu einem Stück Boilerplate kompiliert und die Anweisung:

movl    (%rdi), %eax
ret

Sie erinnern sich aus dem vorherigen Beispiel, dass %rdi Auf x86-64 das erste Funktionsargument ist, der implizite Zeiger this für den Aufruf von e.foo(). Wenn Sie es in Klammern setzen, bedeutet (%rdi), Dass Sie die Variable an dieser Stelle nachschlagen. (Da die einzigen Daten in einer example -Instanz x sind, entspricht &e.x In diesem Fall zufällig &e.) Verschieben des Inhalts nach %eax Setzt den Rückgabewert.

In diesem Fall benötigte der Compiler das implizite Argument this für foo(/* example* this */), um &e Und damit &e.x Finden zu können. Tatsächlich bedeuten in einer Mitgliedsfunktion (das ist nicht static) x, this->x Und (*this).x Alle dasselbe.

3
Davislor

this ist in der Tat ein Laufzeitzeiger (obwohl implizit geliefert vom Compiler), wie in den meisten Antworten wiederholt wurde. Es wird verwendet, um anzugeben, auf welche Instanz einer Klasse eine bestimmte Member-Funktion beim Aufruf angewendet werden soll. Wenn für eine bestimmte Instanz c der Klasse C eine Mitgliedsfunktion cf() aufgerufen wird, wird c.cf() eine this übergeben. Zeiger gleich &c (dies gilt natürlich auch für jede Struktur s vom Typ S, wenn die Member-Funktion s.sf() aufgerufen wird, wie sie für die Bereinigung verwendet werden soll Demonstrationen). Es kann sogar wie jeder andere Zeiger cv-qualifiziert werden, mit den gleichen Effekten (aber leider nicht der gleichen Syntax, da es speziell ist). Dies wird häufig für die const Korrektheit und viel seltener für die volatile Korrektheit verwendet.

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";

Beispielausgabe:

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

Diese Werte sind nicht garantiert und können sich leicht von einer Ausführung zur nächsten ändern. Dies kann am einfachsten beim Erstellen und Testen eines Programms mithilfe von Build-Tools beobachtet werden.


Mechanisch ähnelt es einem versteckten Parameter, der am Anfang der Argumentliste jeder Mitgliedsfunktion hinzugefügt wird. x.f() cv kann als spezielle Variante von f(cv X* this) angesehen werden, allerdings aus sprachlichen Gründen mit einem anderen Format. Tatsächlich es gab kürzlich Vorschläge von Stroustrup und Sutter , die Aufrufsyntax von x.f(y) und f(x, y) zu vereinheitlichen, was dieses implizite Verhalten zu einem Problem gemacht hätte explizite Sprachregel. Es wurde leider befürchtet, dass es für Bibliotheksentwickler einige unerwünschte Überraschungen bereiten könnte und daher noch nicht implementiert wurde. Meines Wissens ist der jüngste Vorschlag ein gemeinsamer Vorschlag, damit f(x,y) auf x.f(y) zurückgreifen kann, wenn keine f(x,y) gefunden wird , ähnlich der Interaktion zwischen zB std::begin(x) und member function x.begin().

In diesem Fall ähnelt this eher einem normalen Zeiger, und der Programmierer kann ihn manuell angeben. Wenn eine Lösung gefunden wird, die eine robustere Form zulässt, ohne das Prinzip des geringsten Erstaunens zu verletzen (oder andere Bedenken zu zerstreuen), dann kann ein Äquivalent zu this auch implizit als normaler Zeiger generiert werden auch für Nicht-Mitglieder-Funktionen.


Zu beachten ist auch, dass this die Adresse der Instanz ist, von dieser Instanz aus gesehen; Während der Zeiger selbst eine Laufzeitsache ist, hat er nicht immer den Wert, von dem Sie denken, dass er ihn hat. Dies wird relevant, wenn Klassen mit komplexeren Vererbungshierarchien betrachtet werden. Insbesondere, wenn Fälle betrachtet werden, in denen eine oder mehrere Basisklassen, die Elementfunktionen enthalten, nicht dieselbe Adresse wie die abgeleitete Klasse selbst haben. Dabei fallen mir insbesondere drei Fälle ein:

Beachten Sie, dass dies mit MSVC demonstriert wird, wobei Klassenlayouts über den Compiler-Parameter ndokumentiert -d1reportSingleClassLayout ausgegeben werden, da ich finde, dass er leichter lesbar ist als GCC- oder Clang-Entsprechungen.

  1. Nicht standardmäßiges Layout: Wenn es sich bei einer Klasse um ein standardmäßiges Layout handelt, ist die Adresse des ersten Datenelements einer Instanz genau mit der Adresse der Instanz selbst identisch. Daher kann gesagt werden, dass this der Adresse des ersten Datenmitglieds entspricht. Dies gilt auch dann, wenn das Datenelement Mitglied einer Basisklasse ist, solange die abgeleitete Klasse weiterhin den Standardlayoutregeln folgt. ... Umgekehrt bedeutet dies auch, dass wenn die abgeleitete Klasse ist nicht Standardlayout ist, dies nicht mehr garantiert ist.

    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";
    

    Beispielausgabe:

    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
    

    Beachten Sie, dass StandardBase::address() mit einem anderen this Zeiger als NonStandardDerived::address() geliefert wird, auch wenn er auf derselben Instanz aufgerufen wird. Dies liegt daran, dass der Compiler durch die Verwendung einer vtable ein ausgeblendetes Element eingefügt hat.

    class StandardBase      size(4):
            +---
     0      | i
            +---
    class NonStandardDerived        size(16):
            +---
     0      | {vfptr}
            | +--- (base class StandardBase)
     8      | | i
            | +---
            | <alignment member> (size=4)
            +---
    NonStandardDerived::[email protected]:
            | &NonStandardDerived_meta
            |  0
     0      | &NonStandardDerived::f 
    NonStandardDerived::f this adjustor: 0
    
  2. Virtuelle Basisklassen: Aufgrund von virtuellen Basen, die nach der am häufigsten abgeleiteten Klasse folgen, wird der Zeiger this an eine Mitgliedsfunktion übergeben, die von a geerbt wurde Die virtuelle Basis unterscheidet sich von der Basis, die den Mitgliedern der abgeleiteten Klasse selbst zur Verfügung gestellt wird.

    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";
    

    Beispielausgabe:

    Derived class with virtual base:
    * v address:                                    0x0000008f8314f8b0
    * v this pointer:                               0x0000008f8314f8b0
    * this pointer (as VBase):                      0x0000008f8314f8b8
    * this pointer (as VDerived):                   0x0000008f8314f8b0
    

    Wieder wird die Member-Funktion der Basisklasse mit einem anderen this Zeiger versorgt, da VDeriveds geerbtes VBase eine andere Startadresse hat als VDerived selbst .

    class VDerived  size(8):
            +---
     0      | {vbptr}
            +---
            +--- (virtual base VBase)
            +---
    VDerived::[email protected]:
     0      | 0
     1      | 8 (VDerivedd(VDerived+0)VBase)
    vbi:       class  offset o.vbptr  o.vbte fVtorDisp
               VBase       8       0       4 0
    
  3. Mehrfachvererbung: Wie zu erwarten ist, kann Mehrfachvererbung leicht dazu führen, dass sich der an eine Memberfunktion übergebene Zeiger this von unterscheidet Der Zeiger this wurde an eine andere Member-Funktion übergeben, auch wenn beide Funktionen mit derselben Instanz aufgerufen werden. Dies kann für Member-Funktionen einer anderen als der ersten Basisklasse auftreten, ähnlich wie beim Arbeiten mit nicht standardmäßigen Layout-Klassen (bei denen alle Basisklassen nach der ersten an einer anderen Adresse beginnen als die abgeleitete Klasse selbst) ... aber es Dies kann insbesondere bei virtual Funktionen überraschend sein, wenn mehrere Member virtuelle Funktionen mit derselben Signatur bereitstellen.

    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";
    

    Beispielausgabe:

    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)
    

    Wir würden erwarten, dass jede raw_address() denselben Regeln unterliegt, da jede explizit eine separate Funktion ist und daher Base2::raw_address() einen anderen Wert als Derived::raw_address() zurückgibt. Aber da wir wissen, dass abgeleitete Funktionen immer die am häufigsten abgeleitete Form aufrufen, wie ist address() korrekt, wenn sie von einem Verweis auf Base2 Aufgerufen werden? Dies ist auf einen kleinen Compiler-Trick zurückzuführen, der als "adjustor thunk" bezeichnet wird. Hierbei handelt es sich um einen Helfer, der den Zeiger this einer Basisklasseninstanz verwendet und ihn gegebenenfalls so anpasst, dass er auf die am häufigsten abgeleitete Klasse verweist.

    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::[email protected]@:
            | &Derived_meta
            |  0
     0      | &Derived::address 
    Derived::[email protected]@:
            | -16
     0      | &thunk: this-=16; goto Derived::address 
    Derived::address this adjustor: 0
    

Wenn Sie neugierig sind, können Sie gerne mit diesem kleinen Programm basteln, um zu sehen, wie sich die Adressen ändern, wenn Sie es mehrmals ausführen, oder in Fällen, in denen es möglicherweise einen anderen Wert als hat Sie können erwarten.

3
Justin Time

this ist ein Zeiger. Es ist wie ein impliziter Parameter, der Bestandteil jeder Methode ist. Sie können sich vorstellen, einfache C-Funktionen zu verwenden und Code zu schreiben wie:

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

In C++ könnte ähnlicher Code folgendermaßen aussehen:

mySocket.send(someValue); // The subject, `mySocket`, is passed in as a param called "this"
Value newData = mySocket.receive();
2
Alexander