web-dev-qa-db-fra.com

Comment déterminez-vous la taille d'un objet en C++?

Par exemple, disons que j'ai une classe Temp:

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

    private:
        int foobar;
};

Lorsque je crée un objet de classe Temp, comment puis-je calculer l'espace requis et comment est-il représenté en mémoire (par exemple | 4 octets pour foobar | 8 octets pour function1 | etc |

36
Joel

Pour une approximation de premier ordre, la taille d'un objet est la somme de la taille de ses données membres. Vous pouvez être sûr qu'il ne sera jamais plus petit que cela.

Plus précisément, le compilateur est autorisé à insérer un espace de remplissage entre les membres de données pour s’assurer que chaque membre de données répond aux exigences d’alignement de la plate-forme. Certaines plates-formes sont très strictes en matière d'alignement, tandis que d'autres (x86) sont plus tolérantes, mais fonctionneront nettement mieux avec un alignement correct. Ainsi, même le paramètre d'optimisation du compilateur peut affecter la taille de l'objet.

Les fonctions d'héritage et virtuelles ajoutent une complication supplémentaire. Comme d'autres l'ont déjà dit, les fonctions membres de votre classe ne prennent pas elles-mêmes l'espace "par objet", mais l'existence de fonctions virtuelles dans l'interface de cette classe implique généralement l'existence d'une table virtuelle, essentiellement une table de recherche de pointeurs de fonctions utilisés pour résoudre dynamiquement l'implémentation de la fonction appropriée à appeler au moment de l'exécution. La table virtuelle (vtbl) est généralement accessible via un pointeur stocké dans chaque objet.

Les objets de classe dérivés incluent également tous les membres de données de leurs classes de base.

Enfin, les spécificateurs d’accès (public, privé, protégé) accordent au compilateur une certaine marge de manœuvre avec l’emballage des membres de données.

La réponse courte est que sizeof (myObj) ou sizeof (MyClass) vous indiquera toujours la taille appropriée d'un objet, mais que son résultat n'est pas toujours facile à prédire.

60
Drew Hall
sizeof(Temp)

vous donnera la taille. Très probablement, il s'agit de 4 octets (étant donné beaucoup d'hypothèses) et ce n'est que pour l'int. Les fonctions ne prennent pas de place par objet, elles sont compilées une fois et liées par le compilateur à chaque utilisation.

Il est impossible de dire exactement quelle est la disposition des objets. Toutefois, la norme ne définit pas la représentation binaire des objets.

Il faut tenir compte de certaines choses avec les représentations binaires, comme si elles ne correspondaient pas nécessairement à la somme des octets des membres de données, en raison de choses comme structure padding

17
Todd Gardner

Si vous souhaitez des informations détaillées sur la façon dont les objets sont représentés en mémoire au moment de l'exécution, la spécification ABI ( Application Binary Interface ) est l'endroit à utiliser. Vous devrez chercher à déterminer quel ABI votre compilateur implémente; Par exemple, les versions 3.2 et supérieures de GCC implémentent l'ABI Itanium C++ .

8
Paul Morie

Je me suis toujours demandé ce genre de chose, alors j'ai décidé de fournir une réponse complète. Il s'agit de ce à quoi vous pouvez vous attendre, et c'est prévisible (oui)! Ainsi, avec les informations ci-dessous, vous devriez être capable de prédire la taille d'une classe.

À l'aide de Visual Studio Community 2017 (version 15.2), en mode de publication avec toutes les optimisations désactivées et RTTI ( Informations de type à l'exécution ) désactivées, j'ai déterminé les éléments suivants:


Réponse courte:

Tout d'abord:

  • En 32 (x86) bits, <size of pointer> == 4 octets
  • En 64 (x64) bits, <size of pointer> == 8 octets
  • Quand je dis "héritage de classe virtuel", je veux dire par exemple: class ChildClass: virtual public ParentClass

Maintenant, mes conclusions sont que:

  • les classes vides sont 1 octet
  • l'héritage d'une classe vide est toujours de 1 octet
  • les classes vides avec des fonctions sont toujours 1 octet (?! voir Note ci-dessous pour l'explication)
  • l'héritage d'une classe vide avec une fonction est toujours de 1 octet
  • l'ajout d'une variable à une classe vide correspond à <size of variable> octets
  • l'héritage d'une classe avec une variable et l'ajout d'une autre variable est <size of variables> octets
  • l'héritage d'une classe et le remplacement de sa fonction ajoute une vtable (explication supplémentaire fournie dans la section Conclusions) et correspond à <size of pointer> octets
  • déclarer simplement une fonction virtuelle ajoute également une vtable, ce qui la rend <size of pointer> octets
  • l'héritage de classe virtuelle d'une classe vide (avec ou sans fonction membre) ajoute également une vtable et rend la classe <size of pointer> octets
  • l'héritage de classe virtuelle d'une classe non vide ajoute également une vtable, mais cela devient un peu compliqué: il ajoute<size of pointer> octets au total, wrapping toutes les variables membres dans autant de <size of pointer> octets incrémente tel quel nécessaire pour couvrir <total size of member variables> - oui, vous avez bien lu ... (voyez ce que je pense de ce qui se passe dans Conclusions ...)

Note que j'ai même essayé de demander à la fonction () d'exploiter du texte, de créer une instance de la classe et d'appeler la fonction; cela ne change pas la taille de la classe de fonctions (ce n'est pas une optimisation)! J'étais un peu surpris, mais cela a du sens: les fonctions membres ne changent pas, elles peuvent donc être stockées à l'extérieur de la classe.

