C++ / FUNDAMENTAL TYPES AND VARIABLES
sizeof and alignment of objects
Predict and verify the size, alignment and padding of any C++ type, and explain why a struct is usually larger than the sum of its members.
What you will learn
- Use sizeof and alignof to read a type's storage footprint and address requirement
- Compute a struct's layout by hand: member offsets, internal padding, tail padding
- Reorder members by decreasing alignment to shrink a struct with no code changes
- Know that sizeof never evaluates its operand and yields std::size_t, never int
Understanding sizeof and alignment of objects
sizeof(T) gives the number of bytes one object of type T occupies in storage, where a byte is by definition sizeof(char) == 1. The most useful mental model is that sizeof is a stride: in T a[10], element k sits exactly k * sizeof(T) bytes past the start, so sizeof has to cover every byte the object reserves, not just the bytes that carry information. The operand is never evaluated, so sizeof(bump()) only asks the compiler for the type of that call and the function is never invoked, and the answer is a compile-time constant of type std::size_t.
Alignment is the other half of the story. Every type has an alignment requirement, a power of two reported by alignof(T), and an object of that type may only live at an address that is a multiple of it, because that is what the machine's load and store instructions and the platform ABI assume. A class type's alignment is the strictest alignment among its members, and members are laid out in declaration order, each pushed forward to the next offset that is legal for its own type. The bytes skipped in that process are internal padding.
Tail padding falls straight out of the stride model: sizeof(T) must be a multiple of alignof(T) so that element 1 of an array begins on just as legal an address as element 0. That is why a struct holding char, double, int costs 24 bytes on x86-64 while its members total 13, and why moving the char to the end brings it down to 16. Almost nothing here is fixed by the language beyond sizeof(char) == 1, so when a layout genuinely matters, such as a file header or a wire format, pin it with static_assert rather than trusting a number you measured once.
<cstddef>
<iostream>
struct Packet {
char tag; // 1 byte, alignment 1
double value; // 8 bytes, alignment 8
int count; // 4 bytes, alignment 4
};
int main() {
std::cout << "sizeof(char) = " << sizeof(char) << '\n'
<< "sizeof(double) = " << sizeof(double) << '\n'
<< "sizeof(int) = " << sizeof(int) << '\n'
<< "sum of members = "
<< sizeof(char) + sizeof(double) + sizeof(int) << '\n'
<< "sizeof(Packet) = " << sizeof(Packet) << '\n'
<< "alignof(Packet) = " << alignof(Packet) << '\n'
<< "offset of tag = " << offsetof(Packet, tag) << '\n'
<< "offset of value = " << offsetof(Packet, value) << '\n'
<< "offset of count = " << offsetof(Packet, count) << '\n';
}
sizeof measures the entire storage footprint of a type, padding included, because alignment forces every member and every array element onto a legal address.
Worked examples
Member order changes the size
Two structs with identical members differ in size purely because of where padding has to go.
<iostream>
struct Bad { char a; int b; char c; };
struct Good { int b; char a; char c; };
int main() {
std::cout << "Bad: " << sizeof(Bad) << " bytes, align " << alignof(Bad) << '\n';
std::cout << "Good: " << sizeof(Good) << " bytes, align " << alignof(Good) << '\n';
Bad arr[3]{};
std::cout << "arr: " << sizeof(arr) << " bytes, "
<< sizeof(arr) / sizeof(arr[0]) << " elements\n";
}
Example explained
Line 1In Bad, int b cannot start at offset 1, so 3 padding bytes are inserted and b begins at offset 4.
Line 2char c then takes offset 8, and 3 tail bytes follow so sizeof(Bad) is a multiple of alignof(Bad) == 4.
Line 3Good declares the int first, so a and c occupy offsets 4 and 5 and only 2 tail bytes are needed.
Line 4sizeof(arr) / sizeof(arr[0]) yields 3 because sizeof(Bad) is exactly the array stride, padding included.
sizeof does not run its operand
The operand of sizeof is inspected for its type only, and a pointer's size says nothing about what it points at.
<iostream>
int calls = 0;
int bump() { ++calls; return 42; }
int main() {
std::cout << sizeof(bump()) << '\n';
std::cout << "calls = " << calls << '\n';
int values[10]{};
int* p = values;
std::cout << sizeof(values) << ' ' << sizeof(p) << ' ' << sizeof(*p) << '\n';
}
Example explained
Line 1sizeof(bump()) is 4, the size of the int the call would return; only the expression's type is used.
Line 2calls stays 0, which proves the call was never executed at run time.
Line 3sizeof(values) is 40 because values is a genuine array object of ten ints, not a pointer.
Line 4sizeof(p) is 8 (a pointer on this ABI) while sizeof(*p) is 4, and *p is not actually dereferenced.
alignas widens both alignment and size
Raising a type's alignment forces its size up too, because size must stay a multiple of alignment.
<cstdint>
<iostream>
struct Vec4 { float x, y, z, w; };
struct alignas(32) Aligned { float x, y, z, w; };
int main() {
std::cout << sizeof(Vec4) << ' ' << alignof(Vec4) << '\n';
std::cout << sizeof(Aligned) << ' ' << alignof(Aligned) << '\n';
Aligned a{};
std::uintptr_t addr = reinterpret_cast<std::uintptr_t>(&a);
std::cout << (addr % alignof(Aligned) == 0 ? "aligned" : "misaligned") << '\n';
}
Example explained
Line 1Vec4 inherits alignment 4 from float, and four 4-byte members pack with no padding at all: 16 bytes.
Line 2alignas(32) raises Aligned's alignment to 32, and since size must remain a multiple of alignment, sizeof grows to 32.
Line 3The modulo test passes because the compiler places even this local object on a 32-byte boundary to honour the request.
Important notes
Every number here except sizeof(char) == 1 belongs to one ABI (x86-64 Linux with 64-bit long); on other targets both the type sizes and the padding change, so static_assert is the only portable guarantee.
alignas can only strengthen alignment; asking for less than the type's natural alignment is ill-formed. Packing attributes do remove padding, but they create under-aligned members that can be slow or undefined to access.
Common mistakes
Adding the member sizes and treating that as the record length: writing 13 bytes of the 24-byte Packet to a file or socket truncates count and shifts every record that follows.
Using sizeof to recover a length from a pointer, including an array function parameter, which has decayed to a pointer: inside void f(int a[10]) the expression sizeof(a) / sizeof(a[0]) is 8 / 4 == 2, so loops stop after two elements.
Comparing structs with memcmp: padding bytes are not written by member-wise assignment, so two objects whose members are all equal can still compare as different.
Try it yourself
Change, predict, then run
Declare struct Log { char level; double timestamp; int id; short code; }; predict its sizeof and alignof, then print offsetof for each member to check yourself. Reorder the members to reach the smallest possible size and lock that size in with a static_assert.
Open the C++ workspaceCheck your understanding
For struct S { double d; char c; }; on a platform where sizeof(double) is 8 and alignof(double) is 8, why does sizeof(S) come out as 16 instead of 9?
- Because sizeof always rounds a struct's size up to the next power of two
- Because a char occupies 8 bytes once it becomes a member of a struct
- So that in an array of S the double in every element still starts on an 8-byte boundary
- Because sizeof always reports a multiple of the machine word size, which is 8 here
Show answer
S inherits alignment 8 from its double, and a type's size must be a multiple of its alignment so that consecutive array elements remain correctly aligned; hence 7 bytes of tail padding after c. The power-of-two answer is tempting but false: struct { char a, b, c; } has size 3, which is neither a power of two nor a multiple of the word size, so neither rounding rule exists.