C++ Part I · Module 9 · Arrays, Strings, and Aggregate Data
struct: Grouping Data That Belongs Together
Every type in the six preceding chapters answered one question: how many of these do I have. An array of four ints, a string of fifty-six characters, a view onto thirty of them — the count varies and the element type does not. A great deal of real data is not shaped like that. A register in a device is an address, a width in bits, a field at some offset, and an access mode; four things of four different kinds, none of which means anything useful without the others. You can represent that with four separate variables, or with four parallel arrays if you have many registers, and both work in the sense that the program compiles. What neither does is say that the four belong together, which means nothing stops them drifting apart — a sort that reorders one array and not the others, a function that takes four parameters in an order only its body reveals. This chapter is the construct that makes the grouping into a type, so that the compiler keeps the pieces together and a reader sees one thing rather than four. It is also where the arrow operator Chapter 8.2 deferred finally has something to point at.
Foundation16 min readC++Part IFundamentalsStructs
Part I · Module 9 · Chapter 9.7 · struct: Grouping Data That Belongs Together
1. A Different Question
| Chapters | The question | The answer |
|---|---|---|
| 9.1–9.3 | how many ints? | an array |
| 9.4–9.6 | how many characters? | a char array, a std::string, a view |
| 9.7–9.11 | what are the parts of this thing? | a struct |
The first six chapters varied the count and kept the element type fixed. A struct does the opposite: it fixes the count and varies the type.
struct Field { int offset; int width; };A
structis a type made of named members of possibly different types, stored together as one object.
Two things in that definition do the work. Named — the parts have names rather than positions, so field.width cannot be confused with field.offset. As one object — it has a single address, is copied as a unit, and can be passed, returned and stored as one value.
2. The Debt This Chapter Pays
Chapter 6.2 §5 needed something expensive to copy and borrowed a struct, declaring it:
struct SampleBlock { int sample[4096]; }; // Chapter 9.7's subject, borrowedThat is now an ordinary declaration you can read: a type with one member, which is an array of 4096 ints. Chapter 6.2 measured sizeof(SampleBlock) as 16384 and used it to show a by-value parameter copying 16 KB. Nothing about it was special; it was a group of one.
Chapter 8.2 §7 also deferred something to here, and §7 pays it.
3. Describing the Problem Rather Than the Machine
The frozen purpose's phrasing is worth taking seriously, because it rules out an alternative that works.
Suppose you have a register: an address, a width, and a field at an offset. Without a struct:
int reg_address = 0x40;
int reg_width = 32;
int field_offset = 8;
int field_width = 4;Four variables, and the only thing connecting them is a naming convention. And with many registers:
int addresses[64];
int widths[64];
int field_offsets[64];
int field_widths[64];Four parallel arrays, where addresses[i] and widths[i] describe the same register because everyone agrees they do. That agreement is not in any type, and three things follow:
Nothing keeps them the same length. Chapter 9.1 §7 established that an array's length is in its type, so four arrays have four lengths that happen to match.
Nothing keeps them aligned. Sort one and the correspondence is gone, silently.
A function needs four parameters. And Chapter 6.9 §10's trap applies — configure(0x40, 32, 8, 4) has four ints whose order only the body reveals.
With a struct:
struct Field { int offset; int width; };
struct Register { int address; int width; Field field; };
Register regs[64];One array, one length, nothing to misalign, one parameter. And configure(reg) cannot have its arguments in the wrong order because there is one.
That is what "describes the problem rather than the machine" means. The machine's view is bytes and counts; the problem's view is that a register is a thing with parts. The struct is the second one written down.
One member type deliberately missing. A register's access mode — read-only, read-write, write-one-to-clear — is naturally a small set of named values, and representing it as an int throws that away. Chapter 9.9 is the type for it, and this chapter leaves the member out rather than modelling it badly.
4. Defining and Initializing
struct Field { int offset; int width; };
Field f1{8, 4}; // brace initialization, member by member
Field f2{}; // every member zeroed
Field f3; // members UNINITIALIZED -- Chapter 3.8Field f3; leaves the members indeterminate, exactly as Chapter 9.1 §2 warned for arrays and Chapter 3.8 established in general. The braces cost two characters.
Measured on struct P { int a; int b; };:
P p1{1,2} : a=1 b=2
P p2{} : a=0 b=0
P p3{5} : a=5 b=0 <- b was filled inSupplying fewer values than there are members is legal, and the rest are filled in rather than left indeterminate — p3.b is 0, not garbage. That is a real rule and Chapter 9.8 states it properly.
The members are initialized in declaration order, which is why Field f1{8, 4} gives offset = 8 and width = 4. That positional dependence is a real cost of the brace form and Chapter 9.8 is where it gets examined properly — along with the rules about what you may omit, and why sizeof a struct is sometimes larger than the sum of its members.
This chapter uses brace initialization and does not derive its rules. One habit to adopt now: declare the members in an order a reader would guess, because a brace initializer is read positionally.
5. Reaching the Members
The . operator, and there is nothing to it:
Field f{8, 4};
f.width = 5; // read or write
std::cout << f.offset;f.width is an object of type int. Everything from Modules 3 to 8 applies to it unchanged — it has an address, you can take a reference to it, it can be const.
6. At Any Depth
The frozen purpose says at any depth, and the mechanism is that a member can itself be a struct:
#include <iostream>
struct Field { int offset; int width; };
struct Reg { int address; Field field; }; // nested
int main()
{
Reg r{0x40, Field{8, 4}};
std::cout << "r.address = " << r.address << '\n';
std::cout << "r.field.width = " << r.field.width << " (reach members at any depth)\n";
Reg* p = &r;
std::cout << "(*p).address = " << (*p).address << '\n';
std::cout << "p->address = " << p->address << " (the same thing, readably)\n";
std::cout << "p->field.offset = " << p->field.offset << '\n';
std::cout << "sizeof(Field) = " << sizeof(Field) << " sizeof(Reg) = " << sizeof(Reg) << '\n';
return 0;
}Measured output, zero warnings:
r.address = 64
r.field.width = 4 (reach members at any depth)
(*p).address = 64
p->address = 64 (the same thing, readably)
p->field.offset = 8
sizeof(Field) = 8 sizeof(Reg) = 12r.field.width is two dots, and each one is the same operator applied once. r.field is a Field; .width on that is an int. There is no special rule for depth — the first . produced something that the second . could be applied to.
0x40 printed as 64, which is Chapter 3.3's hexadecimal literal, unchanged.
The sizeof line is reported and not explained. 4 + 8 = 12 here, which happens to be the sum of the members. Chapter 9.8 is where that stops being reliable, and §8 says why.
7. The Arrow Chapter 8.2 Deferred
Chapter 8.2 §7 said: "When a pointer points at something with members, (*p).member works and is unpleasant, so C++ provides p->member for it. There is nothing to point at with members until Chapter 9.7's structs."
Measured above: (*p).address and p->address both gave 64.
p->membermeans exactly(*p).member. It is shorter, and it is the form everybody writes.
Two reasons the shorthand exists rather than being a stylistic preference:
The parentheses are mandatory in the long form. *p.address does not mean what it looks like — . binds more tightly than *, so it reads as *(p.address), which tries to find a member of a pointer. Chapter 4.9's precedence table is the reason, and the compiler is unusually helpful about it:
error: member reference type 'R *' is a pointer; did you mean to use '->'?
error: indirection requires pointer operand ('int' invalid)The first line names the fix.
It chains. p->field.offset is one arrow and one dot, measured at 8 above. Written the long way it is (*p).field.offset, and a second level of pointer would need another pair of parentheses.
Nothing else is going on. -> is not a separate concept; it is Chapter 8.2's dereference and this chapter's ., written as two characters instead of four.
8. What sizeof Does Not Tell You Yet
§6 measured sizeof(Reg) as 12, with members of 4 and 8. The temptation is to conclude that a struct's size is the sum of its members' sizes.
It is not, in general. It happened to be here. Chapter 9.8 measures a case where four members totalling 10 bytes produce a 16-byte struct, and where reordering the same four members produces 12 — which also means two structs with identical members can have different sizes. A smaller preview, measured:
struct S { char a; int b; char c; };
members total 1+4+1 = 6 bytes
sizeof(S) = 12 alignof(S) = 4
offsets: a=0 b=4 c=8Six bytes of members, twelve bytes of struct.
So the honest position for this chapter: a struct groups its members and stores them in declaration order, and how many bytes that takes is Chapter 9.8's subject. Using sizeof is fine; predicting it from the members is not, yet.
9. What to Actually Do
Group members that have no meaning apart. An address and a width describing one register belong in one type; two unrelated counters do not.
Prefer one array of struct to several parallel arrays. One length, nothing to misalign, one parameter.
Always brace-initialize. Field f{}; zeroes; Field f; leaves the members indeterminate.
Declare members in the order a reader would guess, because brace initialization is positional.
Use p->member, not (*p).member.
Reach any depth with repeated . — there is no special rule.
Do not predict sizeof from the members — Chapter 9.8.
Use a struct while it is a group, and reach for a class when some combination of values must be impossible — Module 11.
10. Mistakes Worth Making Once
Leaving a struct uninitialized. Field f; has indeterminate members — Chapter 3.8.
Keeping parallel arrays. Nothing keeps them the same length or aligned.
Writing *p.member. . binds tighter than *; it reads as *(p.member).
Getting brace-initializer order wrong. It is positional, and two int members will accept either order silently.
Assuming sizeof(S) is the sum of its members. Measured true here and false in Chapter 9.8.
Reaching for a class because the data has several members. The number of members is not the test; whether a combination can be invalid is.
Modelling an access mode as an int. Chapter 9.9 is the type for a small set of named values.
11. Interview Relevance
"What is the difference between struct and class in C++?" One default: struct members are public, class members are private. Then the part the question is really after — by convention a struct groups data and a class maintains an invariant, and the test is whether any combination of member values would be invalid.
"What is wrong with parallel arrays?" Nothing keeps them the same length or correspondingly ordered, and the relationship between them exists only as a convention. One array of struct makes the relationship a type.
"What does -> do?" Exactly (*p).member. If you add that *p.member does not work because . binds tighter, you have explained why the operator exists rather than what it does.
12. What You Should Be Able to Say Now
- The first six chapters of this module answered how many; a
structanswers what are the parts. - A
structis named members of possibly different types, stored together as one object. - Named means positions cannot be confused; one object means it has one address and copies as a unit.
- Chapter 6.2 §5's borrowed
SampleBlockwas a group of one — andsizeof16384 was what made its by-value copy measurable. - Parallel arrays work and say nothing. Nothing keeps four arrays the same length or aligned, and a function then needs four parameters.
- One array of
structgives one length, nothing to misalign, one parameter. - Brace-initialize always —
Field f;leaves members indeterminate. - Brace initialization is positional, so declare members in a guessable order.
.reaches a member; repeated.reaches any depth — measured,r.field.widthis 4.p->memberis exactly(*p).member— measured, both gave 64.- The shorthand exists because
*p.memberdoes not work (.binds tighter) and because it chains. - Measured:
sizeof(Field)8 andsizeof(Reg)12 — reported, not explained. - Do not predict
sizeoffrom the members. Chapter 9.8 shows 10 bytes of members producing 16, and reordering giving 12. - A
structgroups; a class promises. The test is whether any combination of member values must be impossible — Chapter 11.1.
13. Exercises
Exercise 1 — which question? For each, say whether the natural type is a sequence (array/string/vector) or a struct: (a) sixteen delay measurements (b) a register's address, width and access mode (c) the characters of a filename (d) a transaction's command, address and data (e) eight transactions.
Exercise 2 — replace the parallel arrays. Rewrite int addr[32]; int width[32]; int offset[32]; as one declaration, and list the three problems your version removes.
Exercise 3 — predict the output. For struct P { int a; int b; }; give the values of a and b after: (a) P p{1, 2}; (b) P p{}; (c) P p{5}; (d) P p; as a local.
Exercise 4 — reach the member. For struct Inner { int v; }; struct Outer { Inner in; }; Outer o{}; Outer* p = &o; write four different expressions that reach v.
Exercise 5 — why the arrow. Explain what *p.address actually means, which chapter's rule decides it, and why -> exists.
Exercise 6 — do not predict it. struct S { char a; int b; char c; }; has members totalling 6 bytes. Say what you can and cannot conclude about sizeof(S), and name the chapter that settles it.
Exercise 7 — struct or class? For each, choose and justify in one clause: (a) a 2-D point (b) a buffer holding a pointer and a length that must agree (c) a register description with an address and a width (d) a temperature reading with a value and a unit (e) a bank balance that must never be negative.
14. Summary
- Six chapters asked how many; a
structasks what are the parts. - Named members of possibly different types, stored as one object.
- Parallel arrays work and say nothing — no shared length, no enforced alignment, four parameters.
- One array of
struct: one length, nothing to misalign, one parameter. - Always brace-initialize; initialization is positional, so order the members sensibly.
.reaches a member at any depth — measured,r.field.widthis 4.p->memberis exactly(*p).member— measured, both 64 — and exists because*p.memberdoes not work.- Measured:
sizeof(Field)8,sizeof(Reg)12 — and do not generalise from that. - A
structgroups; a class promises. The test is whether a combination of values must be impossible. - Chapter 6.2's borrowed
SampleBlockand Chapter 8.2's deferred->are both now paid.
Two things in this chapter were deliberately reported without explanation, and both are the next chapter. The brace initializer was used from §4 onward without its rules — what happens when you supply fewer values than there are members, whether you may skip one, what the order actually guarantees. And sizeof was measured twice and both times flagged as not generalisable. The next chapter settles both, and the reason they belong together is the same reason the second one is surprising: a struct's members are not simply laid end to end. The compiler inserts gaps, their placement follows from a rule about where each type is allowed to start, and the consequence is that the same four members in a different order produce a type a quarter smaller.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
