Embercode
✕
Pointers and Memory AddressesLesson 12 of 16

40 min read

Struct padding and object sizes

Why this matters

A struct groups several fields into one object. A program that stores a million records, one struct for each, uses a million times the size of one struct in memory. If the struct is 12 bytes instead of 8, the records take half as much room again. Copying a struct, passing it to a function and reading it from memory all cost more for a bigger struct.

Beginners often guess the size of a struct by adding up its fields. The guess is wrong for most structs. A struct with one char and one int is 8 bytes on the machine used for this lesson, and a struct with a char, an int and a char is 12 bytes, even though the three fields add up to 6. This lesson explains where the extra bytes come from, how to measure them, and how to reduce them by changing the order of the fields.

The previous lesson in this module covered how an array is laid out in memory. This lesson covers how one object is laid out. The cache locality lesson later in this module explains why a smaller object can also run faster.

What sizeof measures

sizeof is an operator. It asks the compiler for the size, in bytes, of a type or a variable. It does not run the program. The answer is fixed when the program is compiled.

The first program below prints the sizes of the basic types, and the sum of the fields of a small struct. Those sums are the numbers a beginner expects.

#include <cstddef>
#include <iostream>
 
struct Bad {
    char tag;   // 1 byte
    int count;  // 4 bytes
    char flag;  // 1 byte
};
 
struct Good {
    int count;  // 4 bytes
    char tag;   // 1 byte
    char flag;  // 1 byte
};
 
int main() {
    std::cout << "sizeof(char) = " << sizeof(char) << '\n';
    std::cout << "sizeof(int) = " << sizeof(int) << '\n';
    std::cout << "sum of field sizes = " << sizeof(char) + sizeof(int) + sizeof(char) << '\n';
    std::cout << '\n';
 
    std::cout << "Bad: sizeof = " << sizeof(Bad) << ", alignof = " << alignof(Bad) << '\n';
    std::cout << "  tag at offset " << offsetof(Bad, tag) << '\n';
    std::cout << "  count at offset " << offsetof(Bad, count) << '\n';
    std::cout << "  flag at offset " << offsetof(Bad, flag) << '\n';
    std::cout << '\n';
 
    std::cout << "Good: sizeof = " << sizeof(Good) << ", alignof = " << alignof(Good) << '\n';
    std::cout << "  count at offset " << offsetof(Good, count) << '\n';
    std::cout << "  tag at offset " << offsetof(Good, tag) << '\n';
    std::cout << "  flag at offset " << offsetof(Good, flag) << '\n';
    std::cout << '\n';
 
    std::cout << "array of 1000 Bad: " << sizeof(Bad[1000]) << " bytes\n";
    std::cout << "array of 1000 Good: " << sizeof(Good[1000]) << " bytes\n";
    return 0;
}

Two more names appear in this program. alignof gives the alignment of a type, which is explained in the next section. offsetof gives the byte position of a named field inside a struct, counted from the start of the struct. offsetof is a macro from <cstddef>, and alignof is a keyword built into the language.

Compile it with g++ -std=c++20 -Wall padding1.cpp -o padding1 and run it. The output is:

sizeof(char) = 1
sizeof(int) = 4
sum of field sizes = 6
 
Bad: sizeof = 12, alignof = 4
  tag at offset 0
  count at offset 4
  flag at offset 8
 
Good: sizeof = 8, alignof = 4
  count at offset 0
  tag at offset 4
  flag at offset 5
 
array of 1000 Bad: 12000 bytes
array of 1000 Good: 8000 bytes

The sum of the fields in Bad is 6. The real size is 12. The next two sections explain the missing 6 bytes.

Why a struct can be bigger than its parts

The reason is alignment. Alignment is a rule about where a value may start in memory. A type with alignment 4 must start at an address that is a multiple of 4. A char has alignment 1, so it can start anywhere. An int has alignment 4, so it must start at an address such as 0, 4, 8 or 12. A double on this machine has alignment 8.

The processor reads a value in one step when the value is aligned to its size. When a value straddles two aligned blocks, the processor may need two reads, or on some processors the program fails. On x86 processors the unaligned read still works, but it can be slower. The compiler avoids the problem by leaving gaps. A gap added to make a field start at the right address is called padding.

