Это вывод данной программы:
sizeof(Empty) 1
sizeof(Derived1) 1
sizeof(Derived2) 4
sizeof(Derived3) 1
sizeof(Derived4) 8
sizeof(Dummy) 1
Это программа:
#include <iostream>
using namespace std;
class Empty
{};
class Derived1 : public Empty
{};
class Derived2 : virtual public Empty
{};
class Derived3 : public Empty
{
char c;
};
class Derived4 : virtual public Empty
{
char c;
};
class Dummy
{
char c;
};
int main()
{
cout << "sizeof(Empty) " << sizeof(Empty) << endl;
cout << "sizeof(Derived1) " << sizeof(Derived1) << endl;
cout << "sizeof(Derived2) " << sizeof(Derived2) << endl;
cout << "sizeof(Derived3) " << sizeof(Derived3) << endl;
cout << "sizeof(Derived4) " << sizeof(Derived4) << endl;
cout << "sizeof(Dummy) " << sizeof(Dummy) << endl;
return 0;
}
Размер Derived3 составляет 1 байт. Тогда почему размер Derived 4 составляет 8 байт ?? Если выравнивание является ответом, то почему нет выравнивания в случае производного3?
Это зависит от выравнивания членов данных в классе. Кажется, что если класс имеет виртуальный базовый класс, то его реализация содержит ссылку на этот виртуальный базовый класс, который в вашем случае равен 4 байта. Когда вы добавляете элемент данных типа char, он дополняется тремя байтами, чтобы обеспечить выравнивание для ссылки на базовый виртуальный класс.
Для конкретный тип T
, sizeof
означает две вещи:
представление завершить объект a
типа T
занимает только sizeof(T)
байт в [(char*)&a
, (char*)&a + sizeof(T)
);
массив из T
хранит второй объект sizeof(T)
после первого.
Байты, занятые законченными объектами, не перекрываются: либо один является субъектом другого и содержится в нем, либо они не имеют общих байтов.
Вы можете перезаписать полный объект (с memset
) и затем используйте размещение new, чтобы восстановить его (или просто присваивание объектам без осмысленного построения), и все будет хорошо, если деструктор не был важен (не делайте этого, если деструктор отвечает за освобождение ресурса) , Вы не можете перезаписать только подобъект базового класса, так как он разрушит весь объект. sizeof
говорит вам, сколько байтов вы можете перезаписать, не разрушая другие объекты.
Элементы данных класса являются полными объектами, поэтому размер класса всегда равен сумме размеров его членов.
Некоторые типы являются «полными»: каждый бит в объекте имеет смысл; в частности, unsigned char
, Некоторые типы имеют неиспользуемые биты или байты. У многих классов есть такие «дыры» для заполнения. Пустой класс имеет нулевой значащий бит: ни один бит не является частью состояния, так как его нет. Пустой класс — это конкретный класс, но он создан; каждый экземпляр имеет идентичность, следовательно, отдельный адрес, поэтому его размер не может быть нулевым, даже если стандарт допускает нулевые значения sizeof
, Пустой класс — это чистый отступ.
Рассматривать:
struct intchar {
int i;
char c;
};
Выравнивание intchar
это выравнивание int
, На типичной системе, где sizeof(int)
равно 4 и выравнивание этих основных типов равно размеру,
так intchar
имеет выравнивание 4 и размер 8, потому что размер соответствует расстоянию между двумя элементами массива, поэтому 3 байта не используются для представления.
Дано intchar_char
struct intchar_char {
intchar ic;
char c;
};
размер должен быть больше размера intchar
даже с неиспользованными байтами существуют в ic
из-за выравнивания: член ic
является законченным объектом и занимает все его байты, и memset
разрешено в этом объекте.
sizeof
хорошо определен только для конкретных типов (которые могут быть созданы) и завершенных объектов. Так вам нужно sizeof
определить размер пустого класса, если вы хотите создать массивы таких; но для подобъекта базового класса, sizeof
не дает вам информацию, которую вы хотите.
В C ++ нет оператора для измерения количества байтов, используемых в представлении класса, но вы можете попробовать с производным классом:
template <class Base, int c=1>
struct add_chars : Base {
char dummy[c];
};
template <class T>
struct has_trailing_unused_space {
static const bool result = sizeof (add_chars<T>) == sizeof (T);
};
Обратите внимание, что add_chars<T>
не имеет члена типа T
так нет T
завершить объект и memset
не допускается на intchar
субобъект. dummy
является законченным объектом, который не может перекрываться с любым другим завершенным объектом, но может перекрываться с подобъектом базового класса.
Размер производного класса не всегда является, по крайней мере, суммой размеров его объектов.
Член dummy
занимает ровно один байт; если есть какой-либо завершающий байт в Base
большинство компиляторов будет выделять dummy
в неиспользованном пространстве; has_trailing_unused_space
проверяет это свойство.
int main() {
std::cout << "empty has trailing space: ";
std::cout << has_trailing_unused_space<empty>::result;
}
пустое место в конце: 1
При рассмотрении расположения классов, включающих виртуальные функции и классы виртуальных баз, вам необходимо учитывать скрытый vptr и внутренние указатели. Они будут иметь те же свойства (размер и выравнивание), что и void*
в типичных реализациях.
class Derived2 : virtual public Empty
{};
В отличие от обычного наследования и членства, виртуальное наследование не определяет строгое, прямое отношение владения, но общее косвенное владение, подобно вызову виртуальной функции, вызывает косвенное обращение. Виртуальное наследование создает два вида макетов классов: подобъекты базового класса и полные макеты объектов.
Когда создается экземпляр класса, компилятор будет использовать макет, определенный для завершенных объектов, что может быть с использованием vptr, как это делает GCC, а Titanium ABI предусматривает:
struct Derived2 {
void *__vptr;
};
Vptr указывает на полную vtable со всей информацией о времени выполнения, но язык C ++ не считает такой класс полиморфным классом, поэтому dynamic_cast
/typeid
не может быть использован для определения динамического типа.
AFAIK, Visual C ++ использует не vptr, а указатель на подобъект:
struct Derived2 {
Empty *__ptr;
};
И другие компиляторы могут использовать относительное смещение:
struct Derived2 {
offset_t __off;
};
Derived2
очень простой класс; макет подобъекта Derived2
такой же, как и полная компоновка объекта.
Не рассматривайте немного более сложный случай:
struct Base {
int i;
};
struct DerV : virtual Base {
int j;
};
Здесь полный макет DerV
может быть (в стиле Titanium ABI):
struct complete__DerV {
void *__vptr;
int j;
Base __base;
};
Макет подобъекта
struct DerV {
void *__vptr;
int j;
};
Все полные или неполные объекты типа DerV
есть этот макет.
Vtable содержит относительные смещения виртуальной базы: offsetof(complete__DerV,__base)
в случае объекта динамического типа DerV
,
Вызов виртуальной функции может быть выполнен путем поиска переопределителя во время выполнения или знания динамического типа по правилам языка.
Upcast (преобразование указателя в виртуальный базовый класс), что часто происходит неявно, когда функция-член вызывается для базового класса:
struct Base {
void f();
};
struct DerV : virtual Base {
};
DerV d;
d.f(); // involves a derived to base conversion
либо использует известное смещение, когда известен динамический тип, как здесь, либо использует информацию времени выполнения для определения смещения:
void foo (DerV &d) {
d.f(); // involves a derived to base conversion
}
можно перевести на (Titanium ABI-style)
void foo (DerV &d) {
(Base*)((char*)&d + d.__vptr.off__Base)->f();
}
или Visual C ++ — стиль:
void foo (DerV &d) {
d.__ptr->f();
}
или даже
void foo (DerV &d) {
(Base*)((char*)&d + d.__off)->f();
}
Издержки зависят от реализации, но они присутствуют всякий раз, когда динамический тип неизвестен.