Lesson 1
Building Classes in C++ Introduction
Welcome to Building Classes in C++, the second course in the C++ for C Programmers series. This course teaches the foundational techniques for working with classes and objects in C++, introducing encapsulation as a cornerstone of object-oriented programming (OOP). Rather than treating classes as mere syntactic extensions of C structs, this course explores how modern C++ classes express rich semantics through constructors, destructors, member functions, and access control, while leveraging C++23 features that make class design safer and more expressive than ever before. Whether you're transitioning from C, Java, or another language, mastering C++ class construction provides the foundation for building distributed systems, high-performance applications, and reusable libraries.
After completing this course, you will be able to write C++ programs that use the class construct to implement encapsulation of data and behavior, write member functions that act on member data, use constructor and destructor member functions to manage object lifetimes, implement useful dynamic data structures including dynamically allocated stacks, bounds-checking arrays, and singly linked lists, and apply modern resource management techniques using RAII and smart pointers.
What Makes C++ Classes Unique
C++ classes differ fundamentally from class constructs in other languages in several important ways. Unlike Java or Python, C++ gives developers explicit control over object lifetime, memory layout, and resource ownership. This control enables C++ classes to achieve performance characteristics impossible in garbage-collected languages while maintaining the organizational benefits of object-oriented design. Modern C++11 through C++23 has dramatically improved class design through move semantics, constexpr constructors, inline static members, concepts, and explicit object parameters, making C++ classes both more powerful and safer than their predecessors.
// Modern C++23 class demonstrating current best practices
#include <string>
#include <iostream>
class Person {
private:
std::string name;
int age;
public:
// Constructor with member initializer list
Person(const std::string& personName, int personAge)
: name(personName), age(personAge) {}
// Const getters - read-only access
const std::string& getName() const { return name; }
int getAge() const { return age; }
// Validated setter
void setAge(int newAge) {
if (newAge >= 0 && newAge < 150) {
age = newAge;
}
}
void display() const {
std::cout << "Name: " << name
<< ", Age: " << age << '\n';
}
};
Core Objective 1: Encapsulation
Encapsulation bundles data and the functions that operate on that data into a single unit, restricting direct access to internal state. In C++, access specifiers (private, protected, public) enforce these boundaries at compile time, unlike many other languages where such restrictions are conventions rather than enforced rules. Proper encapsulation prevents external code from placing objects in invalid states, simplifies debugging by narrowing where state changes can occur, and allows implementation changes without affecting client code.
class BankAccount {
private:
std::string accountNumber;
double balance;
std::vector<std::string> transactionLog;
public:
BankAccount(const std::string& acct, double initial)
: accountNumber(acct), balance(initial) {
transactionLog.push_back(
"Account opened: $" + std::to_string(initial)
);
}
// Controlled access through member functions
double getBalance() const { return balance; }
bool deposit(double amount) {
if (amount <= 0) return false;
balance += amount;
transactionLog.push_back("Deposit: $" + std::to_string(amount));
return true;
}
bool withdraw(double amount) {
if (amount <= 0 || amount > balance) return false;
balance -= amount;
transactionLog.push_back("Withdrawal: $" + std::to_string(amount));
return true;
}
};
Core Objective 2: Reusability and Single Responsibility
Well-designed C++ classes follow the Single Responsibility Principle—each class should have one clear purpose and one reason to change. This design philosophy creates classes that are easier to test, maintain, and reuse across different projects. A class that handles both data storage and network communication, for example, violates this principle and becomes difficult to reuse in contexts that require only one of those capabilities. Modern C++ reinforces single responsibility through concepts (C++20), which express interface requirements explicitly, allowing the compiler to verify that classes meet their contractual obligations.
// Each class has a single, clear responsibility
class TemperatureReading {
private:
double celsius;
std::string location;
public:
TemperatureReading(double temp, const std::string& loc)
: celsius(temp), location(loc) {}
double getCelsius() const { return celsius; }
double getFahrenheit() const { return celsius * 9.0 / 5.0 + 32; }
const std::string& getLocation() const { return location; }
};
class TemperatureLogger {
private:
std::vector<TemperatureReading> readings;
public:
void addReading(const TemperatureReading& reading) {
readings.push_back(reading);
}
double getAverageCelsius() const {
if (readings.empty()) return 0.0;
double sum = 0.0;
for (const auto& r : readings) {
sum += r.getCelsius();
}
return sum / readings.size();
}
void printAll() const {
for (const auto& r : readings) {
std::cout << r.getLocation() << ": "
<< r.getCelsius() << "°C\n";
}
}
};
Core Objective 3: Data Abstraction
Data abstraction exposes what a class does without revealing how it does it. Clients interact with a clean public interface while implementation details remain hidden behind private access specifiers. This separation allows the implementation to evolve independently—you can replace a linear search with a hash table, or swap a raw array for a vector, without changing any client code. Modern C++ concepts (C++20) formalize abstraction by expressing interface requirements as compile-time constraints, providing clearer error messages when implementations fail to satisfy their contracts.
// Abstract interface hides implementation details
class WordFrequencyCounter {
private:
// Implementation detail: client doesn't need to know this is a map
std::unordered_map<std::string, int> frequencies;
public:
// Clean public interface
void addWord(const std::string& word) {
++frequencies[word];
}
int getFrequency(const std::string& word) const {
auto it = frequencies.find(word);
return (it != frequencies.end()) ? it->second : 0;
}
size_t uniqueWordCount() const {
return frequencies.size();
}
std::string mostFrequent() const {
if (frequencies.empty()) return "";
return std::max_element(
frequencies.begin(), frequencies.end(),
[](const auto& a, const auto& b) {
return a.second < b.second;
}
)->first;
}
};
Core Objective 4: Resource Management with RAII
Resource Acquisition Is Initialization (RAII) represents one of C++'s most powerful and distinctive patterns. By tying resource lifetimes to object lifetimes, RAII guarantees that resources—memory, file handles, network connections, mutexes—are always released when objects go out of scope, even when exceptions occur. Destructors implement the release side of RAII, automatically called when objects are destroyed. Modern C++ extends RAII through smart pointers (std::unique_ptr, std::shared_ptr) and standard library containers that manage their own resources, making manual memory management increasingly rare in well-written modern C++ code.
#include <memory>
#include <fstream>
class FileProcessor {
private:
std::ifstream file;
std::string filename;
mutable int linesProcessed = 0;
public:
explicit FileProcessor(const std::string& path)
: filename(path), file(path) {
if (!file.is_open()) {
throw std::runtime_error(
"Cannot open file: " + path
);
}
}
// Destructor automatically closes file (RAII)
~FileProcessor() {
if (file.is_open()) {
file.close();
}
}
// Delete copy to prevent double-close
FileProcessor(const FileProcessor&) = delete;
FileProcessor& operator=(const FileProcessor&) = delete;
// Allow move for transfer of ownership
FileProcessor(FileProcessor&&) = default;
FileProcessor& operator=(FileProcessor&&) = default;
std::string readLine() {
std::string line;
if (std::getline(file, line)) {
++linesProcessed;
}
return line;
}
int getLinesProcessed() const { return linesProcessed; }
bool isOpen() const { return file.is_open(); }
};
void processFile(const std::string& path) {
try {
FileProcessor processor(path); // File opened here
std::string line;
while (!(line = processor.readLine()).empty()) {
std::cout << line << '\n';
}
} // File automatically closed here, even if exception thrown
catch (const std::runtime_error& e) {
std::cerr << "Error: " << e.what() << '\n';
}
}
Core Objective 5: Inheritance and Polymorphism
Inheritance enables new classes to build upon existing ones, reusing and extending functionality without duplication. Polymorphism allows different classes to respond to the same interface in ways appropriate to their specific types. In C++, virtual functions enable runtime polymorphism while templates enable compile-time polymorphism. Modern C++20 concepts provide a third form—constrained generics—that combine template flexibility with interface clarity. When designing class hierarchies, prefer composition over deep inheritance trees, and use virtual functions only when runtime dispatch is genuinely required.
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual std::string name() const = 0;
void describe() const {
std::cout << name() << ": area=" << area()
<< ", perimeter=" << perimeter() << '\n';
}
};
class Circle : public Shape {
private:
double radius;
static constexpr double PI = 3.14159265358979;
public:
explicit Circle(double r) : radius(r) {}
double area() const override { return PI * radius * radius; }
double perimeter() const override { return 2 * PI * radius; }
std::string name() const override { return "Circle"; }
};
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override { return width * height; }
double perimeter() const override { return 2 * (width + height); }
std::string name() const override { return "Rectangle"; }
};
void describeShapes(const std::vector<std::unique_ptr<Shape>>& shapes) {
for (const auto& shape : shapes) {
shape->describe(); // Polymorphic dispatch
}
}
Core Objective 6: Error Handling and Robustness
Robust C++ classes handle errors gracefully through a combination of validation, exceptions, and design-by-contract principles. Constructors should ensure objects are always created in valid states—throw exceptions rather than allowing partially initialized objects. Setter methods validate inputs before modifying state. Modern C++ provides std::optional for functions that might not return a value, std::expected (C++23) for operations that can fail with error information, and std::variant for type-safe alternatives. These modern tools reduce the need for error-prone null pointer checks and errno-style error codes.
#include <optional>
#include <stdexcept>
class Temperature {
private:
double kelvin; // Store in Kelvin internally
static constexpr double ABSOLUTE_ZERO = 0.0;
public:
explicit Temperature(double k) {
if (k < ABSOLUTE_ZERO) {
throw std::invalid_argument(
"Temperature cannot be below absolute zero"
);
}
kelvin = k;
}
// Factory methods for different units
static Temperature fromCelsius(double c) {
return Temperature(c + 273.15);
}
static Temperature fromFahrenheit(double f) {
return Temperature((f - 32.0) * 5.0 / 9.0 + 273.15);
}
// Conversion functions
double toKelvin() const { return kelvin; }
double toCelsius() const { return kelvin - 273.15; }
double toFahrenheit() const { return (kelvin - 273.15) * 9.0 / 5.0 + 32; }
// C++23: std::expected for operations that can fail
static std::optional<Temperature> tryCreate(double kelvin) {
if (kelvin < ABSOLUTE_ZERO) return std::nullopt;
return Temperature(kelvin);
}
};
Core Objective 7: Move Semantics and Modern Resource Ownership
C++11 introduced move semantics, fundamentally changing how C++ manages resource ownership. Move constructors and move assignment operators transfer ownership of resources without copying them, enabling efficient return of large objects from functions and storage in containers. Understanding the Rule of Zero (prefer classes that need no custom copy/move/destructor), Rule of Three (if you need one of destructor/copy constructor/copy assignment, you likely need all three), and Rule of Five (extend to include move constructor and move assignment) guides proper resource management in modern C++ class design.
class Buffer {
private:
size_t capacity;
std::unique_ptr<char[]> data; // Smart pointer manages memory
public:
explicit Buffer(size_t size)
: capacity(size), data(std::make_unique<char[]>(size)) {
std::fill(data.get(), data.get() + size, '\0');
}
// Rule of Five
~Buffer() = default; // unique_ptr handles cleanup
// Copy: creates independent copy
Buffer(const Buffer& other)
: capacity(other.capacity)
, data(std::make_unique<char[]>(other.capacity)) {
std::copy(other.data.get(), other.data.get() + capacity, data.get());
}
Buffer& operator=(const Buffer& other) {
if (this != &other) {
capacity = other.capacity;
data = std::make_unique<char[]>(capacity);
std::copy(other.data.get(), other.data.get() + capacity, data.get());
}
return *this;
}
// Move: transfers ownership without copying
Buffer(Buffer&& other) noexcept
: capacity(other.capacity)
, data(std::move(other.data)) {
other.capacity = 0;
}
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
capacity = other.capacity;
data = std::move(other.data);
other.capacity = 0;
}
return *this;
}
size_t size() const { return capacity; }
char* get() { return data.get(); }
const char* get() const { return data.get(); }
};
C++20 Concepts and Class Design
C++20 concepts transform how C++ expresses interface requirements. Rather than relying on documentation or runtime errors, concepts express constraints directly in code, checked at compile time. When designing class hierarchies, concepts can specify what operations a type must support, providing clearer alternatives to virtual functions in many contexts. For class templates, concepts replace the cryptic template metaprogramming previously required to express type constraints.
#include <concepts>
// Define interface requirements as concepts
template<typename T>
concept Printable = requires(T t, std::ostream& os) {
{ os << t } -> std::same_as<std::ostream&>;
};
template<typename T>
concept Comparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
{ a == b } -> std::convertible_to<bool>;
};
template<typename T>
concept Sortable = Printable<T> && Comparable<T>;
// Class constrained by concepts
template<Sortable T>
class SortedCollection {
private:
std::vector<T> elements;
public:
void insert(const T& item) {
auto pos = std::lower_bound(elements.begin(), elements.end(), item);
elements.insert(pos, item);
}
void print(std::ostream& os = std::cout) const {
for (const auto& elem : elements) {
os << elem << " ";
}
os << '\n';
}
size_t size() const { return elements.size(); }
};
C++23 Class Enhancements
C++23 introduces several enhancements specifically relevant to class design. Explicit object parameters (deducing this) enable member functions to receive the object as an explicit parameter, simplifying const overloading and enabling recursive lambdas. std::expected provides a modern alternative to exceptions for error handling in constructors and member functions. Enhanced constexpr support allows more class operations at compile time. std::mdspan enables multi-dimensional views into contiguous memory without ownership, enabling efficient class designs for numerical computing.
// C++23: explicit object parameters simplify class design
class Registry {
private:
std::map<std::string, std::string> entries;
public:
void add(const std::string& key, const std::string& value) {
entries[key] = value;
}
// C++23: single template replaces two const/non-const overloads
template<typename Self>
auto&& getEntries(this Self&& self) {
return std::forward<Self>(self).entries;
}
bool contains(const std::string& key) const {
return entries.contains(key);
}
};
What You Will Learn in This Course
This course progresses through increasingly sophisticated class design topics. Early lessons cover the internal mechanics of the class construct—how the compiler handles member functions, access control, and object layout. Subsequent lessons explore the scope resolution operator, showing how to define class members outside the class body and work with nested types. Function overloading demonstrates how C++ selects the appropriate function based on argument types. Static and const member functions reveal how to implement class-level operations and read-only access. The this pointer shows how member functions reference the current object, enabling method chaining and fluent interfaces. Dynamic data structures like stacks and linked lists demonstrate how classes manage heap-allocated resources safely through RAII. Throughout, modern C++23 features enhance each topic with current best practices.
This course assumes familiarity with C++ control statements, basic data types, arrays, and functions. Experience with C or another systems programming language provides helpful context for understanding C++'s low-level control and explicit resource management. Readers with object-oriented experience in Java, Python, or C# will find many familiar concepts expressed differently in C++, with the key distinctions being explicit resource management, value semantics, and the absence of garbage collection. If your background is primarily procedural, plan to spend additional time on the object-oriented design sections before proceeding to implementation details.
Conclusion
Building classes in C++ requires mastering a rich set of interrelated concepts—encapsulation, abstraction, resource management, inheritance, and error handling—while leveraging modern language features that make these patterns safer and more expressive. From the foundational RAII principle that ties resource lifetimes to object lifetimes, to C++23's explicit object parameters that simplify complex member function patterns, C++ class design continues evolving toward greater clarity and safety without sacrificing performance. This course provides the foundation for all advanced C++ development, including template metaprogramming, standard library usage, concurrent programming, and distributed systems design. The investment in understanding C++ class construction pays dividends across every subsequent C++ topic you encounter.