Here is how the compiler lays out Bad, one field at a time:

BytesFieldNotes
0tag (char)1 byte, starts at 0
1 to 3paddingthe int must start at a multiple of 4
4 to 7count (int)starts at 4, which is a multiple of 4
8flag (char)1 byte, starts at 8
9 to 11paddingmakes the total a multiple of 4

The fields take 6 bytes. The padding takes the other 6 bytes, three before count and three at the end. The padding before count is the one that most people expect. The padding at the end is the less obvious one. It exists so that the struct itself follows the same alignment rule as its largest field.

The trailing padding is there for arrays

Why does the compiler pad the end of a struct when no field follows? The answer is arrays. Each element of an array starts right after the one before it, so the next element must also start at a correctly aligned address. If Bad were 9 bytes, the second element would start at byte 9, and its int would be at byte 13, which is not a multiple of 4. Padding the struct to 12 bytes keeps every element aligned.

The output shows the effect. An array of 1000 Bad values takes 12000 bytes, which is exactly 1000 times 12. The array of Good values takes 8000 bytes.

The struct takes the largest alignment of its fields

A struct has its own alignment, and that alignment is the largest alignment of any field inside it. Bad contains an int, so its alignment is 4. alignof(Bad) printed 4 in the output above. That is why the size of Bad is a multiple of 4. The rule is simple: the size of a struct is the offset of the last field plus its size, rounded up to a multiple of the struct's alignment.

Field order changes the size

The layout of Good is different from Bad, and it shows the main lesson of this module on padding. The fields are the same, but count comes first. Its offset is 0, which is already a multiple of 4, so no padding is needed before it. The two char fields follow it with no gaps. At the end there are 2 bytes, which give 8 in total. The Good struct is 8 bytes, and Bad is 12 bytes.

The rule of thumb is to list the largest fields first and the smallest fields last. That order lets each field start right after the previous one, without a gap. The rule is not a law, and the next example shows a case where order does not matter. Read the sizes, then decide.

Worked example 2: more field types

The second program uses double, which is 8 bytes with an alignment of 8. It also shows a struct with a pointer, and an empty struct.

#include <cstddef>
#include <iostream>
 
// Same three fields, two orders.
struct Mixed1 {
    char kind;
    double weight;
    char level;
};
 
struct Mixed2 {
    double weight;
    char kind;
    char level;
};
 
// A pointer, an int and a char. Both orders below come out the same size.
struct NodeA {
    char kind;
    int value;
    NodeA* next;
};
 
struct NodeB {
    NodeA* next;
    int value;
    char kind;
};
 
struct Empty {};
 
struct Pair {
    int a;
    char b;
};
 
int main() {
    std::cout << "Mixed1 size " << sizeof(Mixed1) << ", alignof " << alignof(Mixed1) << '\n';
    std::cout << "Mixed2 size " << sizeof(Mixed2) << ", alignof " << alignof(Mixed2) << '\n';
    std::cout << "NodeA size " << sizeof(NodeA) << '\n';
    std::cout << "NodeB size " << sizeof(NodeB) << '\n';
    std::cout << "Empty size " << sizeof(Empty) << '\n';
    std::cout << "Pair size " << sizeof(Pair) << ", offset of b " << offsetof(Pair, b) << '\n';
    std::cout << "sizeof(void*) = " << sizeof(void*) << '\n';
    return 0;
}

The output is:

Mixed1 size 24, alignof 8
Mixed2 size 16, alignof 8
NodeA size 16
NodeB size 16
Empty size 1
Pair size 8, offset of b 4
sizeof(void*) = 8

Mixed1 is 24 bytes and Mixed2 is 16 bytes. The fields are identical. In Mixed1 the char kind comes first, so the double must start at offset 8. That leaves 7 bytes of padding after kind. Then level sits at offset 16, and 7 more bytes of trailing padding make the total 24. In Mixed2 the double comes first at offset 0. The two char fields follow directly at offsets 8 and 9, and 6 bytes of trailing padding bring the total to 16.

