Free Handbook · Every example compiled & verified

Classes & Objects

Classes and structs, access control, constructors and destructors, const and static members, copying, the rule of three and five, move semantics and operator overloading.

0 / 145 lessons🔥 0 day streak
ShareXLinkedIn

Module 07 · what you'll be able to do

  • Write a class with private data, public member functions and constructors that use member initializer lists
  • Mark member functions const correctly and use static members for data shared by all objects
  • Explain when the copy constructor, copy assignment and destructor run, and apply the rule of three, five or zero
  • Use std::move to move instead of copy, and say what state a moved-from object is in
  • Overload <<, == and + for your own types, and know when a friend is justified
01

Classes, structs and access control

A class bundles data (member variables) with the functions that work on it (member functions). The point is not just grouping: a class can protect its data so that it is only changed through functions that keep it valid. An Account balance should only change through deposit and withdraw, which can refuse bad amounts. That guarantee is called an invariant, and protecting it is called encapsulation.

Access specifiers say who may use a member. public members are usable by anyone; private members only by the class's own member functions (and friends); protected matters for inheritance (Module 08). The only difference between struct and class in C++ is the default: a struct's members are public until you say otherwise, a class's are private. The convention: struct for plain data with no invariant (a Point, a config record), class when there is something to protect.

C++main.cpp
#include <iostream>
#include <string>

struct Point {             // plain data: members public by default
    int x = 0;
    int y = 0;
};

class Account {            // members private by default
public:
    void deposit(int amount) {
        if (amount > 0) balance_ += amount;      // the class enforces its rule
    }
    bool withdraw(int amount) {
        if (amount <= 0 || amount > balance_) return false;
        balance_ -= amount;
        return true;
    }
    int balance() const { return balance_; }     // read access only
private:
    int balance_ = 0;       // default member initializer
};

int main() {
    Point p{3, 4};
    p.x = 10;               // fine: public data
    std::cout << p.x << ',' << p.y << '\n';

    Account acc;
    acc.deposit(500);
    acc.deposit(-200);      // ignored
    std::cout << std::boolalpha << acc.withdraw(800) << ' ' << acc.withdraw(300) << '\n';
    std::cout << "balance " << acc.balance() << '\n';
}
Outputcompiled & run with real C++
10,4
false true
balance 200
Your turn

Add a transferTo(Account& other, int amount) member function that only moves money if the withdrawal succeeds. Note that it may read other.balance_ directly: private is per class, not per object.

Error you will hit

'balance_' is a private member of 'Account'

C++
#include <iostream>

class Account {
public:
    void deposit(int amount) { if (amount > 0) balance_ += amount; }
    int balance() const { return balance_; }
private:
    int balance_ = 0;
};

int main() {
    Account acc;
    acc.balance_ = 1'000'000;
    std::cout << acc.balance() << '\n';
}
main.cpp:13:9: error: 'balance_' is a private member of 'Account'
   13 |     acc.balance_ = 1'000'000;
      |         ^
main.cpp:8:9: note: declared private here
    8 |     int balance_ = 0;
      |         ^
Why the compiler said that

Code outside the class cannot touch private members. This is the compiler enforcing the class's promise that the balance only changes through deposit and withdraw. The trailing underscore in balance_ is just a common naming convention for data members.

The fix

Go through the public interface. If outside code genuinely needs to set a value, add a member function that validates it, rather than making the field public.

C++
acc.deposit(1'000'000);
02

Constructors and member initializer lists

A constructor is a member function with the class's name and no return type. It runs when an object is created and its job is to put the object into a valid state. A class can have several (they are overloads). A constructor that takes no arguments is the default constructor. If you write no constructors, the compiler generates a default one; as soon as you write any constructor, it does not.

