Lesson 2 — Moving Records Around

← Module home · ← Lesson 1 — The struct · Lesson 3 — Enums and design →

In this lesson you will learn


1. Passing structures — three doors

A struct is a first-class type: it crosses function boundaries by any of the routes you know. The mechanisms are pure Unit 13 — only the payload got bigger.

Door 1 — by value (a copy crosses)

void printStudent(Student s) {          // s is a FULL COPY of the argument
    std::cout << s.name << ": " << s.score << '\n';
    s.score = -1;                       // changes the copy only — harmless, pointless
}
// call:  printStudent(roster[0]);       — no & needed; the record walks in whole

The entire record is copied field by field — the free deep copy from Lesson 1 §4 at work. Small records: perfect. Large ones: wasteful, and the function cannot modify the caller’s record.

Door 2 — by pointer (the handle crosses)

void bumpScore(Student* s, int by) {    // s holds the CALLER's record's address
    if (s == nullptr) return;           // pointers guard — the normal case
    s->score += by;                     // arrow operator: (*s).score, written humanely
}
// call:  bumpScore(&roster[0], 5);

New syntax, and it earns its place: s->score is shorthand for (*s).score — “follow the pointer, then use the dot.” The parens-plus-star version is legal but noisy; the arrow operator is the humane spelling and the one real code uses.

When do structs travel as pointers? Exactly when pointers earn their keep: arrays of records (the handle + count convention from Unit 09 — Student* roster, int n), optional records (“might not exist”), and ownership-heavy designs. One honest note for later: you may also meet Student* used as an output route in older code — in this course, references (below) are the default for that job.

Door 3 — by reference (the alias crosses) — the course default

void readInto(Student& s) {             // s IS the caller's record — a soldered alias
    std::cout << "name: ";
    std::getline(std::cin, s.name);     // no arrow, no star, no ceremony
    std::cout << "score: ";
    std::cin >> s.score;
}
// call:  readInto(roster[1]);          — no & at the call site

The reference-parameter rule applied to records: when the callee must modify the caller’s record and it always exists, take a reference — the alias mechanics make it the same box with a new name.

And the star of the show: const& — read-only, no copy

void printBest(const Student& a, const Student& b) {   // no copy, no modification
    if (a.score >= b.score) std::cout << a.name << '\n';
    else                    std::cout << b.name << '\n';
}

const Student& is the default door for big read-only records: reference efficiency (no copy) with value safety (the compiler forbids modification). You have been living on this rung since Unit 09’s const std::string arr[] — same promise, record-shaped.

The decision table

Situation Door Why
Small record, read-only, want a snapshot value copy is cheap; callee can’t touch the caller’s
Callee must modify the caller’s record Student& alias — the same box
Read-only, potentially large record const Student& no copy + compiler-enforced safety — the default
Array of records / optional / ownership Student* handle + count; nullable; re-aimable
Anything using Student* guard nullptr the pointer contract from Unit 13

2. Returning structures

Functions can hand back whole records:

Student readStudent() {                 // build locally, return by value
    Student s;
    std::cout << "name: ";
    std::getline(std::cin, s.name);
    std::cout << "score: ";
    std::cin >> s.score;
    return s;
}
// call:  Student a = readStudent();    — the record comes back whole

return s; copies the record out — the same deep field-wise copy, in reverse. For course-sized records the copy is trivial, and the shape is beautiful: a function that manufactures a value.

Return-vs-outparameter — the course rule:

Need Choose
Produce a new value from inputs (compute, read-and-build) return the record — readStudent(), makeDefault(name)
Fill in a record the caller already has reference parameter — readInto(s)
Report “nothing found” return a pointer (nullptr) or a bool + out-param

Date — the record with rules

A record type whose validity rules live in one function:

struct Date {
    int year;
    int month;      // 1–12
    int day;        // 1–31
};

bool isValid(const Date& d) {
    if (d.year < 1900 || d.year > 2100) return false;
    if (d.month < 1 || d.month > 12)    return false;
    if (d.day < 1 || d.day > 31)        return false;
    return true;
}

