Polyglot
Languages C++ oop
C++ § oop

Classes and OOP

C++ is an object-oriented language: classes are first-class user-defined types, with constructors, destructors, member functions, access control, and inheritance. The language admits single, multiple, and virtual inheritance; polymorphism is realised through virtual functions dispatched via a per-class virtual-function table. The mechanisms are integrated tightly with RAII, with templates, and with the value-semantic conventions of the standard library, and most non-trivial C++ programs are organised around classes. This page covers the construction surface — class definitions, constructors, member functions, access control, inheritance, virtual dispatch — and the conventions for using them idiomatically. The deeper interaction with templates is in Templates and Concepts; the resource-management discipline is in Memory and RAII.

Classes and members

A class is a user-defined type with members:

class Counter {
public:
    Counter() = default;
    explicit Counter(int initial) : count_(initial) {}

    void increment()        { ++count_; }
    int  value() const      { return count_; }

private:
    int count_ = 0;
};

Counter c;
c.increment();
int v = c.value();

Members may be:

  • Data members — variables that each instance carries.
  • Member functions — functions invoked on an instance, with implicit access to the instance through this.
  • Static members — variables and functions associated with the class itself, not with any instance.
  • Type members — nested type definitions and aliases (using value_type = T).

The keyword class and the keyword struct introduce class types. They differ only in the default access level: members and bases default to private in class, public in struct. The convention in idiomatic code is class for types with non-trivial behaviour and struct for aggregate types whose members are all public:

struct Point { double x = 0, y = 0; };       // aggregate
class  Widget { /* ... */ };                  // behavioural

Constructors

A constructor initialises an instance:

class Vector3 {
public:
    Vector3();                                  // default
    Vector3(double x, double y, double z);      // value
    Vector3(const Vector3 &other);              // copy
    Vector3(Vector3 &&other) noexcept;          // move

private:
    double x_, y_, z_;
};

Vector3::Vector3() : x_(0), y_(0), z_(0) {}
Vector3::Vector3(double x, double y, double z) : x_(x), y_(y), z_(z) {}
Vector3::Vector3(const Vector3 &other) : x_(other.x_), y_(other.y_), z_(other.z_) {}
Vector3::Vector3(Vector3 &&other) noexcept = default;

The portion before the body — : x_(x), y_(y), z_(z) — is the member initialiser list. Members are initialised here, in the order they appear in the class definition (regardless of the order in the list). The body of the constructor runs after all members are constructed; modification of members in the body is assignment, not initialisation.

The conventional form for member initialisation in modern C++ uses default member initialisers in the class definition itself:

class Vector3 {
public:
    Vector3() = default;
    Vector3(double x, double y, double z) : x_(x), y_(y), z_(z) {}

private:
    double x_ = 0;
    double y_ = 0;
    double z_ = 0;
};

The defaults make the default constructor’s job a single = default and document the initial state at the point of declaration.

Special member functions

Six functions get special treatment from the compiler:

FunctionGenerated by default if
Default constructor T()No constructor is user-declared
Destructor ~T()Always (unless deleted)
Copy constructor T(const T&)The class is copyable (subject to nuance)
Copy assignment T& operator=(const T&)The class is copy-assignable
Move constructor T(T&&)The class is movable and no copy operations are user-declared
Move assignment T& operator=(T&&)The class is move-assignable

Each may be:

  • Defaulted (= default) — request the compiler-generated implementation.
  • Deleted (= delete) — forbid the operation.
  • User-defined — supply a body.

The interactions among these defaults are subtle and are the subject of the rule of zero / three / five (treated in Memory and RAII).

explicit

A single-argument constructor declared explicit does not participate in implicit conversion:

class Counter {
public:
    Counter(int initial);          // permits Counter c = 5;  (implicit conversion)
};

class StrictCounter {
public:
    explicit StrictCounter(int initial);   // forbids implicit conversion
};

Counter        c1 = 5;             // OK
StrictCounter  c2 = 5;             // ERROR
StrictCounter  c3{5};              // OK: direct-initialisation

The conventional discipline is to declare any single-argument non-copy/move constructor explicit unless the implicit conversion is genuinely intended.

Destructors

A destructor releases resources owned by the instance:

class FileHandle {
public:
    explicit FileHandle(const std::string &path);
    ~FileHandle();

private:
    std::FILE *fp_ = nullptr;
};

FileHandle::FileHandle(const std::string &path)
    : fp_(std::fopen(path.c_str(), "r"))
{
    if (!fp_) throw std::runtime_error("open failed");
}

FileHandle::~FileHandle() {
    if (fp_) std::fclose(fp_);
}

The destructor runs automatically when the instance goes out of scope, when an exception unwinds the stack, or when delete is invoked on a pointer to the instance. The mechanism is the foundation of RAII.

A class that participates in inheritance — that is, that may be deleted through a pointer to a base class — must have a virtual destructor in the base:

class Shape {
public:
    virtual ~Shape() = default;     // virtual: derived destructors run correctly
};

class Circle : public Shape { /* ... */ };

std::unique_ptr<Shape> p = std::make_unique<Circle>();
// when p is destroyed, ~Circle runs first, then ~Shape — only because ~Shape is virtual

Without the virtual destructor, deleting through the base pointer runs only ~Shape, leaking whatever resources Circle held. The discipline is universal: any class designed to be a base must have a virtual destructor.

Member functions

Member functions are invoked on an instance and have implicit access to the instance through this:

class Counter {
public:
    void increment() { ++count_; }            // implicit this->count_
    void set(int v)  { this->count_ = v; }    // explicit, equivalent
    int  value() const { return count_; }     // const member: no modification

private:
    int count_;
};

The const-qualifier on the member function is part of the type: a const instance can invoke only const member functions. The convention is to mark every member function const that does not modify the instance.

Member functions may be defined inside the class (implicitly inline) or outside with the Class::name syntax:

class Counter {
public:
    void increment();
    int  value() const;

private:
    int count_;
};

void Counter::increment()        { ++count_; }
int  Counter::value() const      { return count_; }

Access control

Three access levels modify visibility:

  • public — accessible from any code.
  • protected — accessible from member functions, friends, and derived classes.
  • private — accessible from member functions and friends only.
class Widget {
public:
    Widget();
    void public_method();

protected:
    void protected_helper();

private:
    int internal_state_;
};

Access control prevents the compiler from generating code that accesses the protected or private member through ordinary means; it is not a security mechanism (a determined caller can still access the bytes through reinterpret_cast or similar). The discipline is conventional: public for the interface, private for the implementation, protected rarely (only when subclasses genuinely need access).

friend

A friend declaration grants another class or function access to the private and protected members:

class Counter {
    friend std::ostream &operator<<(std::ostream &os, const Counter &c) {
        return os << c.count_;
    }
};

The conventional uses are operator overloads that need internal access (most commonly the stream insertion operator) and tightly-coupled helper classes. Friendship is not transitive, not inherited, and does not propagate: friend grants are explicit and narrow.

Inheritance

A class may inherit from another:

class Animal {
public:
    virtual ~Animal() = default;
    virtual void speak() const = 0;        // pure virtual: must be overridden
    virtual std::string name() const { return "animal"; }
};

class Dog : public Animal {
public:
    void speak() const override { std::cout << "woof\n"; }
    std::string name() const override { return "dog"; }
};

class Cat : public Animal {
public:
    void speak() const override { std::cout << "meow\n"; }
};

The base class’s members become accessible (subject to the access level applied to the inheritance) in the derived class. The conventional inheritance form is class Derived : public Base; the public keyword preserves the access of inherited members. Other forms — protected and private inheritance — are rare and conceptually represent has-a rather than is-a relationships.

Virtual functions

A virtual function admits dynamic dispatch: a call through a pointer or reference to the base type dispatches to the derived class’s implementation:

std::vector<std::unique_ptr<Animal>> animals;
animals.push_back(std::make_unique<Dog>());
animals.push_back(std::make_unique<Cat>());

for (const auto &a : animals) {
    a->speak();      // dispatches to Dog::speak() or Cat::speak()
}

The mechanism is implemented through a virtual function table (vtable): each class with virtual functions has a per-class table of function pointers, and each instance carries a pointer to its class’s vtable. The dispatch — a->speak() — fetches the function pointer from the vtable and calls it. The cost is one indirection per call.

