wake-up-neo.net

Wie bestimmen Sie die Größe eines Objekts in C++?

Angenommen, ich habe eine Klasse Temp:

class Temp
{
    public:
        int function1(int foo) { return 1; }
        void function2(int bar) { foobar = bar; }

    private:
        int foobar;
};

Wenn ich ein Objekt der Klasse Temp erstelle, wie würde ich berechnen, wie viel Speicherplatz benötigt wird und wie es im Speicher dargestellt wird (z. B. | 4 Byte für foobar | 8 Byte für Funktion1 | etc |)

36
Joel

In erster Näherung ist die Größe eines Objekts die Summe der Größen seiner konstituierenden Datenelemente. Sie können sicher sein, dass es niemals kleiner sein wird.

Genauer gesagt, der Compiler ist berechtigt, einen Abstand zwischen Datenelementen einzufügen, um sicherzustellen, dass jedes Datenelement die Ausrichtungsanforderungen der Plattform erfüllt. Einige Plattformen sind hinsichtlich der Ausrichtung sehr streng, während andere (x86) eher fehlerverzeihend sind, sich jedoch bei korrekter Ausrichtung deutlich verbessern. Daher kann sogar die Compiler-Optimierungseinstellung die Objektgröße beeinflussen.

Vererbung und virtuelle Funktionen sind eine zusätzliche Komplikation. Wie andere bereits gesagt haben, beanspruchen die Mitgliedsfunktionen Ihrer Klasse selbst keinen "pro Objekt" -Raum, aber das Vorhandensein von virtuellen Funktionen in der Schnittstelle dieser Klasse impliziert im Allgemeinen das Vorhandensein einer virtuellen Tabelle, im Wesentlichen einer Nachschlagetabelle der verwendeten Funktionszeiger Die dynamische Funktionsimplementierung dynamisch auflösen, die zur Laufzeit aufgerufen werden soll. Auf die virtuelle Tabelle (vtbl) wird im Allgemeinen über einen in jedem Objekt gespeicherten Zeiger zugegriffen.

Abgeleitete Klassenobjekte umfassen auch alle Datenmitglieder ihrer Basisklassen.

Schließlich geben Zugriffsbezeichner (öffentlich, privat, geschützt) dem Compiler einen gewissen Spielraum beim Packen von Datenmitgliedern.

Die kurze Antwort lautet, dass sizeof (myObj) oder sizeof (MyClass) immer die richtige Größe eines Objekts angibt, das Ergebnis ist jedoch nicht immer leicht vorhersagbar.

60
Drew Hall
sizeof(Temp)

wird dir die Größe geben. Am wahrscheinlichsten sind es 4 Bytes (bei einer Vielzahl von Annahmen), und das ist nur für das int. Die Funktionen nehmen pro Objekt keinen Platz ein, sie werden einmal kompiliert und bei jeder Verwendung vom Compiler verknüpft.

Es ist unmöglich genau zu sagen, wie das Objektlayout ist, der Standard definiert jedoch nicht die binäre Darstellung von Objekten.

Es gibt ein paar Dinge, die Sie bei binären Repräsentationen beachten sollten, da sie nicht unbedingt die Summe der Bytes der Datenelemente sind, z. B. durch Strukturauffüllung .

17
Todd Gardner

Ich habe mich schon immer über so etwas gewundert und mich entschlossen, eine vollständige Antwort zu finden. Es geht darum, was Sie vielleicht erwarten, und es ist vorhersehbar (yay)! Mit den folgenden Informationen sollten Sie also in der Lage sein, die Größe einer Klasse vorherzusagen.

Ich habe festgestellt, dass bei Verwendung von Visual Studio Community 2017 (Version 15.2) im Release-Modus alle Optimierungen deaktiviert und RTTI ( Informationen zum Laufzeit-Typ ) deaktiviert sind folgende:


Kurzantwort:

Als erstes:

  • In 32 (x86) Bit <size of pointer> == 4 Bytes
  • In 64 (x64) Bit <size of pointer> == 8 Bytes
  • Wenn ich "Vererbung virtueller Klassen" sage, meine ich beispielsweise: class ChildClass: virtual public ParentClass

Meine Erkenntnisse sind nun:

  • leere Klassen sind 1 Byte
  • die Vererbung einer leeren Klasse beträgt noch 1 Byte
  • leere Klassen mit Funktionen sind immer noch 1 Byte (?! siehe Hinweis unten zur Erläuterung)
  • die Vererbung einer leeren Klasse mit einer Funktion beträgt noch 1 Byte
  • das Hinzufügen einer Variablen zu einer leeren Klasse umfasst <size of variable> Bytes
  • das Erben einer Klasse mit einer Variablen und das Hinzufügen einer weiteren Variablen sind <size of variables> Bytes
  • das Erben einer Klasse und das Überschreiben ihrer Funktion fügt eine vtable hinzu (weitere Erläuterungen finden Sie im Abschnitt Schlussfolgerungen ) und hat die Größe <size of pointer> Bytes
  • durch einfaches Deklarieren einer virtuellen Funktion wird auch eine vtable hinzugefügt, wodurch sie zu <size of pointer> Bytes wird
  • die virtuelle Klassenvererbung einer leeren Klasse (mit oder ohne Member-Funktion) fügt ebenfalls eine vtable hinzu und erstellt die Bytes der Klasse <size of pointer>
  • die virtuelle Klassenvererbung einer nicht leeren Klasse fügt auch eine vtable hinzu, was jedoch etwas kompliziert wird: Sie fügt <size of pointer> Bytes zur Gesamtsumme hinzu , Umschließen aller Mitgliedsvariablen in so viele <size of pointer> - Byte-Inkremente, wie zur Abdeckung von <total size of member variables> erforderlich sind - ja, Sie haben das richtig gelesen ... (siehe meine Vermutung, was in vor sich geht. Schlussfolgerungen ...)

Beachten Sie , dass ich sogar versucht habe, mit function () einen Text zu schreiben, eine Instanz der Klasse zu erstellen und die Funktion aufzurufen. es ändert nicht die Größe der Funktionsklasse (es ist keine Optimierung)! Ich war etwas überrascht, aber es macht tatsächlich Sinn: Mitgliedsfunktionen ändern sich nicht, sodass sie außerhalb der Klasse selbst gespeichert werden können.

Schlussfolgerungen:

  • Leere Klassen sind 1 Byte, da dies das Minimum ist, um im Speicher vorhanden zu sein. Sobald Daten oder vtable-Daten hinzugefügt wurden, beginnen Sie mit der Zählung bei 0 Byte.
  • Das Hinzufügen einer ( nicht-virtuellen ) Elementfunktion ändert nichts an der Größe, da die Elementfunktion extern gespeichert wird.
  • Das Deklarieren einer Mitgliedsfunktion als virtuell (auch wenn die Klasse nicht überschrieben wird!) Oder das Überschreiben einer Mitgliedsfunktion in einer untergeordneten Klasse fügt das hinzu, was als "vtable" oder "virtuelle Funktionstabelle" bezeichnet wird für Dynamic Dispatch (das ist aber wirklich super toll und ich kann es nur empfehlen). Diese vtable belegt <size of pointer> Bytes und fügt dieser Klasse <size of pointer> Bytes hinzu. Diese vtable kann natürlich nur einmal pro Klasse existieren (entweder oder nicht).
  • Durch Hinzufügen einer Elementvariablen wird die Größe der Klasse um diese Elementvariable erhöht, unabhängig davon, ob sich die Elementvariable in der übergeordneten oder untergeordneten Klasse befindet (die übergeordnete Klasse behält jedoch ihre eigene Größe bei).
  • Die Vererbung virtueller Klassen ist der einzige Teil, der kompliziert wird ... Also ... ich denke, nach ein wenig Experimentieren geht es um Folgendes: Die Größe der Klasse wird tatsächlich in <size of pointer> Bytes erhöht, auch wenn dies der Fall ist muss nicht so viel Speicher verbrauchen, da es einen vtable "helper block" für jedes <size of pointer> Byte Speicher oder so hinzufügt ...

