web-dev-qa-db-fra.com

Différence entre classe et structure en ce qui concerne le remplissage et l'héritage

Tout ce qui suit sera fait sur GCC 9.1 en utilisant Explorateur du compilateur , en x86-64, en utilisant -O3.

J'ai ce code:

struct Base {
    Base() {}
    double foo;
    int bar;
};

struct Derived : public Base {
    int baz;
};

int main(int argc, char** argv)
{
    return sizeof(Derived);
}

https://godbolt.org/z/OjSCZB

Il renvoie correctement 16, Comme je m'y attendais, 8 octets pour foo, et 4 octets pour bar et 4 octets pour baz. Cela ne fonctionne que parce que Derived hérite de Base et qu'il n'a donc pas besoin de remplir après bar car Derived étant un seul type contenant à la fois Base et Derived éléments.

J'ai deux questions, comme ci-dessous:

Première question

Si je supprime le constructeur explicite de Base() {}, il commence à renvoyer 24, Au lieu de 16. c'est-à-dire qu'il ajoute un remplissage après bar et baz.

https://godbolt.org/z/0gaN5h

Je ne peux pas expliquer pourquoi avoir un constructeur par défaut explicite est différent d'avoir un constructeur implicite par défaut.

Deuxième question

Si je change ensuite struct en class pour Base, cela revient à retourner 16. Je ne peux pas l'expliquer non plus. Pourquoi les modificateurs d'accès changeraient-ils la taille de la structure?

https://godbolt.org/z/SCYKwL

43
Salgar

Tout se résume à savoir si votre type est un agrégat ou non. Avec

struct Base {
    Base() {}
    double foo;
    int bar;
};

struct Derived : public Base {
    int baz;
};

Base n'est pas un agrégat à cause du constructeur. Lorsque vous supprimez le constructeur, vous créez Base un agrégat qui, par l'ajout d'un constructeur par défaut à une classe de base modifie sizeof () un type dérivé , signifie que gcc ne sera pas "optimisé" pour l'espace et l'objet dérivé n'utilisera pas le rembourrage arrière de la base.

Lorsque vous modifiez le code en

class Base {
    double foo;
    int bar;
};

struct Derived : public Base {
    int baz;
};

foo et bar sont désormais privés (parce que les classes ont une accessibilité privée par défaut), ce qui signifie que Base n'est plus un agrégat car les agrégats ne sont pas autorisés à avoir des membres privés. Cela signifie que nous revenons au fonctionnement du premier cas.

36
NathanOliver

Avec votre classe Base, vous obtiendrez 4 octets de remplissage de queue, et la même chose avec la classe Derived, c'est pourquoi il devrait normalement être 24 bytes total pour la taille de Derived.

Il devient 16 octets, car votre compilateur est capable de faire réutilisation du remplissage de la queue .

Cependant, la réutilisation du rembourrage de la queue est problématique avec POD types (tous les membres sont publics, constructeur par défaut, etc ...), car il casse les hypothèses courantes qu'un programmeur ferait. (Donc, fondamentalement, tout compilateur sensé ne fera pas de réutilisation du rembourrage de queue pour les types de pod)

Imaginons que les compilateurs utilisent le tail padding reuse pour les types de POD:

struct Base {
    double foo;
    int bar;
};

struct Derived : Base {
    int baz;
};

int main(int argc, char** argv)
{
    // if your compiler would reuse the tail padding then the sizes would be:
    // sizeof(Base) == 16
    // sizeof(Derived) == 16

    Derived d;
    d.baz = 12;
    // trying to zero *only* the members of the base class,
    // but this would zero also baz from derived, not very intuitive
    memset((Base*)&d, 0, sizeof(Base));

    printf("%d", d.baz); // d.baz would now be 0!
}

Lors de l'ajout d'un constructeur explicite à la classe Base ou en changeant les mots clés struct en class, la classe Derived ne satisfait plus la définition du POD et donc la réutilisation du remplissage de la queue ne se produit pas.

10
Turtlefight