The mechanism behind the Probe message
Lesson 6-2's Probe struct printed a message when main ended without anything calling it. The mechanism was the destructor, which C++ runs automatically when an object's scope ends.
~Probe() is that destructor, and the language guarantees it runs when the object dies at the closing brace. This unit teaches the full lifecycle: constructors that set objects up, methods that operate on them, and destructors that clean up.
Bundling data with the code that guards it
Real programs are about domain data, whether that is a player, an order, or a 2D point. Classes exist to bundle that data with the operations that keep it valid, so the rest of the program cannot corrupt it.
Nearly every type you have used so far is a class somebody wrote exactly this way, including std::string, std::vector, and the stream behind std::cout. This unit teaches you to write your own.
Bundling data: struct
A struct groups related variables into one new type:
struct Point { double x; double y; }; Point p; // a Point of your own p.x = 3.0; // members accessed with a dot p.y = 4.0;
This is like Python's classes, but with every member's type declared. struct and class in C++ are nearly the same keyword, and the only built-in difference is default access. struct members are public, meaning accessible from anywhere, while class members are private, meaning accessible only from the type's own functions.
By convention, struct is for simple open data bundles and class is for protecting invariants, meaning conditions that must stay true for the object's entire lifetime. Examples are that a bank balance is never negative, or that a vector's size never exceeds its capacity. The next lesson shows how the protection works.
Constructors
A constructor runs when the object is created, which guarantees it starts out valid. It carries the class's name and has no return type:
struct Point { double x, y; Point(double px, double py) : x(px), y(py) {} }; Point p(3.0, 4.0); // constructor called here
The : x(px), y(py) part is the member initializer list, which is the idiomatic place to initialize members because it initializes them directly instead of assigning after the fact. Once you define any constructor, creating a Point without arguments stops compiling unless you also provide a no-argument constructor.
A struct with a constructor and a method
A Point that knows how to compute its own distance from the origin, using its members without being handed them.
#include <iostream> #include <cmath> struct Point { double x, y; Point(double px, double py) : x(px), y(py) {} double distance_from_origin() { return std::sqrt(x * x + y * y); } }; int main() { Point p(3.0, 4.0); std::cout << "x=" << p.x << " y=" << p.y << "\n"; std::cout << "distance: " << p.distance_from_origin() << "\n"; return 0; }
Output
x=3 y=4 distance: 5
Inside distance_from_origin, the names x and y refer to the members of the specific object the method was called on. Nothing is passed in, and there is no self parameter to declare, unlike Python. C++ supplies the object implicitly, and this names it when disambiguation is needed.
The values print as 3 and 4 rather than 3.0 and 4.0, which is the default double formatting from lesson 2-1 again. The distance comes out as exactly 5 because 3-4-5 is a Pythagorean triple, and std::sqrt needs <cmath>.
Marking that method const, as double distance_from_origin() const, would be better practice, and the next lesson explains why.
The one built-in difference
The only built-in difference between struct and class is default member access, which is public for struct and private for class.
Everything else is shared. Both can have methods, constructors, destructors, inheritance, and operators, so neither keyword is more capable than the other. Where an object lives depends on how it is created, not on which keyword declared its type, so a class can sit on the stack and a struct can be allocated with new.
struct A { int x; }; // x is public class B { int x; }; // x is private class C { public: int x; }; // identical to A in every way
Default inheritance access follows the same split, being public for struct and private for class, though that detail rarely comes up in practice.
The choice between them is therefore a signal to readers about intent rather than a technical decision. Seeing struct tells a reader to expect an open bag of data, and seeing class tells them to expect an interface guarding something.
A struct with two computed methods
A Rectangle holding width and height, with both derived quantities exposed as methods rather than stored as members.
#include <iostream> struct Rectangle { double w, h; Rectangle(double pw, double ph) : w(pw), h(ph) {} double area() { return w * h; } double perimeter() { return 2 * (w + h); } }; int main() { Rectangle r(3.0, 4.0); std::cout << "area: " << r.area() << "\n"; std::cout << "perimeter: " << r.perimeter() << "\n"; return 0; }
Output
area: 12 perimeter: 14
Computing the area on demand rather than storing it in a member is the right call, because a stored copy would go stale the moment w or h changed. Derived values belong in methods, and only independent state belongs in members.
The semicolon after the struct's closing brace is mandatory, and omitting it produces a confusing cascade of errors pointing at the lines below rather than at the missing character. That trailing semicolon is a leftover from C, where a struct definition could declare a variable in the same statement.
A constructor taking a string and an int
A Student bundling a name with a score, built through a member initializer list.
#include <iostream> #include <string> struct Student { std::string name; int score; Student(std::string n, int s) : name(n), score(s) {} }; int main() { Student a("Ada", 95); Student b("Linus", 88); std::cout << a.name << " " << a.score << "\n"; std::cout << b.name << " " << b.score << "\n"; return 0; }
Output
Ada 95 Linus 88
Two Student objects exist independently, each with its own name and score, so writing to a.score has no effect on b. That is the same one-box-per-variable model from unit 2, applied to a type with two fields instead of one.
The parameter std::string n is taken by value, which copies the string. A production version would take const std::string&, following lesson 4-2's rule for large read-only parameters, or would take it by value and use std::move, which is a topic for later.
Note the deliberate name difference between the parameter n and the member name. Writing Student(std::string name, int score) : name(name), score(score) {} is legal and does the right thing, since the initializer list resolves the left name as the member and the right one as the parameter, but the version here is easier to read.