为什么 class 大小会在 int64_t 更改为 int32_t 时增加
Why class size increases when int64_t changes to int32_t
在我的第一个示例中,我有两个使用 int64_t
的位域。当我编译并获得 class 的大小时,我得到 8.
class Test
{
int64_t first : 40;
int64_t second : 24;
};
int main()
{
std::cout << sizeof(Test); // 8
}
但是当我将第二个 bitfeild 更改为 int32_t
时,class 的大小加倍为 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 位。这导致 class 的大小为单个 64 位整数。在第二个例子中你有
int64_t first : 40;
int32_t second : 24;
这是存储在两个不同内存块中的两个独立位域。您使用 64 位整数的 40 位,然后使用另一个 32 位整数的 24 位。这意味着您至少需要 12 个字节(此示例使用 8 位字节)。您看到的额外 4 个字节很可能是填充以在 64 位边界上对齐 class。
正如其他答案和评论所指出的那样,这是实现定义的行为,您 can/will 在不同的实现上看到不同的结果。
C 标准的位域规则不够精确,无法告诉程序员有关布局的任何有用信息,但仍然拒绝实现可能有用的自由。
特别是,位域需要存储在 指定类型的对象中,或者 signed/unsigned 等同的类型。在您的第一个示例中,第一个位域必须存储在 int64_t 或 uint64_t 对象中,
第二个也一样,但是有足够的空间让他们适应
同一个对象。在第二个示例中,第一个位域必须存储在
int64_t 或 uint64_t,以及 int32_t 或 uint32_t 中的第二个。 uint64_t 将有 24 位,即使在结构的末尾添加了额外的位字段,也将是 "stranded"; uint32_t 有 8 位当前未使用,但可用于另一个宽度小于 8 的 int32_t 或 uint32_t 位域被添加到类型中。
恕我直言,标准在给予编译器自由与给予程序员有用之间取得了最糟糕的平衡 information/control,但事实就是如此。我个人认为,如果首选语法让程序员根据普通对象精确指定其布局,位域会更有用(例如,位域 "foo" 应存储在 3 位中,从第 4 位(值为 16)开始,字段 "foo_bar") 但我知道没有计划在标准中定义这样的东西。
标准说:
§ 9.6 bit-fields
Allocation of bit-fields within a class
object is implementation-defined. Alignment of bit-fields is implementation-defined. [Note: Bit-fields straddle allocation units on some machines and not
on others. Bit-fields are assigned right-to-left on some machines, left-to-right on others. — end note]
因此布局取决于编译器实现、编译标志、目标架构等。刚刚检查了几个编译器,输出大部分是 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);
}
补充一下其他人已经说过的话:
如果要检查它,可以使用编译器选项或外部程序输出结构布局。
考虑这个文件:
// 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
是 class 或结构名称的全部或一部分)或Clang++ 的 -Xclang -fdump-record-layouts
(其中 -Xclang
告诉编译器将 -fdump-record-layouts
解释为 Clang 前端命令而不是 GCC 前端命令),我们可以转储 Test_1
和 Test_2
到标准输出。 [不幸的是,我不确定如何直接使用 GCC 执行此操作。]
如果我们这样做,编译器将输出以下布局:
- Visual Studio:
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 使用的版本)似乎默认将两个位域优化为单个变量,我不确定如何禁用此行为.
在我的第一个示例中,我有两个使用 int64_t
的位域。当我编译并获得 class 的大小时,我得到 8.
class Test
{
int64_t first : 40;
int64_t second : 24;
};
int main()
{
std::cout << sizeof(Test); // 8
}
但是当我将第二个 bitfeild 更改为 int32_t
时,class 的大小加倍为 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 位。这导致 class 的大小为单个 64 位整数。在第二个例子中你有
int64_t first : 40;
int32_t second : 24;
这是存储在两个不同内存块中的两个独立位域。您使用 64 位整数的 40 位,然后使用另一个 32 位整数的 24 位。这意味着您至少需要 12 个字节(此示例使用 8 位字节)。您看到的额外 4 个字节很可能是填充以在 64 位边界上对齐 class。
正如其他答案和评论所指出的那样,这是实现定义的行为,您 can/will 在不同的实现上看到不同的结果。
C 标准的位域规则不够精确,无法告诉程序员有关布局的任何有用信息,但仍然拒绝实现可能有用的自由。
特别是,位域需要存储在 指定类型的对象中,或者 signed/unsigned 等同的类型。在您的第一个示例中,第一个位域必须存储在 int64_t 或 uint64_t 对象中, 第二个也一样,但是有足够的空间让他们适应 同一个对象。在第二个示例中,第一个位域必须存储在 int64_t 或 uint64_t,以及 int32_t 或 uint32_t 中的第二个。 uint64_t 将有 24 位,即使在结构的末尾添加了额外的位字段,也将是 "stranded"; uint32_t 有 8 位当前未使用,但可用于另一个宽度小于 8 的 int32_t 或 uint32_t 位域被添加到类型中。
恕我直言,标准在给予编译器自由与给予程序员有用之间取得了最糟糕的平衡 information/control,但事实就是如此。我个人认为,如果首选语法让程序员根据普通对象精确指定其布局,位域会更有用(例如,位域 "foo" 应存储在 3 位中,从第 4 位(值为 16)开始,字段 "foo_bar") 但我知道没有计划在标准中定义这样的东西。
标准说:
§ 9.6 bit-fields
Allocation of bit-fields within a class object is implementation-defined. Alignment of bit-fields is implementation-defined. [Note: Bit-fields straddle allocation units on some machines and not on others. Bit-fields are assigned right-to-left on some machines, left-to-right on others. — end note]
因此布局取决于编译器实现、编译标志、目标架构等。刚刚检查了几个编译器,输出大部分是 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);
}
补充一下其他人已经说过的话:
如果要检查它,可以使用编译器选项或外部程序输出结构布局。
考虑这个文件:
// 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
是 class 或结构名称的全部或一部分)或Clang++ 的 -Xclang -fdump-record-layouts
(其中 -Xclang
告诉编译器将 -fdump-record-layouts
解释为 Clang 前端命令而不是 GCC 前端命令),我们可以转储 Test_1
和 Test_2
到标准输出。 [不幸的是,我不确定如何直接使用 GCC 执行此操作。]
如果我们这样做,编译器将输出以下布局:
- Visual Studio:
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 使用的版本)似乎默认将两个位域优化为单个变量,我不确定如何禁用此行为.