Initialise members in the member initializer list, the part after the colon: Employee(std::string n, int s) : name_(std::move(n)), salary_(s) {}. Assigning inside the body instead first default-constructs each member and then overwrites it, and some members cannot be assigned at all (const members, references, types without a default constructor). A delegating constructor calls another constructor of the same class from its initializer list, so validation lives in one place. Mark single-argument constructors explicit to stop surprising implicit conversions.

C++main.cpp
#include <iostream>
#include <stdexcept>
#include <string>

class Employee {
public:
    // The main constructor: every member set in the initializer list
    Employee(std::string name, std::string dept, int salary)
        : name_(std::move(name)), dept_(std::move(dept)), salary_(salary) {
        if (salary_ < 0) throw std::invalid_argument("negative salary");
    }
    // Delegating constructor: reuses the one above
    explicit Employee(std::string name) : Employee(std::move(name), "Unassigned", 0) {}
    // Default constructor
    Employee() : Employee("Vacant") {}

    void print() const {
        std::cout << name_ << " | " << dept_ << " | " << salary_ << '\n';
    }
private:
    std::string name_;
    std::string dept_;
    int salary_;
};

int main() {
    Employee a("Asha", "Data", 90000);
    Employee b("Ravi");
    Employee c;
    a.print();
    b.print();
    c.print();
    try {
        Employee bad("Zed", "Ops", -5);
    } catch (const std::invalid_argument& e) {
        std::cout << "rejected: " << e.what() << '\n';
    }
}
Outputcompiled & run with real C++
Asha | Data | 90000
Ravi | Unassigned | 0
Vacant | Unassigned | 0
rejected: negative salary
Your turn

Remove explicit from the one-argument constructor and write Employee d = std::string("Meera");. It now compiles as an implicit conversion. Put explicit back and it is rejected, which is usually what you want.

Error you will hit

no matching constructor for initialization

C++
#include <string>

class Employee {
public:
    Employee(std::string name, int salary) : name_(std::move(name)), salary_(salary) {}
private:
    std::string name_;
    int salary_;
};

int main() {
    Employee e;
}
main.cpp:12:14: error: no matching constructor for initialization of 'Employee'
   12 |     Employee e;
      |              ^
