struct S { char a; // memory location #1 int b : 5; // memory location #2 int c : 11, // memory location #2 (continued) char : 0, int d : 8; // memory location #3 struct { int ee : 8; // memory location #4 } e;} obj; // The object 'obj' consists of 4 separate memory locations
A class is meant to store data that is somehow connected to each other.
For example, an array and the length of the array. These two should never go out of sync, that’s what the class is responsible for.
If all you want to do is store some variables, a struct is the better choice. A struct is technically almost identical to a class. Their differences are mostly conventional.
Since x and a, and a,b,c are unrelated, let’s use a struct.
struct A{ int x{}; char a{};};
Next, int and char could have any size from 1 to 64 bits. That makes it difficult to talk about these examples. C++ has fixed-width integer types. It’s up to implementations to support these types, so their use is generally discouraged. But because the size of types is important in this example, let’s use them.
Just looking at the members of these structs, you might expect sizeof(A1) == sizeof(A2) and sizeof(B1) == sizeof(B2).
That’s not the case though, so member variable size cannot be all that matters.
Introducing, alignment.
Every type wants to be placed at a multiple of some value.
A std::uint8_t wants to be placed at a multiple of 1 (ie. any address).
A std::uint16_t wants to be placed at a multiple of 2 (0, 2, 4, 6, etc.).
A std::uint32_t wants to be placed at a multiple of 4 (0, 4, 8, etc.).
所有数据的地址必须是这个数据类型所占大小的整数倍。
Alignment, as everything in my reply, is implementation-defined. You might see different values.
You can get the alignment of a type using alignof(T), just like you used sizeof(T).
I’m using fixed-width types, but the same goes for char, int, etc.
For example, an std::uint32_t cannot be at address 0xab421, because that’s not a multiple of 4. If 0xab420 is already occupied, the std::uint32_t will be placed at 0xab424, the next closest address with fitting alignment.
The compiler does to because the CPU can read aligned values faster. Unaligned reads would cause the CPU to read memory from before and after the variable and discard it later.
Consider this new struct
struct C{ std::uint8_t a{}; std::uint32_t b{};};
and suppose an instance of C is created at address 0. a is at placed address 0. That’s fine, std::uint8_t can be placed at any address. b has to be placed after a, that would be address 1. But that won’t work, because 1 is not a multiple of 4 (alignof(std::uint32_t)). To fix it, the compiler generated padding bytes between the a and b.
struct C_padded{ std::uint8_t a{}; std::uint8_t padding[3]; // This is an array. It means 3 variables of type std::uint8_t. std::uint32_t b{};};
Now if a is at address 0, padding occupies address 1, 2, and 3. That places b at address 4, which is fine.
This doesn’t need padding, does it? a can be placed at address 0 and b can be placed at address 2, so where does the 1 additional byte come from to cause sizeof(A2) to be 4?
Let’s think about what alignof(A2) must be. All member variables of A2 must be properly aligned. alignof(A2) cannot be 1, because that would place a at odd addresses. But 2 works. In fact, using the greatest alignment of all member variables as the alignment of the struct always works.
Cool, so how is this related to the 1 extra byte we observe in A2?
Say we have two successive instances of A2
Suppose sizeof(A2) is 3, ie. sizeof(a) + sizeof(b).
The compiler again has to consider the alignment of the member variables. cauliflower is fine at address 0, because everything is fine at address 0. A2 has to be aligned to 2, so it cannot be placed at address 3.
1 padding byte would have to be inserted between cauliflower and mayflower to move mayflower to address 4. Although that’s what would happen according to what I wrote so far, the compiler chooses a different approach for structs.
Unrelated to how A2 is used, the compiler inserts 1 padding byte at the end of A2
That’s where the 1 extra byte in A2 came from. It automatically aligns successive instances of A2 without having to insert padding. We don’t need padding between cauliflower and mayflower, because the padding is built-into A2.
The question now is, why does the compiler add the padding into the struct, rather than outside?
Why alignment?
When you CPU wants to read from memory, all it can do is read a word. A word means 2 bytes of memory.
Say you have a std::uint16_t with value 0x1234.
If the compiler placed the std::uint16_t at address 0x10, the memory might look like this
address value
...
000c 83 21 // some other variable
000e ab cd // some other variable
0010 12 34
0012 ef 01 // some other variable
...
When the CPU wants to read the std::uint16_t, it can do so with a single read operation on address 0x10.
If the std::uint16_t were misaligned, eg. placed at address 0x11
address value
...
000c 83 21 // some other variable
000e ab cd // some other variable
0010 44 12
0012 34 ef
...
the CPU would have to read a word from 0x10 and 0x12, then discard the 0x44 and 0xef it didn’t want, before it can give you your value.
cppmem-segment
The memory that a program uses is typically divided into a few different areas, called segments:
The code segment (also called a text segment), where the compiled program sits in memory. The code segment is typically read-only.
The bss segment (also called the uninitialized data segment), where zero-initialized global and static variables are stored.
The data segment (also called the initialized data segment), where initialized global and static variables are stored.
The heap, where dynamically allocated variables are allocated from.
The call stack, where function parameters, local variables, and other function-related information are stored.
The heap has advantages and disadvantages:
Allocating memory on the heap is comparatively slow.
Allocated memory stays allocated until it is specifically deallocated (beware memory leaks) or the application ends (at which point the OS should clean it up).
Dynamically allocated memory must be accessed through a pointer. Dereferencing a pointer is slower than accessing a variable directly.
Because the heap is a big pool of memory, large arrays, structures, or classes can be allocated here.
The stack has advantages and disadvantages:
Allocating memory on the stack is comparatively fast.
Memory allocated on the stack stays in scope as long as it is on the stack. It is destroyed when it is popped off the stack.
All memory allocated on the stack is known at compile time. Consequently, this memory can be accessed directly through a variable.
Because the stack is relatively small, it is generally not a good idea to do anything that eats up lots of stack space. This includes passing by value or creating local variables of large arrays or other memory-intensive structures.