Conclusions:

  • Les classes vides ont 1 octet, car c'est le minimum requis pour qu'il soit présent en mémoire. Une fois les données ou les données vtable ajoutées, commencez à compter à 0 octet.
  • L'ajout d'une fonction membre (non virtuelle) ne fait rien pour la taille, car la fonction membre est stockée en externe.
  • Le fait de déclarer une fonction membre virtuelle (même si la classe n’est pas surchargée!) Ou de remplacer une fonction membre dans une classe enfant ajoute ce que l’on appelle une "vtable" ou une "table de fonctions virtuelles" , qui autorise Dynamic Dispatch (qui est vraiment génial à utiliser et que je recommande vivement de l’utiliser). Cette table virtuelle consomme <size of pointer> octets, en ajoutant <size of pointer> octets à ladite classe. Cette table virtuelle ne peut exister qu'une fois par classe (que ce soit ou non), bien sûr.
  • L'ajout d'une variable membre augmente la taille de la classe associée à cette variable, que cette variable appartienne à la classe parent ou à la classe enfant (la classe parent reste bien sûr à sa propre taille).
  • L'héritage de classes virtuelles est la seule partie qui se complique ... Alors ... Je pense que ce qui se passe après quelques expériences est la suivante: la taille de la classe augmente en fait de <size of pointer> octets à la fois, même si elle n'a pas besoin de consomme autant de mémoire, je suppose, car cela ajoute un "bloc d'assistance" vtable pour chaque <size of pointer> octets de mémoire ou quelque chose du genre ...

Longue réponse:

J'ai déterminé tout cela en utilisant ce code:

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

Sortie:

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 bits (x64):

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)

Si vous voulez des informations sur l'héritage multiple, allez le découvrir vous-même! -.-

7
Andrew

Les méthodes appartiennent à la classe et non à un objet instancié particulier.

Sauf s'il existe des méthodes virtuelles, la taille d'un objet correspond à la somme de la taille de ses membres non statiques, plus un remplissage optionnel entre les membres pour l'alignement. Les membres seront probablement disposés séquentiellement en mémoire, mais la spécification ne garantit pas un ordre entre sections avec des spécifications d'accès différentes, ni un ordre relatif à la disposition des superclasses.

Avec les méthodes virtuelles présentes, il peut y avoir un espace supplémentaire pris pour vtable et d’autres informations RTTI.

Sur la plupart des plates-formes, le code exécutable est inséré dans la section en lecture seule .text (ou portant le même nom) de l'exécutable ou de la bibliothèque et n'est jamais copié nulle part. Lorsque class Temp a une méthode public: int function1(int), les métadonnées Temp peuvent avoir un pointeur sur une fonction _ZN4Temp9function1Ei (le nom modifié peut être différent selon le compilateur) pour l'implémentation réelle, mais elle ne contiendra certainement jamais le code exécutable incorporé.

6
ephemient

Les fonctions membres ne tiennent pas compte de la taille des objets d'une classe particulière. La taille de l'objet dépend uniquement des variables membres. Dans le cas de classes contenant des fonctions virtuelles, le VPTR est ajouté à la présentation de l'objet. La taille des objets est donc essentiellement la taille des variables membres + la taille des VPTR. Parfois, cela peut ne pas être vrai car les compilateurs essaient de localiser les variables de membre à la limite DWORD.

4
Canopus

Si vous utilisez Microsoft Visual C++, une option du compilateur vous indique la taille réelle de votre objet:/d1reportSingleClassLayout

C'est non documenté sauf pour cette vidéo de Lavavej http://channel9.msdn.com/Shows/Going+Deep/C9-Lectures-Stephan-T-Lavavej-Advanced-STL-3-of-n

3
Johannes Gerer

Si vous souhaitez examiner la mise en page d'une structure particulière, la macro offsetof(s,member) peut également être utile. Il vous indique à quelle distance de l'adresse de base d'une structure un membre particulier réside:

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

Souhaitez-vous imprimer sur une machine 32 bits typique:

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

Considérant que sur une machine 64 bits typique, il serait imprimer

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

Ceci peut aider.

De plus, les fonctions de classe sont représentées comme toute autre fonction. La seule magie que C++ fait à la fonction est de modifier les noms de fonction pour identifier de manière unique une fonction spécifique avec un ensemble spécifique de paramètres dans une classe spécifique.

0
sybreon

Il existe un appel à l'utilitaire pahole (pour 'Poke-A-HOLE' ) qui est destiné à étudier le remplissage de la disposition des objets, mais il est très utile pour visualiser la taille et la disposition de l'objet en général.

0
Phil Miller