The memory layout of structures

Location
  1. Courses

    /

  2. Complete C++ Course

    /

  3. The basics

    /

  4. The memory layout of objects

The size of objects

The size of an object is not necessarily the summation of the size of each member composing the structure.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream> struct A { int a; double b; char c; }; int main() { A obj; std::cout << sizeof(obj) << "\n"; std::cout << sizeof(obj.a) << ' '; std::cout << sizeof(obj.b) << ' '; std::cout << sizeof(obj.c) << "\n"; return 0; }

In the example above, while the summation of the size of each member is 13, the size of the object is 24 bytes! Why? Because the compiler may decide to 'pad' some members of a structure.

Padding means adding bits at the end of a variable so it takes a specific amount of memory. The compiler may pad certain members to align the data inside the memory space of an object. The goal of data alignment is to divide the memory of the object into sections of the size of its biggest member and make sure that all the bytes of the same member is written into the same section. Since the size of the biggest member of the structure A is 8 bytes long, its memory space is divided into sections of 8 bytes.

Now, why would the compiler do that? Depending on the machine, accesses to the bytes of multi-byte members may be more efficient if they are stored that way.

Note that the way the members of a structure are stored in memory depends on the compiler used. It may even decide to not pad them at all.

Here is the example from above, but with the member of type double taken off:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream> struct A { int a; char b; }; int main() { A obj; std::cout << sizeof(obj) << "\n"; std::cout << sizeof(obj.a) << ' '; std::cout << sizeof(obj.b) << "\n"; return 0; }

In the example above, since the biggest member of the structure is made of 4 bytes, the compiler decided to pad the member of type char so it takes 4 bytes.

Note that the bits/bytes added to pad a variable do not become part of the variable. They are there only to align the data.

The order the members are stored in memory

The members of an object are stored, in memory, in the order they are defined in the structure. The first member is located at the address of the object, the second member is located after it and so on. Two members that are defined one after the other are, however, not guaranteed to be located directly one after the other, because they might be padded (There may be bytes, used for padding, between them, but there can not be variables between them).

Here is an example that illustrates the order the members are stored in memory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream> struct A { double a; double b; double c; }; int main() { A obj; double *ptr = (double*)&obj; // The member 'obj.a' is stored at the address of 'obj'. ptr[0] = 0; /* If the members of the structure A are not padded, the member 'obj.b' should be stored directly after the member 'obj.a'. */ ptr[1] = 111.111; /* If the members of the structure A are not padded, the member 'obj.c' should be stored directly after the member 'obj.b'. */ ptr[2] = 222.222; // Prints the value of the members of 'obj' to show they are stored in the order they are defined. std::cout << "obj.a == " << obj.a << "\n"; std::cout << "obj.b == " << obj.b << "\n"; std::cout << "obj.c == " << obj.c << "\n"; return 0; }

Note that accessing the members of an object that way is not a good practice. It may cause issues if the code is compiled with a compiler that aligns the members of structures, differently. The members should be accessed using their names.

Padding and storing order of members

Let us say we define a structure containing a member of type double, of a size of 8 bytes, and then 2 members of type char. Instead of using 8 bytes to store each member of type char, the compiler will likely store the 2 members of type char into the same 'group' of 8 bytes, because they are defined next to the other and they both fit in 8 bytes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream> struct A { double a; char b; char c; }; int main() { std::cout << sizeof(char) << std::endl; std::cout << sizeof(double) << std::endl; std::cout << sizeof(A) << std::endl; return 0; }

As you can see from the example above, the biggest member of the structure A is a which is of type double, which weights 8 bytes with the compiler used. The memory space of the objects of the structure type A is then divided into sections of 8 bytes. Since the size of objects of type A is only 16 bytes, that means that the members b and c, of type char, are stored into the same section.

Now, look at this example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream> struct A { char a; double b; char c; }; int main() { std::cout << sizeof(A) << std::endl; return 0; }

Even though the structure has the same members, its size is 24 bytes instead of 16! The member a is defined before the member b, so it must be stored before it. Therefore, the members a and c can not be stored in the same section of 8 bytes, because there is the member b between them. For that reason, both members a and c and padded with 7 bytes.

That shows that it is important to think about the order we define the members of a structure.