Lange Antwort:

Ich habe dies alles mit diesem Code bestimmt:

#include <iostream>

using namespace std;

class TestA
{

};

class TestB: public TestA
{

};

class TestC: virtual public TestA
{

};

class TestD
{
    public:
        int i;
};

class TestE: public TestD
{
    public:
        int j;
};

class TestF: virtual public TestD
{
    public:
        int j;
};

class TestG
{
    public:
        void function()
        {

        }
};

class TestH: public TestG
{
    public:
        void function()
        {

        }
};

class TestI: virtual public TestG
{
    public:
        void function()
        {

        }
};

class TestJ
{
    public:
        virtual void function()
        {

        }
};

class TestK: public TestJ
{
    public:
        void function() override
        {

        }
};

class TestL: virtual public TestJ
{
    public:
        void function() override
        {

        }
};

void main()
{
    cout << "int:\t\t" << sizeof(int) << "\n";
    cout << "TestA:\t\t" << sizeof(TestA) << "\t(empty class)\n";
    cout << "TestB:\t\t" << sizeof(TestB) << "\t(inheriting empty class)\n";
    cout << "TestC:\t\t" << sizeof(TestC) << "\t(virtual inheriting empty class)\n";
    cout << "TestD:\t\t" << sizeof(TestD) << "\t(int class)\n";
    cout << "TestE:\t\t" << sizeof(TestE) << "\t(inheriting int + int class)\n";
    cout << "TestF:\t\t" << sizeof(TestF) << "\t(virtual inheriting int + int class)\n";
    cout << "TestG:\t\t" << sizeof(TestG) << "\t(function class)\n";
    cout << "TestH:\t\t" << sizeof(TestH) << "\t(inheriting function class)\n";
    cout << "TestI:\t\t" << sizeof(TestI) << "\t(virtual inheriting function class)\n";
    cout << "TestJ:\t\t" << sizeof(TestJ) << "\t(virtual function class)\n";
    cout << "TestK:\t\t" << sizeof(TestK) << "\t(inheriting overriding function class)\n";
    cout << "TestL:\t\t" << sizeof(TestL) << "\t(virtual inheriting overriding function class)\n";

    cout << "\n";
    system("pause");
}

Ausgabe:

32 (x86) Bits:

int:            4
TestA:          1       (empty class)
TestB:          1       (inheriting empty class)
TestC:          4       (virtual inheriting empty class)
TestD:          4       (int class)
TestE:          8       (inheriting int + int class)
TestF:          12      (virtual inheriting int + int class)
TestG:          1       (function class)
TestH:          1       (inheriting function class)
TestI:          4       (virtual inheriting function class)
TestJ:          4       (virtual function class)
TestK:          4       (inheriting overriding function class)
TestL:          8       (virtual inheriting overriding function class)

64 (x64) Bits:

int:            4
TestA:          1       (empty class)
TestB:          1       (inheriting empty class)
TestC:          8       (virtual inheriting empty class)
TestD:          4       (int class)
TestE:          8       (inheriting int + int class)
TestF:          24      (virtual inheriting int + int class)
TestG:          1       (function class)
TestH:          1       (inheriting function class)
TestI:          8       (virtual inheriting function class)
TestJ:          8       (virtual function class)
TestK:          8       (inheriting overriding function class)
TestL:          16      (virtual inheriting overriding function class)

Wenn Sie Informationen zur Mehrfachvererbung wünschen, finden Sie es selbst heraus! -.-

10
Andrew

Wenn Sie detaillierte Informationen darüber wünschen, wie Objekte zur Laufzeit im Speicher dargestellt werden, ist die ABI-Spezifikation ( Application Binary Interface ) die richtige Stelle. Sie müssen feststellen, welche ABI Ihr Compiler implementiert. GCC-Versionen 3.2 und höher implementieren beispielsweise die Itanium C++ ABI .

