TRT
Sign in

Foundations · Module 4 · Lesson 1 of 9

Structs: Grouping Related Data

Combine values of different types into one type with struct, see members laid out at fixed offsets in memory, and learn what copying and initializing structs do.

FreeAbout 8 minutes

By the end you can

  • Declare a struct, create objects of it and access their members
  • Explain where each member lives and why member access costs nothing extra
  • Predict the effect of copying a struct and then modifying the copy
  • Initialize structs correctly with aggregate, default member and designated initializers

Builds on Arrays: Contiguous Elements and Indexing, Functions: Parameters, Return Values and Scope.

01 · A question first

You need a name, an age and a balance for each contact. Three parallel arrays work until one of them is sorted and the others are not. How does a struct make one record out of several different types, where do its pieces live in memory, and what does copying one actually copy?

Make a guess, then step through the trace below to check it.

02 · Watch it run

A Point in memory, and a copy of itstep 1 of 5
struct Point {    int x;    int y;};Point p{3, 4};int a = p.y;Point q = p;q.x = 9;
  1. byte 0--
  2. byte 1--
  3. byte 2--
  4. byte 3--
  5. byte 4--
  6. byte 5--
  7. byte 6--
  8. byte 7--
  9. byte 8--
  10. byte 9--
  11. byte 10--
  12. byte 11--
  13. byte 12--
  14. byte 13--
  15. byte 14--
  16. byte 15--

A struct definition describes a layout but creates nothing: no bytes are reserved yet. It says that a Point is 8 bytes, an int x followed by an int y. The sixteen cells are bytes of memory that two Point objects will occupy later, p first and q second.

Machine work: compile time: record the layout, x at offset 0 and y at offset 4

03 · The idea

Start with the problem

Information about one thing usually comes in several pieces of different types. A contact has a name, which is text, an age, which is an integer, and a balance, which is a number with a fraction. An array holds only elements of one type. You could keep three arrays, names, ages and balances, and use the same index in all of them, but that is fragile. If one array is sorted or one element is removed and the others are not, the records silently fall out of step. A function that works on a contact would also need three separate parameters.

What you want is a single type that holds all the pieces. That is a struct.

How the idea works

Declaring. A struct defines a new type from members. Note the semicolon after the closing brace, which is easy to forget:

cpp
struct Contact {
    std::string name;
    int age;
    double balance;
};

Creating and using objects. After the definition, Contact is a type like int. You create objects of it, and reach the members with a dot:

cpp
Contact c;
c.name = "Ada";
c.age = 36;
std::cout << c.name << " is " << c.age << "\n";

Initializing. A struct with only public members and no special functions is an aggregate, and you can initialize it with braces, in the order in which the members are declared:

cpp
Contact a{"Ada", 36, 120.5};
Contact b{};                            // everything zero or empty
Contact c{.name = "Lin", .age = 28};    // C++20: members by name, balance is 0

Members can also carry a default value in the definition, so that every object starts in a valid state: write int age = 0; and double balance = 0.0; inside the struct. This matters because, as with standalone variables in lesson 3, the built-in members of a local object are otherwise left with leftover bytes.

Where the members live. The trace shows it. A struct definition only describes a layout. When an object is created, its members are stored in the order of declaration, one after the other, each at a fixed offset from the start of the object. For Point, x is at offset 0 and y at offset 4. Reaching p.y is the start of p plus 4, a single load (step 3). This is like array indexing, but with a constant offset instead of a computed one, and with members of different types.

Copying. A struct is a value. Point q = p; copies every member (step 4), and afterwards the two objects are independent (step 5). Assignment works the same way. Members that are themselves types with copy behavior of their own, such as std::string and std::vector, copy in their own way, which means copying their contents. A pointer member copies only the address, so two structs then share the pointee, which is the aliasing of lesson 6.

Nesting and collections. A struct can contain other structs and arrays, and you can keep many of them in a std::vector<Contact>, which stores the records contiguously. That replaces the parallel arrays: one index gives you the whole record.

Differences from a class. In C++, struct and class are almost the same. The one difference is the default access: members of a struct are public, and those of a class are private. The Core level covers classes.

Why it is correct

Fixing the layout at compile time is what makes structs free to use. The compiler knows the type of every member and its offset, so each member access becomes an address plus a constant. Grouping the pieces also keeps them consistent: one object is one thing, so its parts cannot get out of step as the parallel arrays could, and its type name documents what it means.

Initialization in declaration order, with default member values, means that every object can start in a defined state. And the value semantics of copying let you pass and return a whole record the way you would an int.

What it costs

Size. The size of a struct is the sum of its members plus any padding that the CPU's alignment rules add, which lesson 2 explains. For Contact, the string takes 32 bytes with libstdc++, the int takes 4, and the double takes 8 after 4 bytes of padding, so sizeof(Contact) is 48.

Access. Reading a member is a load at a constant offset, which costs the same as reading a separate variable.

Copying. Copying a struct costs the size of its members, which for a few numbers is a handful of instructions. If it holds strings or vectors, copying may allocate memory, as lesson 5 described. Pass large structs by const reference, as lesson 7 of module 2 recommended.

Cache. A vector of structs keeps each record together, which is ideal when you use most of the fields of a record at a time. If you scan one field across millions of records, keeping that field in its own array can be faster, a technique called structure of arrays that comes up in performance work.

Pitfalls and variations

The missing semicolon. Forgetting the semicolon after the closing brace produces confusing errors, often reported on the next line, as lesson 6 of module 1 explained.

Uninitialized members. A local Contact c; leaves age and balance undefined. Initialize with braces or with default member values.

Designated initializers out of order. In C++20 the names must appear in declaration order, unlike in C.

No automatic comparison. a == b does not compile for a plain struct, and neither does printing with <<. You write the comparison, or the output, member by member, and the Core level shows how to define those operators.

Shallow copies. A struct with a pointer member copies only the pointer. The classic remedy is ownership by a container or a smart pointer, and the Core level covers it.

Surprising sizes. sizeof is not the sum of the members, which the next lesson explains.

Variations. std::pair and std::tuple group values without naming the members, and structured bindings from C++17 unpack a struct: auto [x, y] = p;. A union lets members share the same memory. Struct definitions can live in headers, as lesson 10 of module 2 said, because identical definitions in several files are allowed.

Where it is used

Every program uses structs: records such as contacts and orders, points and vectors in graphics and games, configuration settings, and messages in network protocols. A function that needs to return several values often returns a struct. And a struct with functions and access control is the starting point of classes, which the Core level opens with.

The next lesson, "Struct Layout, Padding and Alignment", looks at the bytes between the members, and at why the size of a struct is often larger than the sum of its parts.

04 · Prove it

Question 1 of 4 · answer from memory

`struct Point { int x; int y; };` and `Point p{3, 4};` with `p` starting at address 1000. Assuming 4-byte ints, at what address is `p.y`?

05 · Practice

Something wrong with this lesson? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save