And the factory function that guarantees validity, in the guard-chain style:

Date readDate() {
    Date d;
    do {
        std::cout << "yyyy mm dd: ";
        std::cin >> d.year >> d.month >> d.day;
        if (!isValid(d)) std::cout << "invalid - try again\n";
    } while (!isValid(d));
    return d;
}

This is the pattern classes will industrialize in Unit 15: construction goes through a checkpoint, so no invalid record ever exists. For now, the discipline is a habit — the type system isn’t enforcing it yet, you are.


3. Nested structures — records inside records

Real data nests. A book has an author; an author has a name and a birth year:

struct Author {
    std::string name;
    int    birthYear;
};

struct Book {
    std::string title;
    Author  writer;        // a record member — a record inside the record
    double  price;
};

Building and reading one — the dots descend:

Book b;
b.title = "The Little Prince";
b.writer.name      = "Antoine de Saint-Exupéry";   // two dots: b's writer, the writer's name
b.writer.birthYear = 1900;
b.price = 9.99;

std::cout << b.writer.name;    // "Antoine de Saint-Exupéry"

Brace initialization nests the same way:

Book c = {
    "The Little Prince",
    {"Antoine de Saint-Exupéry", 1900},   // the nested brace list — position-bound, like always
    9.99
};
   Book c
  ┌───────────────────────────────┐
  │ title: "The Little Prince"    │
  │ writer:                       │
  │   ┌────────────────────────┐  │
  │   │ name: "Antoine de..."  │  │
  │   │ birthYear: 1900        │  │
  │   └────────────────────────┘  │
  │ price: 9.99                   │
  └───────────────────────────────┘

And assignment stays deep all the way down:

Book d = c;    // title, the WHOLE writer record, and price — copied recursively

Nesting is how the course builds records honestly: a Date inside Patient, an Address inside Employee — Lab 5 and Lab 2 do exactly this. Design rule of thumb: a field that has fields of its own is a type waiting to be named.


4. A complete worked program

A report over an array of records — every door in one program:

// 04_roster_report.cpp — Unit 14 · Session 14.2
// Compile: g++ -std=c++17 -Wall -Wextra 04_roster_report.cpp -o roster_report
#include <iostream>
#include <string>
#include <iomanip>

struct Student {
    std::string name;
    int    score;
};

void readInto(Student& s) {                       // Door 3 — modify the caller's
    std::cout << "name: ";
    std::getline(std::cin, s.name);
    std::cout << "score: ";
    std::cin >> s.score;
    std::cin.ignore(1000, '\n');                  // the mixing trap, pre-empted
}

void printRow(int sr, const Student& s) {         // Door: const& — read-only, no copy
    std::cout << std::setw(3) << sr << " | "
              << std::setw(20) << std::left << s.name << " | "
              << std::setw(3) << std::right << s.score << '\n';
}

Student bestOf(const Student roster[], int n) {   // RETURNS a record — a value question
    int best = 0;
    for (int i = 1; i < n; i = i + 1)
        if (roster[i].score > roster[best].score) best = i;
    return roster[best];                          // deep copy out
}

int main() {
    Student roster[50];
    int count = 0;
    char more;
    do {
        readInto(roster[count]);
        count += 1;
        std::cout << "another? (y/n): ";
        std::cin >> more;
        std::cin.ignore(1000, '\n');
    } while (more == 'y' && count < 50);

    std::cout << "--- ROSTER ---\n";
    for (int i = 0; i < count; i = i + 1)
        printRow(i + 1, roster[i]);

    Student top = bestOf(roster, count);
    std::cout << "best: " << top.name << " (" << top.score << ")\n";
    return 0;
}

Trace the doors: readInto by reference (modifies), printRow by const& (read-only), bestOf returning a value (a new answer), roster itself an array (handle + count under the hood). One program, every lesson-2 idea pulling its weight.


Practice

Key takeaways