В моем первом примере у меня есть два битовых поля, используя int64_t
, Когда я компилирую и получаю размер класса, я получаю 8.
class Test
{
int64_t first : 40;
int64_t second : 24;
};
int main()
{
std::cout << sizeof(Test); // 8
}
Но когда я изменяю второй битфайлд, чтобы быть int32_t
размер класса удваивается до 16:
class Test
{
int64_t first : 40;
int32_t second : 24;
};
int main()
{
std::cout << sizeof(Test); // 16
}
Это происходит как на GCC 5.3.0, так и на MSVC 2015. Но почему?
В вашем первом примере
int64_t first : 40;
int64_t second : 24;
И то и другое first
а также second
используйте 64 бита одного 64-битного целого числа. Это приводит к тому, что размер класса будет одним 64-битным целым числом. Во втором примере у вас есть
int64_t first : 40;
int32_t second : 24;
Это два отдельных битовых поля, которые хранятся в двух разных блоках памяти. Вы используете 40 битов 64-битного целого числа, а затем вы используете 24-битные еще 32-битного целого числа. Это означает, что вам нужно как минимум 12 байтов (в этом примере используются 8 битных байтов). Скорее всего, дополнительные 4 байта, которые вы видите, — это заполнение для выравнивания класса по 64-битным границам.
Как указывалось в других ответах и комментариях, это поведение, определяемое реализацией, и вы можете / будете видеть разные результаты в разных реализациях.
Правила Си Стандарта для битовых полей не достаточно точны, чтобы сообщить программистам что-нибудь полезное о макете, но, тем не менее, отрицают реализации, которые в противном случае могли бы быть полезными свободами.
В частности, битовые поля должны быть сохранены в объекты, которые имеют указанные типы, или их подписанный / неподписанный эквивалент. В вашем первом примере первое битовое поле должно храниться в объекте int64_t или uint64_t,
и второй тоже, но есть достаточно места для них, чтобы вписаться в
тот же объект. Во втором примере первое битовое поле должно быть сохранено в
int64_t или uint64_t, а второй — в int32_t или uint32_t. Uint64_t будет иметь 24 бита, которые будут «скручены», даже если дополнительные битовые поля будут добавлены в конец структуры; uint32_t имеет 8 битов, которые в настоящее время не используются, но будут доступны для использования другого битового поля int32_t или uint32_t, ширина которого была меньше 8, были добавлены к типу.
ИМХО, Стандарт устанавливает здесь как можно худший баланс между предоставлением компиляторам свободы и предоставления программистам полезной информации / контроля, но это именно то, что есть. Лично я думаю, что битовые поля были бы намного более полезными, если бы предпочтительный синтаксис позволял программистам точно определять их расположение в терминах обычных объектов (например, битовое поле «foo» должно храниться в 3 битах, начиная с бита 4 (значение 16) поля) foo_bar «) но я не знаю никаких планов по определению такой вещи в Стандарте.
Чтобы добавить к тому, что уже сказали другие:
Если вы хотите проверить это, вы можете использовать опцию компилятора или внешнюю программу для вывода структуры struct.
Рассмотрим этот файл:
// test.cpp
#include <cstdint>
class Test_1 {
int64_t first : 40;
int64_t second : 24;
};
class Test_2 {
int64_t first : 40;
int32_t second : 24;
};
// Dummy instances to force Clang to output layout.
Test_1 t1;
Test_2 t2;
Если мы используем флаг вывода макета, такой как Visual Studio /d1reportSingleClassLayoutX
(где X
все или часть имени класса или структуры) или Clang ++ -Xclang -fdump-record-layouts
(где -Xclang
говорит компилятору интерпретировать -fdump-record-layouts
как команда внешнего интерфейса Clang вместо команды внешнего интерфейса GCC), мы можем вывести Test_1
а также Test_2
на стандартный вывод. [К сожалению, я не уверен, как сделать это напрямую с GCC.]
Если мы сделаем это, компилятор выведет следующие макеты:
cl /c /d1reportSingleClassLayoutTest test.cpp
// Output:
tst.cpp
class Test_1 size(8):
+---
0. | first (bitstart=0,nbits=40)
0. | second (bitstart=40,nbits=24)
+---class Test_2 size(16):
+---
0. | first (bitstart=0,nbits=40)
8. | second (bitstart=0,nbits=24)
| <alignment member> (size=4)
+---
clang++ -c -std=c++11 -Xclang -fdump-record-layouts test.cpp
// Output:
*** Dumping AST Record Layout
0 | class Test_1
0 | int64_t first
5 | int64_t second
| [sizeof=8, dsize=8, align=8
| nvsize=8, nvalign=8]
*** Dumping IRgen Record Layout
Record: CXXRecordDecl 0x344dfa8 <source_file.cpp:3:1, line:6:1> line:3:7 referenced class Test_1 definition
|-CXXRecordDecl 0x344e0c0 <col:1, col:7> col:7 implicit class Test_1
|-FieldDecl 0x344e1a0 <line:4:2, col:19> col:10 first 'int64_t':'long'
| `-IntegerLiteral 0x344e170 <col:19> 'int' 40
|-FieldDecl 0x344e218 <line:5:2, col:19> col:10 second 'int64_t':'long'
| `-IntegerLiteral 0x344e1e8 <col:19> 'int' 24
|-CXXConstructorDecl 0x3490d88 <line:3:7> col:7 implicit used Test_1 'void (void) noexcept' inline
| `-CompoundStmt 0x34912b0 <col:7>
|-CXXConstructorDecl 0x3490ee8 <col:7> col:7 implicit constexpr Test_1 'void (const class Test_1 &)' inline noexcept-unevaluated 0x3490ee8
| `-ParmVarDecl 0x3491030 <col:7> col:7 'const class Test_1 &'
`-CXXConstructorDecl 0x34910c8 <col:7> col:7 implicit constexpr Test_1 'void (class Test_1 &&)' inline noexcept-unevaluated 0x34910c8
`-ParmVarDecl 0x3491210 <col:7> col:7 'class Test_1 &&'
Layout: <CGRecordLayout
LLVMType:%class.Test_1 = type { i64 }
NonVirtualBaseLLVMType:%class.Test_1 = type { i64 }
IsZeroInitializable:1
BitFields:[
<CGBitFieldInfo Offset:0 Size:40 IsSigned:1 StorageSize:64 StorageOffset:0>
<CGBitFieldInfo Offset:40 Size:24 IsSigned:1 StorageSize:64 StorageOffset:0>
]>
*** Dumping AST Record Layout
0 | class Test_2
0 | int64_t first
5 | int32_t second
| [sizeof=8, dsize=8, align=8
| nvsize=8, nvalign=8]
*** Dumping IRgen Record Layout
Record: CXXRecordDecl 0x344e260 <source_file.cpp:8:1, line:11:1> line:8:7 referenced class Test_2 definition
|-CXXRecordDecl 0x344e370 <col:1, col:7> col:7 implicit class Test_2
|-FieldDecl 0x3490bd0 <line:9:2, col:19> col:10 first 'int64_t':'long'
| `-IntegerLiteral 0x344e400 <col:19> 'int' 40
|-FieldDecl 0x3490c70 <line:10:2, col:19> col:10 second 'int32_t':'int'
| `-IntegerLiteral 0x3490c40 <col:19> 'int' 24
|-CXXConstructorDecl 0x3491438 <line:8:7> col:7 implicit used Test_2 'void (void) noexcept' inline
| `-CompoundStmt 0x34918f8 <col:7>
|-CXXConstructorDecl 0x3491568 <col:7> col:7 implicit constexpr Test_2 'void (const class Test_2 &)' inline noexcept-unevaluated 0x3491568
| `-ParmVarDecl 0x34916b0 <col:7> col:7 'const class Test_2 &'
`-CXXConstructorDecl 0x3491748 <col:7> col:7 implicit constexpr Test_2 'void (class Test_2 &&)' inline noexcept-unevaluated 0x3491748
`-ParmVarDecl 0x3491890 <col:7> col:7 'class Test_2 &&'
Layout: <CGRecordLayout
LLVMType:%class.Test_2 = type { i64 }
NonVirtualBaseLLVMType:%class.Test_2 = type { i64 }
IsZeroInitializable:1
BitFields:[
<CGBitFieldInfo Offset:0 Size:40 IsSigned:1 StorageSize:64 StorageOffset:0>
<CGBitFieldInfo Offset:40 Size:24 IsSigned:1 StorageSize:64 StorageOffset:0>
]>
Обратите внимание, что версия Clang, которую я использовал для генерации этого вывода (та, которая используется Rextester) по умолчанию оптимизирует оба битовых поля в одну переменную, и я не уверен, как отключить это поведение.
Стандарт говорит:
§ 9.6 битовые поля
Распределение битовых полей внутри класса
Объект определяется реализацией. Выравнивание битовых полей определяется реализацией. [Замечания: Битовые поля размещают блоки распределения на некоторых машинах, а не
на других. Битовые поля назначаются справа налево на некоторых машинах, слева направо на других. — конец примечания]
Таким образом, компоновка зависит от реализации компилятора, флагов компиляции, целевой арки и так далее. Только что проверил несколько компиляторов и вывод в основном 8 8
:
#include <stdint.h>
#include <iostream>
class Test32
{
int64_t first : 40;
int32_t second : 24;
};
class Test64
{
int64_t first : 40;
int64_t second : 24;
};
int main()
{
std::cout << sizeof(Test32) << " " << sizeof(Test64);
}