Several specifiers refine virtual functions:

  • virtual declares the function as dispatched dynamically.
  • override declares that a function in a derived class overrides a base-class virtual function. The compiler verifies; mismatches are diagnostics.
  • = 0 makes the function pure virtual; the class becomes abstract and cannot be instantiated.
  • final declares that the function may not be further overridden in derived classes.
class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;          // abstract
};

class Square : public Shape {
public:
    Square(double s) : side_(s) {}
    double area() const override final { return side_ * side_; }

private:
    double side_;
};

Multiple inheritance

C++ admits inheritance from more than one base class:

class Drawable { public: virtual void draw() const = 0; };
class Saveable { public: virtual void save() const = 0; };

class Document : public Drawable, public Saveable {
public:
    void draw() const override;
    void save() const override;
};

Multiple inheritance is rare in modern C++. The conventional uses are interface inheritance: a class implements multiple interfaces, each declaring pure virtual functions. The corresponding pattern in object-oriented languages with single inheritance — interfaces or protocols — has no separate construct in C++; multiple inheritance from abstract base classes is the substitute.

When two base classes have a common ancestor, the diamond problem arises: which copy of the common ancestor does the derived class inherit? Virtual inheritance selects a single shared instance:

class A { /* ... */ };
class B : virtual public A { /* ... */ };
class C : virtual public A { /* ... */ };
class D : public B, public C { /* ... */ };
// D has one A subobject, shared between B and C

Virtual inheritance is rare; its principal use is when an interface hierarchy genuinely needs the deduplication.

Polymorphism through templates

C++ admits a second form of polymorphism — static polymorphism — through templates. The pattern is the same — code that operates on multiple types — but the dispatch is at compile time:

template <typename Animal>
void describe(const Animal &a) {
    a.speak();
    std::cout << "name: " << a.name() << '\n';
}

Dog d;
Cat c;
describe(d);
describe(c);

The function does not require Dog and Cat to share a base class; it requires only that they expose speak() and name(). The mechanism is duck typing at compile time: any type that quacks like a duck satisfies the constraint.

The trade-offs:

  • Static polymorphism produces no runtime dispatch overhead and admits inlining; dynamic polymorphism has a vtable indirection per call.
  • Static polymorphism produces separate code per type; dynamic polymorphism shares one implementation across all types.
  • Static polymorphism requires the type to be known at the call site; dynamic polymorphism admits heterogeneous collections.

The conventional advice: use templates when the types are known at compile time, virtual dispatch when they are not. Concepts (C++20) make the static-polymorphism form considerably more pleasant by making the constraints explicit and the error messages tractable.

The CRTP

The Curiously Recurring Template Pattern uses static polymorphism for code reuse without virtual dispatch:

template <typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};

class Concrete : public Base<Concrete> {
public:
    void implementation() { /* ... */ }
};

Base provides an interface that delegates to Derived::implementation; the cast is well-defined because Concrete derives from Base<Concrete>. The pattern is occasionally useful for mixin-style behaviour without runtime cost.

Inheritance versus composition

A recurring design question: when to use inheritance versus when to use composition (a class containing another class as a member). The conventional advice:

  • Inheritance is appropriate for genuine is-a relationships, where every instance of the derived class is meaningfully an instance of the base class, and the base class’s interface is the right interface for the derived class’s clients.
  • Composition is appropriate for has-a relationships, where the contained type is an implementation detail or a non-substitutable component.

The bias in modern C++ is toward composition: inheritance is reserved for cases that genuinely require dynamic dispatch (a heterogeneous collection of related types) or that genuinely model is-a (animals, shapes, AST nodes). Composition is the more flexible default.

A note on the Liskov substitution principle

A derived class should be substitutable for its base: any code that works correctly with the base should work correctly with any derived class. The principle is informal — the language does not enforce it — but violating it produces brittle hierarchies. The conventional discipline:

  • Derived classes do not strengthen preconditions.
  • Derived classes do not weaken postconditions.
  • Derived classes preserve invariants of the base.
  • Derived classes do not throw exceptions the base does not.

Hierarchies that satisfy the principle are easier to reason about and easier to extend; hierarchies that violate it accumulate exceptions (“Square is-a Rectangle except not really”) that complicate every consumer.