C++ Object-Oriented Programming: Classes, Inheritance, and...
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.