Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Memory Layout and Member Values in Structures, Unions, and Nested Data Types

Hello! In the previous lesson, you traced character arrays by separating an object’s size from the string visible before its first '\0'. That distinction still matters when an array is a member of a larger object. Now the questions change slightly: Where does each member begin, how much space does the whole object occupy, and which member values remain valid after a write?

This lesson develops a method for answering those questions for structures, unions, and objects nested inside them. The aim is to make a GATE-style memory trace a calculation rather than a guess.


Structures: separate members, one object

A structure groups members into a single type. Its members have separate storage and are accessed through the structure object:

struct Point {
    int x;
    int y;
};

struct Point a = {3, 5};
struct Point *p = &a;

a.x = 8;
p->y = 12;          /* Equivalent to (*p).y = 12; */

After these statements, a.x is 8 and a.y is 12. The parentheses in (*p).y matter: without them, . would bind to p before * does. You can use . at each level of a nested object and -> when the expression on its left is a pointer to a structure or union.

Separate storage does not mean members are packed without gaps. Read cppreference’s explanation before calculating a layout.

Struct declaration - cppreference.com

Read cppreference’s explanation of structure layout to establish which parts are guaranteed by C and where an implementation may add space.

In the “Explanation” section, start with the paragraph beginning “Within a struct object”. Read the layout paragraph through its final sentence about total size. Focus on increasing member addresses, padding between members or after the last member, and the absence of padding before the first member.

A member’s offset is its byte distance from the start of the structure. Compilers may insert unnamed padding to meet alignment requirements: for instance, an int might need to start at an address divisible by four. Padding can also follow the final member so that every element of an array of these structures starts suitably aligned.

Consider the following layout under stated assumptions: char occupies one byte, int occupies four bytes and requires four-byte alignment, and the structure requires four-byte alignment.

struct Cell {
    char tag;
    int count;
    char end;
};
Part of struct Cell Byte offset(s) Reason
tag 0 First member starts at the structure’s address
Padding 1–3 Align count
count 4–7 Four-byte int
end 8 Next declared member
Tail padding 9–11 Make the next array element align correctly

Thus sizeof(struct Cell) is 12, not the six bytes obtained by adding member sizes. If struct Cell cells[2] begins at byte address 1000, cells[0].count begins at 1004, cells[0].end at 1008, and cells[1] at 1012. The array stride is sizeof(struct Cell), including tail padding.

Reordering can change that result:

struct CompactCell {
    int count;
    char tag;
    char end;
};

Under the same assumptions, the member offsets are 0, 4, and 5; there are two tail-padding bytes, so the size is 8. C preserves declaration order—it does not silently rearrange members to save space.

For a real implementation, <stddef.h> provides offsetof(struct Cell, count), and sizeof(struct Cell) reports its actual size. Do not infer exact offsets merely from the names of the types: type sizes and alignment requirements must be given by the question or measured on the implementation. In particular, “every type is aligned to its own size” is a useful assumption in some worked problems, not a universal C rule. Padding bytes are storage, not extra members whose values you should try to trace.


Unions: different members, shared storage

A union uses a different arrangement. Its members begin at the same address, so a union needs enough storage for its largest member, plus any space needed to satisfy its alignment and array-stride requirements.

Watch the comparison and size example in Neso Academy’s Introduction to Unions in C. The first clip contrasts member addresses; the later clip uses a particular machine’s type sizes, which should be treated as assumptions for that example.

Introduction to Unions in C

Watch Neso Academy’s Introduction to Unions in C to see the storage distinction between a structure and a union.

Watch shared addresses, including the diagram contrasting separate structure members with overlapping union members. Then skip to union size. The size calculation works for the types and alignment in that example; do not generalize it into a rule that a union can never have padding.

The following image depicts two views of one union object, assuming a four-byte int. The char member occupies one byte at the beginning of the same storage used by the int; the diagram does not tell you what character a stored integer will produce on every machine.

An `int x` and a `char y` in a union start at the same address. With a four-byte `int`, the union has room for `x` even when `y` uses only its first byte.

Here is a case where “size equals the largest member size” is insufficient. Assume again that int has size four and alignment four, while char has size and alignment one:

union Payload {
    int number;    /* size 4, alignment 4 */
    char text[6];  /* size 6, alignment 1 */
};

The largest member occupies six bytes, but an array of union Payload must place each element at a suitable address for number. Under these assumptions, the union has alignment four and sizeof(union Payload) is 8: six bytes of required member storage, rounded up for the array stride. All member addresses still equal the union’s starting address.

Shared storage also changes how you trace values:

union Payload u = {.text = "cat"};

u.text initially contains 'c', 'a', 't', '\0', and two more zero characters. Reading it as a string gives "cat". If you subsequently execute u.number = 42;, the write uses the same storage: you must no longer report u.text as "cat".

Be precise about the common trap here. Reading a different union member can inspect an object representation under C’s rules; it is not a portable way to recover the earlier member’s value, and some interpretations can be invalid for a type. In particular, the character observed by writing an int and then reading a char can depend on byte order and representation. For an output question, report a definite character only when the question supplies the necessary machine assumptions. Keep track of the most recently written member for ordinary value tracing.


Nesting: calculate each type from the inside out

A structure can contain another structure, a union, an array, or a pointer. First find the size and alignment of each contained type; then treat each as a member of its enclosing type. Continue using the four-byte int and alignment assumptions above:

struct Inner {
    char code;
    int value;
};

union Payload {
    int number;
    char text[6];
};

struct Record {
    char kind;
    union Payload data;
    struct Inner inner;
};

struct Inner has code at offset 0, three padding bytes, and value at offset 4; its size is 8. We have already found that union Payload has size 8. Therefore:

Part of struct Record Offset(s) within Record
kind 0
Padding 1–3
data 4–11
inner 12–19

sizeof(struct Record) is 20 under these assumptions. Within inner, value has offset 4, so inner.value has offset 16 from the start of Record. If a Record starts at address 2000, that member starts at 2016; the next element of a struct Record array starts at 2020.

Now combine layout with member values:

struct Record r = {
    .kind = 'T',
    .data = {.text = "cat"},
    .inner = {.code = 'X', .value = 7}
};

struct Record *pr = &r;
struct Record copy = r;

copy.data.text[0] = 'b';
r.data.number = 42;

Before the last two statements, r.data.text is "cat", and pr->inner.value is 7. Structure initialization of copy copies its member values; its contained character array is not an alias of r’s array. Changing copy.data.text[0] therefore makes copy.data.text "bat" while leaving r unchanged at that point.

The final assignment writes the union inside r. Now r.data.number is 42, while copy.data.text remains "bat" and both structures’ inner.value members remain 7. Do not continue to treat r.data.text as the old string: its storage has been reused. Accessing pr->inner.value is equivalent to (*pr).inner.value; the pointer did not create another Record.

There is one important limit to the copying rule. If a structure member is a pointer, copying the structure copies the pointer value, not a separate object at its destination. That distinction will matter when the next lesson examines allocation and invalid accesses.


Key takeaways

For a layout question, establish type sizes and alignments, place structure members in declaration order, include internal and tail padding, and use the resulting sizeof as an array’s element stride. Union members share a starting address; their object may be larger than its largest member to meet alignment. For a value trace, follow each write separately: structure members retain independent values, whereas writing a union member reuses the storage of its alternatives.

The next lesson applies these object and pointer rules to dynamically allocated memory, where you will also have to decide whether an address remains valid to access.

Can't find a good explanation? Sign up and we'll make it for you

Sign up