Разница между классом и структурой в отношении заполнения и наследования
Все нижеприведенное будет сделано на GCC 9.1 с использованием Compiler Explorer, в x86-64 с использованием -O3
.
У меня есть этот код:
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
Он правильно возвращает 16
, как и следовало ожидать, 8 байтов для foo
, 4 байта для bar
и 4 байта для baz
. Это работает только потому, что Derived
наследуется от Base
и поэтому ему не нужно заполнять после bar
потому что Derived
является одним типом, содержащим элементы Base
и Derived
.
У меня есть два вопроса, как показано ниже:
Первый вопрос
Если я уберу явный конструктор Base() {}
, он начнет возвращать 24
вместо 16
. то есть он добавляет отступы после bar
и baz
.
https://godbolt.org/z/0gaN5h
Я не могу объяснить, почему наличие явного конструктора по умолчанию отличается от наличия неявного конструктора по умолчанию.
Второй вопрос
Если я затем изменю struct
на class
для Base
, он вернется к возвращению 16
. Я тоже не могу это объяснить. Почему модификаторы доступа могут изменить размер структуры?
https://godbolt.org/z/SCYKwL
Ответы
Ответ 1
Это все сводится к тому, является ли ваш тип совокупным или нет. С
struct Base {
Base() {}
double foo;
int bar;
};
struct Derived : public Base {
int baz;
};
Base
не является агрегатом из-за конструктора. Когда вы удаляете конструктор, вы делаете Base
агрегатом, который, согласно добавлению конструктора по умолчанию к базовому классу, изменяет sizeof() производного типа, что означает, что gcc не будет "оптимизировать" пространство, а производный объект не будет использовать base обивка хвоста.
Когда вы меняете код на
class Base {
double foo;
int bar;
};
struct Derived : public Base {
int baz;
};
foo
и bar
теперь являются частными (поскольку классы имеют закрытую доступность по умолчанию), что снова означает, что Base
больше не является агрегатом, поскольку агрегатам не разрешено иметь закрытых членов. Это означает, что мы вернулись к тому, как работает первый случай.
Ответ 2
С вашим базовым классом вы получите 4 байта дополнения хвостом, и то же самое с классом Derived, поэтому обычно он должен составлять всего 24 bytes
для размера Derived
.
Он становится 16 байтов, потому что ваш компилятор может повторно использовать заполнение хвостом.
Однако повторное использование хвостового отступа проблематично для типов POD
(все члены public, конструктор по умолчанию и т.д.), Потому что это нарушает общие предположения, которые может сделать программист. (Таким образом, в принципе, любой здравомыслящий компилятор не будет использовать повторное использование хвостового отступа для типов pod).
Пусть притворятся, что компиляторы будут использовать tail padding reuse
использование tail padding reuse
для типов 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!
}
При добавлении явного конструктора к базовому классу или при изменении ключевых слов struct
на class
Derived
больше не удовлетворяет определению POD и, следовательно, повторное использование хвостового отступа не происходит.
Ответ 3
Нет разницы между структурой и классом. Они оба могут иметь конструкторы, методы (даже виртуальные), публичные, частные и защищенные члены, использовать наследование и быть шаблонными.
Единственное отличие состоит в том, что если вы не укажете видимость (публичную, приватную или защищенную) членов, они будут публичными в структуре и приватными в классе.
Возможно, проблема зависит от характера ваших частных/публичных членов (распределение/конфликт)