Skip to content
Home
C++ Object-Oriented Programming: Classes, Inheritance, and...

C++ Object-Oriented Programming: Classes, Inheritance, and...

C/C++ C/C++ 8 min read 1556 words Beginner ExcellentWiki Editorial Team

C++ extends C with object-oriented programming, giving you classes, inheritance, polymorphism, and encapsulation. The language’s philosophy is that you pay only for what you use — virtual dispatch, RTTI, and exception handling all have some cost and can be avoided when not needed. Modern C++ (C++11 and later) adds move semantics, lambda expressions, and smart pointers that make OOP safer and more expressive.

This guide covers C++ OOP from the ground up, with modern C++ best practices throughout.

Classes and Objects

#include <iostream>
#include <string>

class Person {
private:
    std::string name_;
    int age_;

public:
    // Constructor
    Person(std::string name, int age)
        : name_(std::move(name))
        , age_(age)
    {}

    // Member functions
    void introduce() const {
        std::cout << "Hi, I'm " << name_ << " and I'm " << age_ << " years old.\n";
    }

    // Getter
    std::string name() const { return name_; }
    int age() const { return age_; }

    // Setter
    void set_age(int age) { age_ = age; }
---;

int main() {
    Person alice("Alice", 30);
    alice.introduce();
---

Constructors

class Widget {
public:
    // Default constructor
    Widget() : id_(0), name_("default") {}

    // Parameterized constructor
    Widget(int id, std::string name)
        : id_(id)
        , name_(std::move(name))
    {}

    // Copy constructor
    Widget(const Widget& other)
        : id_(other.id_)
        , name_(other.name_)
    {
        std::cout << "Copy constructed\n";
    }

    // Move constructor
    Widget(Widget&& other) noexcept
        : id_(other.id_)
        , name_(std::move(other.name_))
    {
        other.id_ = 0;
        std::cout << "Move constructed\n";
    }

    // Copy assignment
    Widget& operator=(const Widget& other) {
        if (this != &other) {
            id_ = other.id_;
            name_ = other.name_;
        }
        return *this;
    }

    // Move assignment
    Widget& operator=(Widget&& other) noexcept {
        if (this != &other) {
            id_ = other.id_;
            name_ = std::move(other.name_);
            other.id_ = 0;
        }
        return *this;
    }

    // Destructor
    ~Widget() = default;

private:
    int id_;
    std::string name_;
---;

Inheritance

class Animal {
public:
    explicit Animal(std::string name)
        : name_(std::move(name))
    {}

    virtual ~Animal() = default;

    virtual void speak() const {
        std::cout << name_ << " makes a sound.\n";
    }

    std::string name() const { return name_; }

private:
    std::string name_;
---;

class Dog : public Animal {
public:
    using Animal::Animal;  // Inherit constructors

    void speak() const override {
        std::cout << name() << " barks: Woof! Woof!\n";
    }

    void fetch() const {
        std::cout << name() << " fetches the ball.\n";
    }
---;

class Cat : public Animal {
public:
    using Animal::Animal;

    void speak() const override {
        std::cout << name() << " meows: Meow!\n";
    }
---;

Access Specifiers

  • public — accessible from anywhere
  • protected — accessible from the class and derived classes
  • private — accessible only from the class itself
class Base {
public:
    int pub;
protected:
    int prot;
private:
    int priv;
---;

class Derived : public Base {
    // pub is public
    // prot is protected
    // priv is inaccessible
---;

class DerivedProtected : protected Base {
    // pub becomes protected
    // prot stays protected
    // priv is inaccessible
---;

Polymorphism

Virtual Functions

void make_animal_speak(const Animal& animal) {
    animal.speak();  // Virtual dispatch — calls the correct override
---

int main() {
    Dog buddy("Buddy");
    Cat whiskers("Whiskers");

    make_animal_speak(buddy);     // "Buddy barks: Woof! Woof!"
    make_animal_speak(whiskers);  // "Whiskers meows: Meow!"

    // Polymorphic container
    std::vector<std::unique_ptr<Animal>> animals;
    animals.push_back(std::make_unique<Dog>("Rex"));
    animals.push_back(std::make_unique<Cat>("Luna"));

    for (const auto& animal : animals) {
        animal->speak();
    }
---

Pure Virtual Functions (Abstract Classes)

class Shape {
public:
    virtual ~Shape() = default;

    virtual double area() const = 0;    // Pure virtual
    virtual double perimeter() const = 0;

    void print() const {
        std::cout << "Area: " << area()
                  << ", Perimeter: " << perimeter() << '\n';
    }
---;

class Circle : public Shape {
public:
    explicit Circle(double radius) : radius_(radius) {}

    double area() const override {
        return pi * radius_ * radius_;
    }

    double perimeter() const override {
        return 2 * pi * radius_;
    }

private:
    static constexpr double pi = 3.141592653589793;
    double radius_;
---;

class Rectangle : public Shape {
public:
    Rectangle(double width, double height)
        : width_(width), height_(height)
    {}

    double area() const override {
        return width_ * height_;
    }

    double perimeter() const override {
        return 2 * (width_ + height_);
    }

private:
    double width_, height_;
---;

RAII (Resource Acquisition Is Initialization)

RAII is the most important C++ concept. Resources (memory, file handles, mutexes, sockets) are acquired during construction and released during destruction.

class FileHandle {
public:
    explicit FileHandle(const std::string& filename, const std::string& mode)
        : file_(fopen(filename.c_str(), mode.c_str()))
    {
        if (!file_) {
            throw std::runtime_error("Failed to open file: " + filename);
        }
    }

    // Destructor releases the resource
    ~FileHandle() {
        if (file_) {
            fclose(file_);
        }
    }

    // No copying (or implement properly)
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    // Move is fine
    FileHandle(FileHandle&& other) noexcept
        : file_(std::exchange(other.file_, nullptr))
    {}

    void write(const std::string& data) {
        fprintf(file_, "%s", data.c_str());
    }

private:
    FILE* file_;
---;

Smart Pointers (Modern RAII)

#include <memory>

// std::unique_ptr — exclusive ownership
auto ptr = std::make_unique<Dog>("Buddy");
ptr->speak();

// Cannot copy unique_ptr
// auto ptr2 = ptr;  // Error

// Can move
auto ptr2 = std::move(ptr);

// std::shared_ptr — shared ownership
auto shared = std::make_shared<Cat>("Whiskers");
{
    auto shared2 = shared;  // Reference count: 2
    shared2->speak();
---  // Reference count: 1

shared->speak();  // Still alive
// Reference count: 0 — object destroyed

Modern C++ Best Practices

// 1. Use 'override' consistently
class Derived : public Base {
    void func() override;  // Compiler checks it actually overrides
---;

// 2. Prefer '= default' and '= delete'
class NonCopyable {
public:
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
---;

// 3. Use 'constexpr' for compile-time constants
constexpr double gravity = 9.81;

// 4. Use 'nullptr' instead of NULL or 0
int* ptr = nullptr;

// 5. Use range-based for loops
std::vector<int> vec = {1, 2, 3, 4, 5};
for (const auto& v : vec) {
    std::cout << v << '\n';
---

// 6. Use 'auto' for type deduction
auto result = compute_something();

// 7. Prefer std::array over C arrays
std::array<int, 5> arr = {1, 2, 3, 4, 5};

// 8. Use 'const' whenever possible
void print_data(const std::vector<int>& data);

Conclusion

C++ OOP gives you fine-grained control over memory, performance, and object relationships. Master classes, inheritance, and virtual functions for polymorphism. Internalize RAII — it is the key to safe resource management in C++. Use smart pointers and follow the Rule of Five (or Zero) for resource management.

Related: Check our C++ STL guide.

Virtual Functions and the Vtable

Virtual functions enable polymorphic behavior through dynamic dispatch. When a class declares a virtual function, the compiler creates a virtual table (vtable) — an array of function pointers. Each object of that class carries a hidden vtable pointer (vptr) that points to the class’s vtable:

class Shape {
public:
    virtual void draw() const = 0;  // Pure virtual — abstract class
    virtual ~Shape() = default;     // Virtual destructor
---;

class Circle : public Shape {
    void draw() const override {
        // Circle-specific drawing code
    }
---;

Calling a virtual function incurs a small runtime cost: two pointer indirections (vptr → vtable → function address). In most applications this overhead is negligible, but in tight loops or real-time systems, virtual calls may need to be avoided.

The Rule of Three / Five / Zero

  • Rule of Three: If a class needs a custom destructor, copy constructor, or copy assignment operator, it likely needs all three.
  • Rule of Five: With move semantics, also consider move constructor and move assignment.
  • Rule of Zero: Design classes that do not require custom destructors — use smart pointers and standard containers instead.
// Rule of Zero — no custom destructor needed
class Person {
    std::string name;  // Destructor, copy, move all auto-generated
    std::unique_ptr<Address> address;  // Smart pointer handles cleanup
---;

Inheritance Hierarchies

Deep inheritance hierarchies are often a design smell. Prefer composition over inheritance — use member objects rather than extending base classes. Consider alternatives like CRTP (Curiously Recurring Template Pattern) for static polymorphism, or std::variant + std::visit for closed type hierarchies.

Virtual Destructors

If a class has any virtual functions, it must have a virtual destructor. Otherwise, deleting a derived object through a base pointer causes undefined behavior — only the base destructor runs, and derived resources leak:

Shape *s = new Circle();
delete s;  // Undefined behavior if ~Shape() is not virtual

FAQ

What is the difference between malloc and new in C++? malloc allocates raw memory without calling constructors; new allocates memory and calls the constructor. In C++, prefer new for objects. free vs delete follows the same pattern — delete calls the destructor.

How do I prevent memory leaks in C/C++? Use RAII (Resource Acquisition Is Initialization) in C++ — smart pointers like std::unique_ptr and std::shared_ptr automatically free memory. In C, always pair every malloc with a free and use tools like Valgrind or AddressSanitizer to detect leaks.

What is undefined behavior in C/C++? Undefined behavior occurs when code performs operations that the language standard does not define — dereferencing a null pointer, buffer overflow, signed integer overflow. The compiler may generate any code, including unexpected results or crashes.

Should I learn C or C++ first? Learn C first if you want to understand low-level memory and system programming. Learn C++ first if you want object-oriented features and the STL. Both are valuable; C++ builds on C concepts.

What is the difference between a header file and a source file? Header files (.h) declare interfaces — function prototypes, class definitions, macros. Source files (.c or .cpp) implement the declarations. Headers are #included; source files are compiled separately and linked.

For a comprehensive overview, read our article on C File Io Guide.

Section: C/C++ 1556 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top