C++ from the ground up

Structs and classes

Gathering related values into your own type, giving that type behaviour, and the three ideas you need to read anyone else class: the constructor, const methods, and private data.

Part 9 of 12

Advanced18 min readBack to Courses

A struct is a named shape

A struct gathers related values under one name. struct Book { std::string title; int pages; bool read; }; defines a new type, and from then on Book sits beside int in your declarations. Each of the values inside is a field, and you reach one with a dot: book.pages.

Grouping beats three parallel vectors every time. When the record is one value, a list of records is one vector, a function takes one parameter, and adding a field later is a change in one place rather than in every loop that walked the three lists by index.

The constructor

A constructor is a function with the same name as the type and no return type. Book(std::string t, int p) runs when you write Book book("Dune", 412);, and its job is to leave the value usable. The part after the colon, : title(t), pages(p), read(false), is the member list, which sets the fields before the body runs.

That is where values that should start false or zero get set, so nothing is ever read uninitialised. A constructor that needs no arguments is written Book() and is what an empty declaration uses.

class, and private data

A class is a struct where the members are private by default. Fields under a private: label can only be touched by the type own functions, and the functions under public: are its interface. That is encapsulation: the outside world calls add and count, and the names of the fields inside are nobody else business.

A method that only reads carries a const after its parameter list: int count() const. That means "this function will not change the value", and it is required, not decorative — a const record cannot call a method without it.

The trailing underscore on name_ and count_ is a convention, not syntax: it marks a member so a parameter can be called name without shadowing it. The compiler allows a parameter and a member to share a name — this->name picks the member — but the underscore makes the distinction visible without the arrow.

A struct with behaviour, a class with private data

#include <iostream>
#include <string>

struct Book {
    std::string title;
    int pages;
    bool read;

    Book(std::string t, int p)
        : title(t), pages(p), read(false) {}

    void finish() { read = true; }

    std::string summary() const {
        std::string state = read ? "read" : "unread";
        return title + " (" + state + ")";
    }
};

int main() {
    Book book("Dune", 412);
    std::cout << book.summary() << '\n';
    book.finish();
    std::cout << book.summary() << '\n';
    return 0;
}

A book, and a shelf that counts

Run it, then add a pages-per-day method to Book that takes the days left as a parameter, and give Shelf a method that prints how many books are still unread.

Output
Click "Run" to execute your code

Declaring Book book; without a constructor that sets the fields leaves them holding whatever happened to be there. A constructor is how you make that state impossible.

Copies, and what const protects

A struct or class value is copied by default. Passing a Book to a function copies every field, and returning one copies it back; the function works on its own value and cannot touch the caller. That is safe and predictable, and for a record with several strings it is also work you did not ask for — which is why a read-only parameter is const Book&.

A const method is part of that promise. It says the object will not change, so the compiler allows it on a const object and refuses a non-const method there. Mark every method that only reads, including the getters, or a function taking const Shelf& will not be able to ask for its count.

Members are initialised in the order they are declared, not the order they appear after the colon in the constructor. If the list says pages(p), title(t) but the fields are declared title first, title is still initialised first. A compiler warning is often the only clue, and the order should match the declarations.

The destructor, and what a class cleans up

A class can also have a destructor: ~Shelf(), which runs when the value goes out of scope. For a class holding a std::string or a vector you write nothing, because those members destroy themselves — that is the rule of zero.

You write a destructor when the class owns something the language does not manage for you, such as a file handle or memory from new. In modern C++ that is rare, and a member that manages itself is almost always the better design: the destructor you do not write is the one that cannot leak.

Three class mistakes: a getter that returns a plain reference to a private field, which lets a caller change the data the class was protecting; a read-only method without the trailing const, which cannot be called on a const object; and a member list ordered differently from the declarations, which initialises in declaration order regardless of what the list looks like.

Try these next

  • Add a pagesPerDay(int days) const method to Book and call it on a const book.
  • Give Shelf a method that returns the title of the last book added.
  • Make a Book, copy it, change the copy, and print both summaries.

An account that refuses an overdraft

Run it and check each balance against the deposit and the withdrawals. Then add a `transfer` method that takes another account by reference and moves money only when the balance allows it.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What is a constructor?