8
Paul Morie

Methoden gehören zur Klasse, nicht zu einem bestimmten instanziierten Objekt.

Wenn keine virtuellen Methoden vorhanden sind, entspricht die Größe eines Objekts der Summe der Größe seiner nicht statischen Elemente plus dem optionalen Auffüllen zwischen den Elementen für die Ausrichtung. Die Member werden wahrscheinlich sequentiell im Speicher abgelegt, aber die Spezifikation garantiert nicht die Reihenfolge zwischen Abschnitten mit unterschiedlichen Zugriffsspezifikationen oder die Anordnung in Bezug auf das Layout der Oberklassen.

Bei vorhandenen virtuellen Methoden kann zusätzlicher Platz für vtable und andere RTTI-Informationen benötigt werden.

Auf den meisten Plattformen wird ausführbarer Code in den schreibgeschützten .text (oder ähnlich benannten) Abschnitt der ausführbaren Datei oder Bibliothek geschrieben und niemals irgendwo kopiert. Wenn class Temp über die Methode public: int function1(int) verfügt, können die Temp-Metadaten einen Zeiger auf eine _ZN4Temp9function1Ei-Funktion (entstellter Name können je nach Compiler unterschiedlich sein) für die eigentliche Implementierung aufweisen, sie enthalten jedoch niemals den eingebetteten ausführbaren Code.

6
ephemient

Elementfunktionen berücksichtigen nicht die Größe der Objekte einer bestimmten Klasse. Die Größe des Objekts hängt nur von den Mitgliedsvariablen ab. Bei Klassen, die virtuelle Funktionen enthalten, wird die VPTR zum Objektlayout hinzugefügt. Die Größe der Objekte ist also im Wesentlichen die Größe der Elementvariablen + die Größe der VPTRs. Dies kann manchmal nicht zutreffen, da Compiler versuchen, Mitgliedsvariablen an der DWORD-Grenze zu finden.

4
Canopus

Wenn Sie Microsoft Visual C++ verwenden, gibt es eine Compileroption, die Sie darüber informiert, wie groß Ihr Objekt tatsächlich ist:/d1reportSingleClassLayout

Es ist undokumentiert, außer für dieses Video von Lavavej http://channel9.msdn.com/Shows/Going+Deep/C9-Lectures-Stephan-T-Lavavej-Advanced-STL-3-of-n

3
Johannes Gerer

Wenn Sie das Layout einer bestimmten Struktur untersuchen möchten, kann auch das Makro offsetof(s,member) von Nutzen sein. Hier erfahren Sie, wie weit von der Basisadresse einer Struktur ein bestimmtes Mitglied entfernt ist:

struct foo {
  char *a;
  int b;
};

// Print placement of foo's members
void printFoo() {
  printf("foo->a is %zu bytes into a foo\n", offsetof(struct foo, a));
  printf("foo->b is %zu bytes into a foo\n", offsetof(struct foo, b));
}

int main() {
  printFoo();
  return 0;
}

Würde auf einem typischen 32-Bit-Computer drucken:

foo->a is 0 bytes into a foo
foo->b is 4 bytes into a foo

Auf einer typischen 64-Bit-Maschine würde es drucken

foo->a is 0 bytes into a foo
foo->b is 8 bytes into a foo
2
Phil Miller

Das kann helfen.

Darüber hinaus werden Klassenfunktionen wie jede andere Funktion dargestellt. Die einzige Magie, die C++ für die Funktion ausübt, besteht darin, die Funktionsnamen zu manipulieren, um eine bestimmte Funktion eindeutig mit einem bestimmten Parametersatz innerhalb einer bestimmten Klasse zu identifizieren.

0
sybreon

Es gibt einen Utility-Aufruf pahole (für 'Poke-A-HOLE' ), der nominell untersuchen soll, wie Objektlayouts aufgefüllt werden, sich aber hervorragend für die Visualisierung von Objektgröße und -layout im Allgemeinen eignet.

0
Phil Miller