The two Node structs are the same size, and this is the case that shows order is not always the whole story. In NodeA the char comes first, so the int needs 3 bytes of padding before it. In NodeB the pointer comes first, and the char ends the struct, so the 3 bytes of padding move to the end. Both layouts need the same total. The lesson is that you should check the number with sizeof rather than assume the order helped.

An empty struct has size 1, not 0. C++ does not allow two different objects to share an address, so an object with no data still takes one byte. The Pair struct shows the same padding rule as Bad, in its smallest form: an int followed by a char gives 8 bytes, with the char at offset 4 and 3 bytes of trailing padding.

Worked example 3: packing and extra alignment

Padding can be removed, and alignment can be increased. Both choices change how the struct is read, so they are useful only in specific cases.

#include <cstddef>
#include <iostream>
 
struct Normal {
    char tag;
    int count;
    char flag;
};
 
#pragma pack(push, 1)
struct Packed {
    char tag;
    int count;
    char flag;
};
#pragma pack(pop)
 
struct alignas(16) Aligned {
    int id;
    char kind;
};
 
int main() {
    std::cout << "Normal: size " << sizeof(Normal) << ", alignof " << alignof(Normal) << '\n';
    std::cout << "Packed: size " << sizeof(Packed) << ", alignof " << alignof(Packed) << '\n';
    std::cout << "Packed offset of count " << offsetof(Packed, count) << '\n';
    std::cout << "Aligned: size " << sizeof(Aligned) << ", alignof " << alignof(Aligned) << '\n';
    return 0;
}

The output is:

Normal: size 12, alignof 4
Packed: size 6, alignof 1
Packed offset of count 1
Aligned: size 16, alignof 16

#pragma pack(push, 1) tells the compiler to stop adding padding inside the block. Packed is 6 bytes, and count now starts at offset 1, which is not a multiple of 4. The struct is smaller, but reading count may take extra work for the processor. The #pragma pack line is a compiler extension. It is not part of the C++ standard, though GCC and Clang accept it. Use it when a file format or a hardware register needs a fixed layout, and do not use it to save a few bytes in ordinary code.

alignas(16) does the opposite. It asks for an alignment of 16, so the struct starts at a multiple of 16. The size becomes 16, which is the smallest multiple of 16 that holds the 5 bytes of fields. Some code needs a larger alignment, for example code that uses vector instructions on the processor.

Measuring a real design

Here is a case where the order of fields matters. A record for one employee has a grade, an identifier, a department code and a salary. The first version lists the fields in the order they might appear on a form. The second lists them from the largest to the smallest.

#include <cstddef>
#include <iostream>
 
struct Employee {
    char grade;
    int id;
    char dept;
    double salary;
};
 
struct EmployeeSorted {
    double salary;
    int id;
    char grade;
    char dept;
};
 
int main() {
    std::cout << "Employee size " << sizeof(Employee) << '\n';
    std::cout << "EmployeeSorted size " << sizeof(EmployeeSorted) << '\n';
    std::cout << "Employee salary offset " << offsetof(Employee, salary) << '\n';
    return 0;
}

The output is:

Employee size 24
EmployeeSorted size 16
Employee salary offset 16

The first version takes 24 bytes. The double salary has to start at offset 16, because the fields before it leave the address at 9 bytes, and the next multiple of 8 is 16. The second version fits the four fields into 14 bytes, and the compiler rounds the total up to 16. Over a million records, the difference is 8 million bytes.

Reordering is not free. A field order that matches a file format or a network message must stay as it is, because the bytes are read back in that order. Reordering a struct that is only used inside one program is safe, as long as the program uses names and not raw byte offsets.

Common mistakes

These messages come from real compiler runs with g++ 15.2.0.

Assuming sizeof equals the sum of the fields

A static_assert is a check that the compiler runs while it compiles. If its condition is false, compilation stops. A common mistake is to write one that checks the sum of the field sizes. The compiler stops with a message that shows the real size:

padding_mistake.cpp:7:27: error: static assertion failed: expected 6 bytes
    7 | static_assert(sizeof(Bad) == 6, "expected 6 bytes");
      |               ~~~~~~~~~~~~^~~~
