C++ Classes, Constructors, and the Rule of Three
Design C++ classes with constructors, destructors, member initializer lists, and the Rule of Three and Five to manage resources safely.
What you'll learn
- ✓Declare classes with public and private members
- ✓Use member initializer lists correctly
- ✓Implement constructors, destructors, copy and move
- ✓Apply the Rule of Three, Five, and Zero
- ✓Encapsulate invariants behind methods
Prerequisites
- •Pointers and memory — see /blog/cpp-pointers-and-memory
A C++ class bundles data with the operations that maintain its invariants. Done well, classes turn dangerous resources into safe values you can copy, move, and destroy without thought. Done poorly, they leak and crash. The difference is a small set of rules.
A first class
class Point {
public:
Point(double x, double y) : x_(x), y_(y) {}
double x() const { return x_; }
double y() const { return y_; }
void translate(double dx, double dy) {
x_ += dx;
y_ += dy;
}
private:
double x_;
double y_;
};
Members are private by default in class (public in struct; the only difference). The trailing underscore is a common convention to distinguish members from parameters.
Member initializer lists
Initialize members in the colon-separated list after the constructor signature. This calls each member’s constructor directly. Assigning inside the body would first default-construct and then assign — wasteful and sometimes impossible for const or reference members.
class User {
public:
User(std::string name, int age)
: name_(std::move(name)), age_(age) {}
private:
std::string name_;
const int age_;
};
const members and references can only be set via the initializer list. Always prefer it.
const member functions
A member function marked const promises not to modify the object. Getters should be const. The compiler refuses to call non-const methods on a const reference, which is exactly the safety you want.
double x() const { return x_; } // ok on a const Point&
void set_x(double v) { x_ = v; } // not ok on const
The destructor
The destructor runs when the object’s lifetime ends. For classes that own raw resources — heap memory, file handles, sockets — the destructor must release them.
class Buffer {
public:
Buffer(std::size_t n) : data_(new char[n]), size_(n) {}
~Buffer() { delete[] data_; }
private:
char* data_;
std::size_t size_;
};
The bug: the compiler-generated copy constructor and copy assignment will copy the pointer, leading to a double-free when both copies are destroyed.
The Rule of Three
If your class needs a custom destructor, it almost certainly also needs a custom copy constructor and copy assignment. This is the Rule of Three.
class Buffer {
public:
Buffer(std::size_t n) : data_(new char[n]), size_(n) {}
Buffer(const Buffer& other)
: data_(new char[other.size_]), size_(other.size_) {
std::copy(other.data_, other.data_ + size_, data_);
}
Buffer& operator=(const Buffer& other) {
if (this == &other) return *this;
char* fresh = new char[other.size_];
std::copy(other.data_, other.data_ + other.size_, fresh);
delete[] data_;
data_ = fresh;
size_ = other.size_;
return *this;
}
~Buffer() { delete[] data_; }
private:
char* data_;
std::size_t size_;
};
The copy assignment uses the strong-exception pattern: allocate first, then free.
The Rule of Five
C++11 added move semantics. If you write the three above, also write move constructor and move assignment — that is the Rule of Five. Moves transfer ownership instead of copying, which is cheap.
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
Buffer& operator=(Buffer&& other) noexcept {
if (this == &other) return *this;
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
return *this;
}
noexcept lets standard containers move your type cheaply during reallocation.
The Rule of Zero
The best Rule of Three implementation is no implementation. If every member of your class is itself a value type that manages its own resources — std::string, std::vector, std::unique_ptr — the compiler-generated copy, move, and destruction do the right thing automatically.
class Document {
public:
Document(std::string title) : title_(std::move(title)) {}
private:
std::string title_;
std::vector<std::string> paragraphs_;
};
You wrote nothing about destruction, copying, or moving and got all five for free. Pursue this style aggressively. See Smart Pointers for how to apply it to heap-owned data.
Encapsulation
Private data plus public methods is the unit of encapsulation. Invariants live in the methods, not at every call site.
class Account {
public:
void deposit(int cents) {
if (cents <= 0) throw std::invalid_argument("non-positive");
balance_ += cents;
}
int balance() const { return balance_; }
private:
int balance_ = 0;
};
Callers cannot accidentally violate the invariant because they cannot reach balance_.
Static members
A static member belongs to the class, not any instance. Use it for shared state or counters.
class Widget {
public:
Widget() { ++count_; }
~Widget() { --count_; }
static int alive() { return count_; }
private:
inline static int count_ = 0; // C++17
};
Explicit constructors
A single-argument constructor enables implicit conversions, which often surprises. Mark such constructors explicit unless you specifically want the conversion.
class Meters {
public:
explicit Meters(double v) : v_(v) {}
private:
double v_;
};
Meters m(2.5); // ok
// Meters m = 2.5; // error — explicit required
What is next
Read Templates to make classes work with any type, then STL Containers to see the standard library’s value-type style in action.
Wrap up
A C++ class is a contract. Initializer lists keep construction efficient. The Rule of Three handles raw ownership when you must. The Rule of Zero, achieved by composing existing safe types, is the modern target. Mark const aggressively, mark single-argument constructors explicit, and let the type system enforce your invariants.