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 David Chen

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 1251 articles in section Report inaccuracy Back to top
DC
David Chen Technology Editor

Technology Editor at ExcellentWiki covering programming, engineering, and consumer technology topics.

View author profile →