padding_mistake.cpp:7:27: note: the comparison reduces to '(12 == 6)'

The note shows the two values that were compared, 12 and 6. The fix is to write the expected size as the real size, or to reorder the fields and then check the new size. Writing static_assert(sizeof(Good) == 8) is a useful way to lock in a layout you chose on purpose.

Writing a struct's raw bytes to a file

Writing a struct with fwrite or std::ofstream::write writes its padding bytes too. The padding bytes do not hold a value that the program is allowed to rely on. A file written this way can differ between two compilers, two machines, or two versions of the same struct. The safe way is to write each field by name, in a fixed order and a fixed size, for example as 4 bytes for an int.

Calling offsetof on a class with virtual functions

offsetof works reliably on a standard-layout type, which is a type with a simple, predictable layout. A class with a virtual function (a function that a subclass can replace) is not standard-layout. The compiler still accepts the call, but it warns that the result is not guaranteed:

offsetof_mistake.cpp:10:27: warning: 'offsetof' within non-standard-layout type 'Shape' is conditionally-supported [-Winvalid-offsetof]
   10 |     std::cout << offsetof(Shape, id) << '\n';
      |                           ^

The warning appears without any flags, because it is on by default. Read the warning as a sign that the offset may not be what you expect. Keep offsetof for plain structs.

Assuming the same sizes on every machine

The sizes in this lesson come from an x86-64 Linux computer with g++ 15.2.0, where a pointer is 8 bytes. The C++ standard does not fix the size of int or the alignment of double. On another system a number may differ, for example a pointer on a 32-bit system is 4 bytes. Check the sizes with sizeof on the machine where the program runs, instead of copying the numbers from a book.

Packing everything by default

#pragma pack can save memory, but it makes some reads slower, and on some processors an unaligned read is an error. Packing a struct that is used in a hot loop can make the program slower, even though it is smaller. Use packing only when the layout must match something outside the program.

Practice

Exercise 1. What is sizeof(struct { char a; short b; char c; })? A short is 2 bytes and has alignment 2.

Show the answer

The layout is a at offset 0, one byte of padding, b at offset 2, c at offset 4, and one byte of trailing padding. The total is 6 bytes. A test program printed 6 for this struct.

Exercise 2. The struct { char a; double b; char c; } is 24 bytes. Write a reordered version with the same three fields that is smaller, and state its size.

Show the answer

Put double b first, then char a and char c. The double takes offsets 0 to 7, the two chars take offsets 8 and 9, and 6 bytes of trailing padding bring the total to 16. A test program printed 16 for the same three fields in the order double, char, char.

Exercise 3. For struct Pair { int a; char b; };, what is offsetof(Pair, b), and what is sizeof(Pair)?

Show the answer

The char b starts right after the 4-byte int, so its offset is 4. The struct has trailing padding to a multiple of 4, so its size is 8. The program printed Pair size 8, offset of b 4.

Exercise 4. What does sizeof(Bad[10]) return, if Bad is the 12-byte struct from the first program in this lesson?

Show the answer

An array of 10 structs is 10 times 12, which is 120 bytes. Each element keeps the trailing padding, so the array has no hidden gap between elements. A test program printed 10 Bad = 120 bytes.

Exercise 5. The struct Employee in the last worked example is 24 bytes. Explain in one or two sentences why the double field is at offset 16 and not offset 8.

Show the answer

The char grade is at offset 0, the int id at offset 4, and the char dept at offset 8, which leaves the address at 9. A double must start at a multiple of 8, and the next such address after 9 is 16. The program printed Employee salary offset 16.

Summary

  • A struct can be bigger than the sum of its fields, because the compiler adds padding so that each field starts at an address that matches its alignment.
  • The size of a struct is rounded up to a multiple of its largest alignment, so that arrays of the struct stay aligned.
  • Listing the largest fields first often removes padding, but the only sure way to know the size is to print it with sizeof.
  • Use alignof and offsetof to check the alignment and the position of each field.
  • Packing and extra alignment change the layout, and each one has a cost that should be weighed before it is used.
Check yourself: Layout and padding (8 questions) →Practice this topic: coding problems for Pointers and Memory Addresses →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Row-major layout