| Lesson 2 |
Object-oriented programming |
| Objective |
Basic Concepts behind Object-Oriented Programming in C++ |
Object-Oriented Programming in C++ (Four Pillars and Class Design Explained)
Object-oriented programming (OOP) is a balanced approach to writing software in which data and behavior are packaged together. This encapsulation creates user-defined types that extend and interact with the native types of the language. The term OOP refers to a collection of related concepts: user-defined types can be easily created and used, and the implementation details of those user-defined types are hidden from callers. Lesson 1 introduced the module's focus on building C++ classes — this lesson establishes the conceptual foundation that every subsequent lesson builds on. The four pillars of OOP — encapsulation, inheritance, polymorphism, and abstraction — organize the concepts that the rest of this module explores in code.
Procedural vs Object-Oriented Programming
The Procedural Approach — Data and Functions Separate
Almost everything a programmer encounters before studying OOP has been procedural programming — writing solutions in terms of fundamental data types and standalone functions. In procedural C, you define a struct to hold data and write separate functions that accept that struct as a parameter. The data and the behavior that operates on it are distinct entities. A program to track bank accounts might define a struct Account with a balance field, and separate functions deposit() and withdraw() that accept a pointer to an Account. The connection between the data and the functions that operate on it exists only in the programmer's mind and in the calling conventions — nothing in the language enforces or expresses that relationship.
The OOP Approach — Data and Behavior Bundled
The essence of object-oriented programming is that you write programs in terms of objects in the
domain of the problem you are trying to solve. Part of the program development process involves designing a set of types that suit the problem context. If you are writing a program to track bank accounts, you define types such as
Account and
Transaction. For a program to analyze baseball scores, you define types such as
Player and
Team. The variables of fundamental types —
int,
double,
char — do not allow you to model real-world objects meaningfully. A baseball player cannot be realistically represented by a single
int or
double. You need several values of a variety of types, bundled together with the operations that make sense for that entity. Classes provide that solution: a class type is a composite of variables of fundamental types or other class types, and it can also have functions as an integral part of its definition.
The Extra Design Step — Designing Types for the Problem
With OOP, the programming task is frequently more demanding than normal procedural programming. There is at least one extra design step before reaching the coding of algorithms — the design of types that are appropriate for the problem at hand. This often requires solving the problem more generally than is strictly necessary for the immediate case. The belief — borne out by decades of software engineering practice — is that this investment pays dividends in several ways: the solution will be more self-contained and thus more robust and easier to maintain and change, and the solution will be more reusable. A well-designed Account class can serve a savings account program, a checking account program, and a loan management program without modification — because the design captured the essential properties of an account rather than the specific needs of one program.
The Four Pillars of OOP in C++
Encapsulation — Bundling Data and Behavior
Encapsulation is the bundling of data and the methods that operate on that data within a single unit — the class. It also involves restricting direct access to some of an object's components, preventing accidental interference and misuse. In C++, encapsulation is enforced through access specifiers: public members form the interface that callers can use, private members are the implementation details that callers cannot touch. A BankAccount class encapsulates the balance as a private data member and exposes deposit() and withdraw() as public member functions — callers can change the balance only through those controlled operations, not by writing directly to the balance field. Encapsulation is the subject of the preceding and following lessons in this module — access specifiers, member functions, and the distinction between interface and implementation.
Inheritance — Reusing and Extending Types
Inheritance allows a new class — the derived class or subclass — to adopt the properties and methods of an existing class — the base class or superclass. The derived class inherits the interface and implementation of the base class and can extend it with additional data members and member functions, or override existing member functions with specialized behavior. This promotes code reusability: a SavingsAccount class can inherit from a general Account base class, reusing all the account management logic while adding interest calculation behavior specific to savings accounts. Inheritance establishes an "is-a" relationship: a SavingsAccount is an Account, which means it can be used anywhere an Account is expected. Inheritance hierarchies and their design are covered in detail in the inheritance modules of this course.
Polymorphism — One Interface, Many Behaviors
Polymorphism refers to the ability of a function or method to behave differently based on the object that invokes it. In C++, polymorphism takes two primary forms. Compile-time polymorphism is achieved through function overloading — multiple functions with the same name but different parameter lists, resolved by the compiler based on the arguments at the call site. A draw() function might be overloaded to accept a Circle, a Rectangle, or a Triangle — the compiler selects the correct version at compile time. Runtime polymorphism is achieved through virtual functions — a base class declares a function virtual, and each derived class overrides it with its own implementation. A call through a base class pointer or reference selects the derived class's version at runtime, allowing a single interface to produce different behavior depending on the actual type of the object. This enables the open/closed principle: code that operates on base class pointers remains unchanged when new derived classes are added.
Abstraction — Hiding Complexity Behind Interfaces
Abstraction means providing only essential information to the outside world while hiding internal details. It simplifies complex reality by modeling classes appropriate to the problem — defining what an object does without exposing how it does it. A Stack class abstracts the concept of last-in, first-out storage: callers push and pop elements without knowing whether the implementation uses an array, a linked list, or a std::deque. In C++, abstraction is most formally expressed through pure virtual functions and abstract base classes — a base class that declares functions as virtual ... = 0 defines an interface that derived classes must implement, with no implementation provided at the base level. C++20 Concepts extend this further, allowing template parameters to be constrained to types that satisfy a specified interface — compile-time abstraction over type requirements.
Classes and Objects — The Foundation
Writing Programs in Terms of Domain Objects
You define a new data type by defining a class. A class in C++ can be thought of as a blueprint or template for creating objects — it defines the properties (data members) and behaviors (member functions) that every object of that type will have. An object is an instance of a class: a specific realization of the blueprint with its own state, defined by the current values of its data members. You could define a class type called Box that contains variables storing a length, a width, and a height to represent boxes. You could then define variables of type Box, just as you define variables of fundamental types. Each Box object would contain its own dimensions, and you could create and manipulate as many Box objects as you need in a program.
The Box Class — A Concrete Example
The following Box class illustrates the OOP concepts introduced in this lesson using simple C++23 syntax. The data members are private — callers cannot set a negative dimension directly. The public interface provides controlled access through the constructor and the volume() member function:
#include <stdexcept> // std::invalid_argument
class Box {
public:
// Constructor — initializes all three dimensions
// Throws if any dimension is non-positive (enforces invariant)
Box(double length, double width, double height)
: length_{length}, width_{width}, height_{height}
{
if (length_ <= 0 || width_ <= 0 || height_ <= 0) {
throw std::invalid_argument("Box dimensions must be positive");
}
}
// Member functions — const because they do not modify the object
[[nodiscard]] double volume() const noexcept { return length_ * width_ * height_; }
[[nodiscard]] double length() const noexcept { return length_; }
[[nodiscard]] double width() const noexcept { return width_; }
[[nodiscard]] double height() const noexcept { return height_; }
// Scales all dimensions by a factor
void scale(double factor) {
if (factor <= 0) throw std::invalid_argument("Scale factor must be positive");
length_ *= factor;
width_ *= factor;
height_ *= factor;
}
private:
double length_; // data members: private, inaccessible to callers
double width_;
double height_;
};
// Usage
int main() {
Box b{3.0, 4.0, 5.0}; // constructor called automatically
double v = b.volume(); // 60.0
b.scale(2.0); // doubles all dimensions
double v2 = b.volume(); // 480.0
// b.length_ = -1.0; // compile error: length_ is private
}
Member Functions and Member Attributes
Member attributes are the data variables contained within a class — length_, width_, and height_ in the Box example. Member functions are the functions defined inside the class that operate on that data — volume(), scale(), and the accessors. The trailing underscore on member variable names (length_) is a widely used C++ convention for distinguishing member variables from local variables and parameters, making code easier to read at a glance. Member functions that do not modify the object's state are declared const — they can be called on both const and non-const objects and through const references.
Constructors and Destructors
Constructors — Initializing Object State
Constructors are special member functions called automatically when an object is created — they initialize the object's data members to a valid state. In the Box class, the constructor takes three double parameters and uses a member initializer list (: length_{length}, ...) to initialize the private data members before the constructor body executes. The constructor also enforces the class invariant — that all dimensions must be positive — by throwing an exception if any dimension is zero or negative. This means a Box object, if it exists, is always in a valid state: the constructor either produces a valid object or throws, and there is no intermediate "partially initialized" state that callers must guard against. C++ guarantees the constructor is called before any other member function can be invoked on the object.
Destructors — Releasing Resources
Destructors are called automatically when an object's lifetime ends — when it goes out of scope, when it is explicitly deleted, or when the program terminates. They are used to release resources held by the object: closing file handles, releasing network connections, freeing dynamically allocated memory. The destructor's name is the class name preceded by a tilde: ~Box(). For the Box class, which holds only double values with no dynamic allocation, the compiler-generated default destructor is correct and no explicit destructor is needed. For classes that manage dynamic resources — a custom string class, a socket wrapper, a database connection — the destructor is essential.
C++23 Resource Management — RAII
The C++ idiom of Resource Acquisition Is Initialization (RAII) ties resource lifetimes to object lifetimes by acquiring resources in the constructor and releasing them in the destructor. Because destructors are called automatically and deterministically, RAII eliminates the need for manual resource cleanup and prevents resource leaks even when exceptions occur. The standard library implements RAII throughout: std::unique_ptr and std::shared_ptr for dynamic memory, std::fstream for files, std::lock_guard for mutexes. In C++23, the same pattern extends to coroutine resources, std::expected error handling, and std::stacktrace capture — all managed through RAII wrapper types.
Method Overloading and Overriding
Overloading — Same Name, Different Parameters
Overloading occurs when two or more member functions in one class have the same name but different parameter lists — different types, different counts, or different order of parameters. The compiler selects the correct overload at the call site based on the types of the arguments provided. A
Box class might overload
scale() to accept either a single uniform factor or three separate factors for each dimension:
void scale(double factor); // uniform scale
void scale(double lx, double wy, double hz); // per-dimension scale
Overloading allows a natural, consistent interface — callers use the same name for conceptually similar operations rather than inventing distinct names like
scaleUniform() and
scalePerDimension().
Overriding — Redefining in a Derived Class
Overriding is redefining a virtual member function in a derived class where it was already defined in the base class. The derived class provides its own implementation, which replaces the base class implementation when the function is called through a pointer or reference to the base class. In C++11 and later, the override keyword should always be used on overriding functions — it instructs the compiler to verify that the function actually overrides a virtual function in the base class, catching the common error of a mismatched signature that silently creates a new function rather than overriding the intended one.
Why OOP Produces Better Software
Self-Contained, Robust Solutions
A well-designed class is self-contained: it enforces its own invariants, manages its own resources, and presents a stable interface that does not require callers to know its internal structure. The Box class enforces that all dimensions are positive in its constructor — no caller needs to check this condition or handle invalid state. The ch_stack class from lesson 9 enforces that the stack index is always within bounds — no caller needs to track the index or guard against overflow. This self-containment makes each class independently testable, independently replaceable, and far more robust than the equivalent procedural code where the caller is responsible for maintaining data validity.
Reusability Across Problems
A class designed to capture the essential properties of a domain concept — rather than the specific needs of one program — can be reused across multiple programs without modification. A Transaction class designed for a bank account program can be reused in an accounting system, a payment processing system, and an audit logging system. The investment in the extra design step that OOP requires pays dividends across the entire lifetime of the codebase, not just in the first program that uses the class. This is the practical meaning of the claim that OOP produces more reusable solutions — not that every class gets reused, but that the design discipline produces classes that can be reused when the need arises.
In the next lesson, you will learn about type extensibility and how C++ classes extend the type system beyond the built-in fundamental types.