main.cpp:3:7: note: candidate constructor (the implicit copy constructor) not viable: requires 1 argument, but 0 were provided
    3 | class Employee {
      |       ^~~~~~~~
main.cpp:3:7: note: candidate constructor (the implicit move constructor) not viable: requires 1 argument, but 0 were provided
    3 | class Employee {
      |       ^~~~~~~~
main.cpp:5:5: note: candidate constructor not viable: requires 2 arguments, but 0 were provided
    5 |     Employee(std::string name, int salary) : name_(std::move(name)), salary_(salary) {}
      |     ^        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Why the compiler said that

Because the class declares a constructor, the compiler no longer generates a default one. Employee e; asks for a constructor with no arguments and none exists. The notes list every candidate and why it does not fit.

The fix

Pass the arguments, or add a default constructor if an "empty" employee makes sense. Employee() = default; brings the compiler-generated one back (give the members default values so nothing is left uninitialised).

C++
class Employee {
public:
    Employee() = default;
    Employee(std::string name, int salary) : name_(std::move(name)), salary_(salary) {}
private:
    std::string name_;
    int salary_ = 0;
};
Error you will hit

field will be initialized after field (-Wreorder-ctor)

C++
#include <iostream>

class Range {
public:
    Range(int lo, int len) : end_(lo + len), start_(lo) {}
    int size() const { return end_ - start_; }
private:
    int start_;
    int end_;
};

int main() {
    std::cout << Range(5, 3).size() << '\n';
}
main.cpp:5:30: warning: field 'end_' will be initialized after field 'start_' [-Wreorder-ctor]
    5 |     Range(int lo, int len) : end_(lo + len), start_(lo) {}
      |                              ^~~~~~~~~~~~~~  ~~~~~~~~~~
      |                              start_(lo)      end_(lo + len)
Why the compiler said that

Members are always initialised in the order they are declared in the class, not the order you list them after the colon. Here that is harmless, but if end_ were computed from start_ (end_(start_ + len)) it would read start_ before it was set: an uninitialised read. The warning is enabled by -Wall.

The fix

Write the initializer list in declaration order, and never compute one member from another that is declared later.

C++
Range(int lo, int len) : start_(lo), end_(lo + len) {}
03

Destructors and the this pointer

A destructor is named ~ClassName(), takes no arguments, and runs automatically when an object's lifetime ends: at the end of its scope for a local, when delete or a smart pointer destroys a heap object, or when its container is destroyed. Its job is releasing whatever the object acquired, which is RAII from Module 06. Members are destroyed automatically after the destructor body runs, in reverse declaration order, so most classes need no destructor at all.

Inside a member function, this is a pointer to the object the function was called on. You rarely need it explicitly, because members are found automatically, but it is useful to tell a member apart from a parameter with the same name (this->name), to pass the current object to another function, and to return *this so calls can be chained.

C++main.cpp
#include <iostream>
#include <string>

class Query {
public:
    explicit Query(std::string table) : table_(std::move(table)) {
        std::cout << "[open query on " << table_ << "]\n";
    }
    ~Query() { std::cout << "[close query on " << table_ << "]\n"; }

    Query& where(const std::string& cond) {      // returns the object itself
        this->conditions_ += (conditions_.empty() ? " WHERE " : " AND ") + cond;
        return *this;
    }
    Query& limit(int n) {
        limit_ = n;
        return *this;
    }
    std::string sql() const {
        std::string s = "SELECT * FROM " + table_ + conditions_;
        if (limit_ > 0) s += " LIMIT " + std::to_string(limit_);
        return s;
    }
private:
    std::string table_;
    std::string conditions_;
    int limit_ = 0;
};

int main() {
    {
        Query q("orders");
        q.where("total > 100").where("status = 'paid'").limit(10);   // chained
        std::cout << q.sql() << '\n';
    }                                                // destructor runs here
    std::cout << "after the block\n";
}
Outputcompiled & run with real C++
[open query on orders]
SELECT * FROM orders WHERE total > 100 AND status = 'paid' LIMIT 10
[close query on orders]
after the block
04

const member functions and static members

Putting const after a member function's parameter list (int balance() const) promises that the function does not modify the object. Inside it, this is a pointer to const. This matters because you can only call const member functions on a const object, and objects passed as const& (the normal way to pass them, from Module 03) are const. Rule: every member function that only reads should be const.

A static data member belongs to the class, not to any one object: there is a single copy shared by all objects, like a counter of how many have been created. Declare it inline static to define it right in the class. A static member function has no this, can only touch static members, and is called as ClassName::function(). Static factory functions are a common use.

C++main.cpp
#include <iostream>
#include <string>

class Ticket {
public:
    static Ticket create(const std::string& title) {    // static factory
        return Ticket(nextId_++, title);
    }
    static int issued() { return nextId_ - 1; }         // no this: reads only statics

    int id() const { return id_; }                      // const: read-only
    const std::string& title() const { return title_; }
    void rename(const std::string& t) { title_ = t; }   // not const: modifies
private:
    Ticket(int id, std::string title) : id_(id), title_(std::move(title)) {}
    inline static int nextId_ = 1;                      // one copy for the whole class
    int id_;
    std::string title_;
};

void show(const Ticket& t) {                            // const&: only const functions allowed
    std::cout << '#' << t.id() << ' ' << t.title() << '\n';
}

int main() {
    Ticket a = Ticket::create("Login fails");
    Ticket b = Ticket::create("Slow report");
    b.rename("Slow monthly report");
    show(a);
    show(b);
    std::cout << "issued: " << Ticket::issued() << '\n';
}
Outputcompiled & run with real C++
#1 Login fails
#2 Slow monthly report
issued: 2
Your turn

The constructor is private, so Ticket t(5, "x"); does not compile: the only way to get a ticket is create, which guarantees unique ids. Try it and read the error.

Error you will hit

'this' argument has type 'const T', but function is not marked const

C++
#include <iostream>

class Temperature {
public:
    explicit Temperature(double c) : celsius_(c) {}
    double fahrenheit() { return celsius_ * 9 / 5 + 32; }
private:
    double celsius_;
};

void report(const Temperature& t) {
    std::cout << t.fahrenheit() << '\n';
}

int main() {
    report(Temperature(21.5));
}
main.cpp:12:18: error: 'this' argument to member function 'fahrenheit' has type 'const Temperature', but function is not marked const
   12 |     std::cout << t.fahrenheit() << '\n';
      |                  ^
main.cpp:6:12: note: 'fahrenheit' declared here
    6 |     double fahrenheit() { return celsius_ * 9 / 5 + 32; }
      |            ^
Why the compiler said that

t is a const&, so only functions that promise not to modify the object may be called on it. fahrenheit does not modify anything, but it did not say so, and the compiler checks the declaration, not the body.

The fix

Add const to every member function that only reads. Doing it from the start avoids this error spreading through a codebase later.

C++
double fahrenheit() const { return celsius_ * 9 / 5 + 32; }
05

Copying, and the rule of three and five

Copying an object uses one of two special member functions. The copy constructor T(const T& other) builds a new object as a copy (T b = a;, passing by value, returning by value). Copy assignment T& operator=(const T& other) overwrites an existing object (b = a;). If you do not write them, the compiler generates versions that copy each member in turn.

Member-by-member copying is exactly right when the members manage themselves (the rule of zero from Module 06). It is exactly wrong when a member is a raw pointer that the class owns: the copy gets the same pointer, and both destructors delete it.

Error you will hit

AddressSanitizer: attempting double-free (shallow copy)

C++
#include <cstring>
#include <iostream>

class Buffer {
public:
    explicit Buffer(const char* text) : size_(std::strlen(text)), data_(new char[size_ + 1]) {
        std::strcpy(data_, text);
    }
    ~Buffer() { delete[] data_; }
    const char* c_str() const { return data_; }
private:
    std::size_t size_;
    char* data_;
};

int main() {
    Buffer a("hello");
    Buffer b = a;              // compiler-generated copy: copies the pointer
    std::cout << b.c_str() << '\n';
}
$ c++ -std=c++20 -fsanitize=address -g main.cpp && ./a.out
==428==ERROR: AddressSanitizer: attempting double-free on 0x6020000000f0 in thread T0:
    #1 0x0001041ed02c in Buffer::~Buffer() main.cpp:9
    #3 0x0001041eca5c in main main.cpp:20

freed by thread T0 here:
    #1 0x0001041ed02c in Buffer::~Buffer() main.cpp:9
    #3 0x0001041eca40 in main main.cpp:20

previously allocated by thread T0 here:
    #1 0x0001041ecf34 in Buffer::Buffer(char const*) main.cpp:6
    #3 0x0001041ec9dc in main main.cpp:17

SUMMARY: AddressSanitizer: double-free main.cpp:9 in Buffer::~Buffer()
Why the compiler said that

No warning and no compile error: the copy compiles and b reads the text fine. Then at the closing brace (line 20) b's destructor frees the buffer, and a's destructor frees the same buffer again. Built with AddressSanitizer (-fsanitize=address -g), the report shows both frees coming from ~Buffer on line 9 during the same closing brace.

The fix

Either replace char* with std::string (rule of zero, the right answer here), or write the copy operations yourself so each object owns its own buffer, as the example below does.

The rule of three: if a class needs a hand-written destructor, copy constructor or copy assignment, it almost certainly needs all three, because all three exist to manage the same resource. C++11 added two move operations (next lesson), making it the rule of five. This example is a minimal owning string: study it once, then use std::string and std::vector, which already do this.

C++main.cpp
#include <algorithm>
#include <cctype>
#include <cstring>
#include <iostream>
#include <utility>

class Buffer {
public:
    explicit Buffer(const char* text) : size_(std::strlen(text)), data_(new char[size_ + 1]) {
        std::copy(text, text + size_ + 1, data_);
    }
    ~Buffer() { delete[] data_; }                                      // 1. destructor

    Buffer(const Buffer& other)                                        // 2. copy constructor
        : size_(other.size_), data_(new char[size_ + 1]) {
        std::copy(other.data_, other.data_ + size_ + 1, data_);        //    deep copy
        std::cout << "  copy ctor\n";
    }
    Buffer& operator=(const Buffer& other) {                           // 3. copy assignment
        std::cout << "  copy assign\n";
        if (this != &other) {
            Buffer tmp(other);                                         //    copy first...
            swap(tmp);                                                 //    ...then swap in
        }
        return *this;
    }
    Buffer(Buffer&& other) noexcept                                    // 4. move constructor
        : size_(std::exchange(other.size_, 0)), data_(std::exchange(other.data_, nullptr)) {
        std::cout << "  move ctor\n";
    }
    Buffer& operator=(Buffer&& other) noexcept {                       // 5. move assignment
        std::cout << "  move assign\n";
        Buffer tmp(std::move(other));
        swap(tmp);
        return *this;
    }

    void swap(Buffer& other) noexcept {
        std::swap(size_, other.size_);
        std::swap(data_, other.data_);
    }
    void upper() { for (std::size_t i = 0; i < size_; ++i) data_[i] = static_cast<char>(std::toupper(data_[i])); }
    const char* c_str() const { return data_ ? data_ : ""; }
private:
    std::size_t size_;
    char* data_;
};

int main() {
    Buffer a("hello");
    Buffer b = a;              // copy ctor
    b.upper();
    std::cout << a.c_str() << ' ' << b.c_str() << '\n';

    Buffer c("tmp");
    c = a;                     // copy assign
    std::cout << c.c_str() << '\n';
}
Outputcompiled & run with real C++
  copy ctor
hello HELLO
  copy assign
  copy ctor
hello

Copy assignment is written as "copy into a temporary, then swap" (the copy-and-swap idiom): if the copy throws, the object is untouched, and self-assignment is safe. Changing b no longer touches a: each owns its own buffer.

In real jobs
Code review will ask "why does this class have a destructor?" If the answer is "to delete a pointer member", the fix is usually to make the member a std::unique_ptr or a container and delete all five special functions from the class. Hand-written rule-of-five classes belong in low-level library code. You can also forbid copying outright with T(const T&) = delete; and T& operator=(const T&) = delete;.
06

Move semantics and std::move

Copying a vector of a million strings copies a million strings. But often the source is about to be thrown away anyway: a temporary returned from a function, or a local you are done with. Moving steals the source's resources instead (for a vector: take its heap pointer and leave it empty), which costs a few pointer assignments regardless of size.

The compiler moves automatically from temporaries (rvalues). For a named variable (an lvalue) it copies, because you might still use it. std::move(x) is how you say "I am done with x, you may move from it". Despite the name, std::move moves nothing: it is a cast to an rvalue reference (T&&), which makes overload resolution pick the move constructor or move assignment. A moved-from object is left in a valid but unspecified state: you may destroy it or assign a new value to it, but do not rely on what it contains.

C++main.cpp
#include <iostream>
#include <string>
#include <utility>
#include <vector>

struct Track {
    std::string name;
    explicit Track(std::string n) : name(std::move(n)) {}
    Track(const Track& o) : name(o.name) { std::cout << "  copy " << name << '\n'; }
    Track(Track&& o) noexcept : name(std::move(o.name)) { std::cout << "  move " << name << '\n'; }
};

Track makeTrack() { return Track("intro"); }      // built directly in the caller

int main() {
    std::vector<Track> playlist;
    playlist.reserve(4);                          // no reallocation noise below

    Track a("verse");
    std::cout << "push_back(a):\n";
    playlist.push_back(a);                        // a is still needed: copy

    Track b("chorus");
    std::cout << "push_back(std::move(b)):\n";
    playlist.push_back(std::move(b));             // done with b: move

    std::cout << "push_back(makeTrack()):\n";
    playlist.push_back(makeTrack());              // temporary: moved automatically

    std::cout << "a is still " << a.name << ", playlist has " << playlist.size() << '\n';
}
Outputcompiled & run with real C++
push_back(a):
  copy verse
push_back(std::move(b)):
  move chorus
push_back(makeTrack()):
  move intro
a is still verse, playlist has 3
Your turn

Remove playlist.reserve(4); and run it again. Extra move lines appear when the vector grows and moves its existing elements to a new buffer. Now remove noexcept from the move constructor: the vector switches to copying old elements when it grows, because it cannot risk a move that throws halfway.

VisualizeCopy versus move into a vectorStep 1 / 5
Track a("verse");
playlist.push_back(a);
Track b("chorus");
playlist.push_back(std::move(b));
playlist.push_back(makeTrack());
Line 1

a is a named object: an lvalue.

Variables now
a"verse"
playlist[]
All 5 steps as a table
StepLineWhat happenedVariables now
11a is a named object: an lvalue.a = "verse" playlist = []
22An lvalue argument picks the copy constructor. a keeps its string and the vector gets its own copy.a = "verse" playlist = ["verse"]
33b is another lvalue.a = "verse" b = "chorus" playlist = ["verse"]
44std::move(b) casts b to Track&&, so the move constructor is chosen and steals b's string buffer.a = "verse" b = moved-from (unspecified) playlist = ["verse", "chorus"]
55makeTrack() returns a temporary, which is already an rvalue: it is moved in without any std::move.a = "verse" b = moved-from (unspecified) playlist = ["verse", "chorus", "intro"]
  • Mark move constructors and move assignment noexcept. Containers only use your move when it cannot throw.
  • Do not write return std::move(local);. Returning a local already moves it or, better, elides the copy entirely; std::move there blocks that optimisation (clang warns with -Wpessimizing-move).
  • Do not std::move a const object: const T&& cannot bind to the move constructor, so it silently copies.
  • A "sink" parameter that will be stored can be taken by value and moved into place, as the constructors in this module do: explicit Track(std::string n) : name(std::move(n)) {}.
07

Operator overloading and friends

You can give your types their own meaning for operators such as +, ==, < and <<, so a Money or Vector2 reads as naturally as an int. An operator is just a function named operator+, operator== and so on. Guidelines that keep overloads unsurprising:

  • Only when the meaning is obvious. + on money adds amounts; + on a Customer means nothing.
  • += as a member, + as a non-member built from it. A non-member treats both operands the same way, including conversions on the left.
  • ==: in C++20, bool operator==(const T&) const = default; compares every member, and != comes for free.
  • << for printing must be a non-member, because the left operand is the std::ostream, not your object. It returns the stream so calls chain.

A friend declaration inside a class lets one named non-member function (or class) access its private members. It is the standard way to write operator<< for a class with private data. Friendship is granted by the class, never taken; keep it to operators and tightly coupled helpers.

C++main.cpp
#include <iomanip>
#include <iostream>

class Money {
public:
    explicit Money(long long paise = 0) : paise_(paise) {}
    static Money rupees(long long r) { return Money(r * 100); }

    Money& operator+=(const Money& other) {           // member: modifies *this
        paise_ += other.paise_;
        return *this;
    }
    bool operator==(const Money&) const = default;    // C++20: member-wise ==, and != too
    auto operator<=>(const Money&) const = default;   // C++20: <, <=, >, >= from one line

    friend std::ostream& operator<<(std::ostream& os, const Money& m) {
        return os << "Rs " << m.paise_ / 100 << '.'
                  << std::setw(2) << std::setfill('0') << m.paise_ % 100;
    }
private:
    long long paise_;
};

Money operator+(Money a, const Money& b) {            // non-member, built from +=
    a += b;
    return a;
}

int main() {
    Money coffee(14950);                              // Rs 149.50
    Money snack = Money::rupees(60);
    Money bill = coffee + snack;
    std::cout << bill << '\n';

    bill += Money(5);
    std::cout << bill << '\n';
    std::cout << std::boolalpha << (coffee + snack == Money(20950)) << ' '
              << (snack < coffee) << '\n';
}
Outputcompiled & run with real C++
Rs 209.50
Rs 209.55
true true
Your turn

Add Money operator*(const Money& m, int times) and print coffee * 3. Then try 3 * coffee: it fails until you add the mirror-image overload.

Error you will hit

invalid operands to binary expression ('ostream' and 'Point')

C++
#include <iostream>

struct Point {
    int x;
    int y;
};

int main() {
    Point p{3, 4};
    std::cout << p << '\n';
}
main.cpp:10:15: error: invalid operands to binary expression ('ostream' (aka 'basic_ostream<char>') and 'Point')
   10 |     std::cout << p << '\n';
      |     ~~~~~~~~~ ^  ~
basic_ostream.h:349:55: note: candidate function template not viable: no known conversion from 'Point' to 'char' for 2nd argument
... (about 50 more notes, one for every operator<< the library defines)
Why the compiler said that

std::cout only knows how to print the types the library gave it an operator<< for. Clang then lists every one of them as a rejected candidate, which buries the real message. With << errors, read the first line and skip the notes.

The fix

Write an operator<< for your type. It takes the stream by reference and returns it so calls chain.

C++
std::ostream& operator<<(std::ostream& os, const Point& p) {
    return os << '(' << p.x << ", " << p.y << ')';
}
Class
A user-defined type bundling data members and member functions. Members are private by default.
Struct
Identical to a class except that members are public by default. Used for plain data.
Encapsulation
Keeping data private so it can only change through member functions that preserve the class's invariant.
Member initializer list
The : a_(x), b_(y) part of a constructor that initialises members directly. Runs in declaration order.
Delegating constructor
A constructor that calls another constructor of the same class in its initializer list.
Destructor
~T(): runs automatically when an object's lifetime ends. Releases resources.
this
A pointer to the object a member function was called on.
const member function
A member function marked const that promises not to modify the object. The only kind callable on a const object.
Static member
A member that belongs to the class rather than any object: one shared copy.
Rule of three / five
If you write any of destructor, copy constructor, copy assignment (and move constructor, move assignment), you probably need all of them.
Move semantics
Transferring a resource from an object that is about to be discarded instead of copying it. Triggered by rvalues and std::move.
friend
A declaration that lets a named function or class access private members.
Quick check

A class has a raw int* data_ member allocated with new[] in the constructor and freed in the destructor. It has no copy constructor. What happens with T b = a;?

Quick check

What does std::move(x) do by itself?

Frequently asked questions

What is the difference between a struct and a class in C++?
Only the default access: struct members and base classes are public by default, class members are private by default. Everything else, including constructors, member functions and inheritance, works the same. By convention, struct is used for plain data and class for types that protect an invariant.
What is the rule of three and the rule of five in C++?
If a class needs a hand-written destructor, copy constructor or copy assignment operator, it needs all three, because they manage the same resource. The rule of five adds the move constructor and move assignment. The rule of zero says to avoid all of them by holding resources in members like std::string, std::vector and std::unique_ptr.
When should I use std::move?
When you pass or assign a named object that you will not use again, such as pushing a local string into a vector or storing a constructor parameter in a member. Do not use it on return statements for locals (it blocks copy elision) or on const objects (it silently copies).

Finish the C++ handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.