Object-Oriented Programming with C++
Detailed C++ notes on classes, object lifetime, references, resource management, overloading, inheritance, polymorphism, streams and the standard library.
These C++ notes are the object-oriented programming and resource-management part of the C/C++ material I kept during the same course period. Later revisions add concepts that became established after C++11, but the original progression through classes, object lifetime, inheritance, polymorphism and the standard library remains intact.
Unit 1: Moving from C to C++
Relationship between C and C++
C++ grew historically from C and retains many low-level facilities, but it is a separate language with its own object model, type rules, standard library and design idioms. Valid C is not automatically good C++, and some C constructs do not have identical semantics in C++.
A minimal program is:
#include <iostream>
int main()
{
std::cout << "hello\
";
return 0;
}The standard library is organized in namespaces, primarily std. A global using namespace std; may shorten teaching examples, but production headers should avoid exporting such broad namespace directives to their users.
Initialization
C++ supports several initialization forms:
int a = 10;
int b(10);
int c{10};Brace initialization is useful because it rejects many narrowing conversions:
int x{3.5}; // ill-formedThe exact choice should reflect the desired constructor and conversion semantics rather than a stylistic rule applied blindly.
auto
auto asks the compiler to deduce a variable's type from its initializer:
auto count = 10;
auto ratio = 0.5;It reduces redundant type spelling, especially with iterators and template-heavy types. It does not make C++ dynamically typed; the deduced type is still a compile-time type.
const and constexpr
const prevents modification through that name or access path where the type system applies it. constexpr expresses that an entity can participate in constant evaluation when its requirements are satisfied.
constexpr int buffer_size = 4096;The two concepts overlap but are not identical: const is primarily about mutability, while constexpr is about constant-expression semantics.
nullptr
C++11 introduced nullptr as a dedicated null pointer literal:
int *p = nullptr;It avoids overload ambiguities caused by integer literal 0 or macro-style NULL values.
Streams, strings and vectors
C++ standard I/O uses streams:
std::cout << value << '\
';
std::cin >> value;std::string owns a dynamically sized character sequence and is generally preferable to manual C-string management for ordinary text processing:
std::string name = "Ali";std::vector<T> is the standard contiguous dynamic sequence container:
std::vector<int> values{1, 2, 3};
values.push_back(4);It manages storage automatically and keeps ownership with the container.
Unit 2: Object-Oriented Programming
Objects and classes
A class defines state and operations that maintain the invariants of objects of that type:
class Counter {
private:
int value_;
public:
explicit Counter(int value) : value_(value) {}
void increment() { ++value_; }
int value() const { return value_; }
};Object-oriented design is more than a mechanism for grouping fields and functions. The useful abstraction is one in which operations preserve a coherent model and hide representation details that callers should not depend on.
Abstraction and encapsulation
Abstraction exposes the properties relevant to users of a type. Encapsulation controls access to representation and implementation detail. C++ provides public, private and protected access control.
A struct and a class have almost the same language capabilities; their main default differences are access and inheritance visibility. struct defaults to public members, while class defaults to private members.
Constructors
A constructor establishes a valid object state:
class Point {
int x_;
int y_;
public:
Point(int x, int y) : x_(x), y_(y) {}
};Members are initialized in declaration order, not in the textual order of the initializer list. This matters when one member depends on another.
A default constructor can be compiler-generated or explicitly requested:
Point() = default;A single-argument constructor can unintentionally define an implicit conversion. explicit suppresses such conversion where it is not part of the intended interface:
explicit Size(std::size_t value);Destructors and RAII
A destructor runs when an object's lifetime ends:
~Resource();C++'s central resource-management idiom is RAII: acquire a resource in object construction and release it in destruction. This associates file handles, locks, memory and other resources with lexical object lifetime, making cleanup work across normal return paths and exception unwinding.
Copying and assignment
Copy construction creates a new object from an existing one. Copy assignment replaces the state of an already existing object. For resource-owning types, these operations must have well-defined ownership semantics.
If a type can be represented safely by standard value members such as std::string, std::vector and smart pointers, the Rule of Zero is usually preferable: let those members implement ownership so the user-defined class does not need custom copy/move/destructor logic.
Move semantics
C++11 move semantics allow resources to be transferred from an expiring object rather than copied:
T(T&& other);
T& operator=(T&& other);A moved-from object must remain valid according to the guarantees of its type, although its value may be unspecified. Move operations are performance and ownership tools, not permission to access destroyed state.
Unit 3: References, Object Pointers and Memory Management
Passing by value and by reference
Passing a small value type by value is often simplest:
int square(int x);A read-only large object can be passed by const reference:
void process(const std::string& text);A non-const reference can make mutation explicit:
void normalize(Vector& v);For APIs, the parameter form should communicate ownership and mutation expectations.
this
Inside a non-static member function, this is a pointer to the current object. It is useful when distinguishing members from parameters or returning the current object:
return *this;new and delete
Raw dynamic allocation exists:
T* p = new T;
delete p;
T* a = new T[n];
delete[] a;but direct ownership with raw new/delete is usually avoidable in modern C++. Containers and RAII wrappers make ownership clearer and exception-safe.
Smart pointers
std::unique_ptr<T> represents exclusive ownership:
auto p = std::make_unique<Object>();It is movable but not copyable.
std::shared_ptr<T> represents shared ownership through reference counting. It should be used only when ownership is genuinely shared; otherwise it obscures lifetime and adds synchronization/reference-counting overhead. Cycles involving shared_ptr require std::weak_ptr or a different ownership design.
Arrays of objects
Prefer containers when the number of objects is dynamic:
std::vector<Object> objects;This preserves deterministic destruction and avoids manual array allocation.
Unit 4: Function and Operator Overloading
Function overloading
Functions can share a name when their parameter lists distinguish them:
void print(int value);
void print(double value);Overload resolution uses compile-time type information and conversion ranking. Return type alone cannot distinguish overloads.
Default arguments
void connect(int timeout_ms = 1000);Default arguments are substituted at the call site according to declarations visible there. Changing a default can therefore affect callers differently from changing an implementation body.
Constructor overloading and delegating constructors
A class can provide multiple construction forms. C++11 also allows one constructor to delegate to another:
Widget() : Widget(0) {}Delegation reduces duplicated initialization logic.
Operator overloading
User-defined types can overload many operators:
Vector operator+(const Vector& a, const Vector& b);An overloaded operator should preserve the ordinary semantic expectation of that operator. Surprising side effects make generic code difficult to reason about.
Comparison operators should define a coherent relation. C++20's three-way comparison can synthesize related comparisons for appropriate types, but older explicit operators remain valid and common.
Prefix and postfix increment are distinct signatures; postfix conventionally takes an unused int parameter:
Counter& operator++();
Counter operator++(int);Unit 5: Inheritance and Polymorphism
Inheritance
Inheritance defines a derived class in terms of a base-class relationship:
class Derived : public Base {
...
};Public inheritance should model an substitutable "is-a" relation, not simply code reuse. Composition is often more appropriate when one object merely contains or uses another.
Protected and private inheritance change accessibility and conversion relationships and are less commonly appropriate for domain modeling.
Construction and destruction order
Base subobjects are constructed before derived-class members and the derived constructor body. Destruction occurs in the reverse order. Virtual bases have additional ordering rules defined by the language.
Multiple inheritance and the diamond
C++ supports multiple direct base classes. A diamond can introduce duplicated base subobjects:
A
/ \
B C
\ /
DVirtual inheritance can make B and C share one A subobject when that is the intended model. Multiple inheritance is powerful but increases layout and lifecycle complexity.
Virtual functions and dynamic dispatch
A virtual function enables runtime dispatch through a base reference or pointer:
class Base {
public:
virtual void run();
};A derived override should use override so mismatches are diagnosed:
void run() override;A polymorphic base intended for deletion through a base pointer normally needs a virtual destructor:
virtual ~Base() = default;A pure virtual function defines an abstract interface:
virtual void run() = 0;Dynamic polymorphism uses runtime dispatch. Templates can provide static polymorphism, where behavior is selected at compile time without a virtual call. The two mechanisms solve different design problems.
Unit 6: C++ I/O and Files
Streams
C++ models I/O as streams. std::cout, std::cerr and std::cin are standard stream objects. Stream state records whether formatted extraction or I/O has failed.
int value;
if (std::cin >> value) {
...
}Checking stream state is preferable to assuming input succeeded.
Formatting
The <iomanip> facilities can control width, precision, base and other formatting properties. Some manipulators modify persistent stream state, so reusable code should not assume formatting is unchanged by previous operations.
File streams
std::ofstream out("data.txt");
std::ifstream in("data.txt");The stream object closes its file during destruction, demonstrating RAII. Open state and I/O errors should still be tested.
std::getline reads a complete line into std::string:
std::string line;
while (std::getline(in, line)) {
...
}Binary and random access
Binary mode is requested with std::ios::binary. read and write transfer byte sequences. As with C, writing the raw memory image of an arbitrary object does not define a portable serialization format.
seekg/seekp and tellg/tellp support positioning where the stream permits it.
Unit 7: The C++ Standard Library
STL model
The Standard Template Library style separates:
- containers that own or organize data,
- iterators that identify positions/ranges,
- algorithms that operate on ranges,
- function objects and callable values that customize operations.
This separation allows one algorithm to work with many container types when their iterator requirements are satisfied.
Sequence containers
std::vector provides contiguous dynamic storage and amortized constant-time insertion at the end. Reallocation can invalidate pointers, references and iterators into the old storage.
std::array<T, N> is a fixed-size array wrapper with ordinary container semantics.
std::list is a doubly linked list. It provides stable node addresses and constant-time insertion/erasure at known positions but has poor locality and no constant-time random access.
std::deque supports efficient insertion/removal at both ends and random access, without requiring one contiguous allocation.
Container adaptors
std::stack, std::queue and std::priority_queue expose restricted interfaces over underlying containers. priority_queue provides access to the highest-priority element according to its comparator rather than sorted iteration over all elements.
Associative and unordered containers
Ordered associative containers such as std::map and std::set maintain key order and usually provide logarithmic search/insert/erase complexity.
Unordered containers such as std::unordered_map use hashing and provide average constant-time lookup under normal hash-distribution assumptions, with different worst-case behavior and iteration semantics.
Iterators
Iterators generalize traversal. Categories encode supported operations, from single-pass input iteration through random-access and contiguous iteration. An algorithm's complexity and valid operations depend on the iterator category it requires.
Algorithms
Common algorithms include:
find
sort
count
search
transformFor example:
std::sort(values.begin(), values.end());requires a random-access range. Algorithms usually do not own the elements; they operate through iterators.
Lambdas
A lambda defines an unnamed callable:
auto pred = [](int x) { return x > 0; };Its capture list determines which surrounding objects or values become part of the closure. Capturing by reference requires lifetime discipline.
Ranges
C++20 ranges add range-aware algorithms, views and pipelines. They reduce explicit iterator pairing and support lazy composition, but views frequently reference other objects and therefore introduce lifetime considerations of their own.
Unit 8: Modern Resource and Type Safety
Raw pointers do not imply ownership
A raw pointer is best treated as an address/non-owning access mechanism unless an API explicitly documents ownership transfer. Ownership should preferably be visible in values, containers or smart-pointer types.
std::span
std::span<T> represents a non-owning contiguous sequence with a pointer and extent:
void process(std::span<const int> values);It makes length part of the view and is safer than a naked pointer without a corresponding count. It does not extend the lifetime of the referenced storage.
std::string_view
std::string_view is a non-owning view of character data. It avoids allocation and copying for read-only string slices, but it can dangle if the original string/storage is destroyed or reallocated.
Exceptions
Exceptions transfer control from a failure point to a matching handler while unwinding automatic objects:
try {
...
} catch (const std::exception& e) {
...
}RAII is what makes this manageable: resources stored in automatic objects are released as destructors run during unwinding.
noexcept
noexcept states that a function is not expected to let exceptions escape. Violating an unconditional noexcept results in termination. It is therefore a semantic contract, not a performance decoration.
Move operations that are noexcept can also enable stronger behavior in standard containers during reallocation.
Undefined behavior
C++ contains operations whose behavior is not defined by the language, including many out-of-bounds accesses, use-after-lifetime errors, invalid shifts, certain signed overflows and data races. Optimization assumes that a well-formed executing program does not perform undefined behavior, so the result can be much more severe than a local wrong value.
Warnings and sanitizers
Compiler warnings should be treated as engineering feedback, not cosmetic output. High warning levels, static analysis and sanitizers such as AddressSanitizer and UndefinedBehaviorSanitizer can expose defects that ordinary tests may not make visible.
Safe default assumptions
Useful defaults for both C and C++ include:
- initialize objects before reading them,
- make ownership explicit,
- preserve bounds with data,
- avoid unchecked arithmetic when sizes come from external input,
- do not rely on unspecified object representation,
- prefer standard-library resource owners to manual allocation,
- keep interfaces const-correct,
- treat compiler diagnostics and runtime instrumentation as part of verification.
Unit 9: Templates, Concepts, and Generic Programming
Generic programming in C++ is more than making one function accept several types. The goal is to preserve type information at compile time, express reusable algorithms, and reject invalid type combinations as early as possible.
Function and class templates
A function template lets the compiler determine a parameter type during compilation:
template <class T>
T maxValue(const T& a, const T& b) {
return b < a ? a : b;
}The compiler instantiates concrete forms for the types that are required. This can provide zero-overhead abstraction because runtime type dispatch is unnecessary, but many instantiations can increase binary size and compile time.
Class templates apply the same model to data structures. std::vector<T>, std::optional<T>, and smart pointers are common examples.
Constraints and concept
Syntactic validity alone is not enough; an algorithm usually expects semantic operations from its type parameters. Since C++20, concept and requires make template constraints explicit:
template <class T>
concept Addable = requires(T a, T b) {
a + b;
};
template <Addable T>
T add(const T& a, const T& b) {
return a + b;
}A concept is not a runtime interface or an inheritance hierarchy. It is a compile-time requirement and participates in overload resolution.
constexpr, consteval, and compile-time computation
constexpr allows an expression or function to be evaluated at compile time when used in a constant-expression context; it does not mean every call must execute during compilation. A consteval function requires compile-time evaluation.
Compile-time computation can remove runtime work, but it can also increase compilation cost, diagnostic complexity, and code size. "Compile time" is therefore not automatically synonymous with "faster overall."
Forwarding references and std::forward
In a deduced template context, a parameter of the form T&& can be a forwarding reference. std::forward preserves whether the original argument was an lvalue or rvalue when passing it to another function.
This is useful in generic factories and wrappers because it can avoid unnecessary copies. It also makes lifetime and overload analysis more subtle. Ownership transfer and preservation of value category are separate concepts.
Template visibility and binary boundaries
A template definition normally must be visible where instantiation occurs, which is why templates are commonly defined in headers. Explicit instantiation can move code generation for selected types into a separate translation unit.
Generic interfaces also have different ABI consequences from ordinary virtual interfaces. Public template types, compile dependencies, and binary compatibility must be considered together when a library boundary is designed.
Standard status and draft features
As of September 2026, the published C++ International Standard is ISO/IEC 14882:2024. The next revision is in ISO's Draft International Standard (DIS) stage. A feature visible in the draft should therefore not be described as part of the published standard until that status is verified.
Unit 10: Concurrency and the C++ Memory Model
C++ concurrency is not only a thread-creation API. Correctness depends on object lifetime, data races, visibility, synchronization, and shutdown behavior.
Concurrency versus parallelism
Concurrency means multiple activities can make progress over time. Parallelism means they execute simultaneously on multiple execution resources. A single-core system can run concurrent software without true parallel execution.
The distinction matters for performance. More threads do not automatically increase throughput; contention, cache effects, and scheduler overhead can dominate.
Data races and undefined behavior
If two threads access the same memory location concurrently, at least one access writes, and there is no suitable synchronization between them, the program has a data race. In the C++ memory model, a data race generally results in undefined behavior.
volatile does not solve this problem. It can constrain certain compiler optimizations around observable accesses, but it does not provide inter-thread atomicity or establish a happens-before relation.
Mutexes and RAII
A mutex protects invariants over shared state. Binding lock ownership to object lifetime prevents forgotten unlock operations during exceptions or early returns:
std::mutex m;
void update() {
std::lock_guard<std::mutex> lock(m);
// protected state
}When multiple mutexes are acquired together, lock ordering and deadlock risk must be considered. std::scoped_lock is designed to manage several mutexes as one scope-bound operation.
Condition variables
A condition variable allows a thread to wait for a state transition. Because wakeups may be spurious and the state can change before the waiter resumes, the predicate must be checked again:
cv.wait(lock, [&] { return ready; });A notification is not a durable message by itself. The shared state and the condition variable form one protocol.
Atomics and memory ordering
std::atomic<T> provides race-free atomic operations for supported types. The default ordering is sequential consistency (memory_order_seq_cst), which provides the strongest and usually simplest mental model.
Acquire/release and relaxed orderings offer weaker guarantees and can reduce synchronization cost in carefully designed low-level code. Weakening ordering without a demonstrated need can turn a performance experiment into a correctness bug.
The happens-before relation
It is not enough for one thread to have physically executed a write "earlier." Reliable visibility requires a happens-before relation defined by the language.
Mutex unlock/lock pairs, suitable release/acquire atomic operations, and thread-lifecycle operations can establish that relation. Hardware cache coherence alone does not prove that a C++ program is data-race free.
Cancellation, shutdown, and lifetime
Long-lived concurrency requires a shutdown protocol as well as startup logic. std::jthread provides scope-bound joining and integrates with std::stop_token for cooperative cancellation.
A reliable shutdown design answers: who requests termination, how blocked threads wake, what happens to queued work, in what order shared objects are destroyed, and how joining remains guaranteed after errors.
Performance: contention, false sharing, and determinism
Contention on one mutex limits scalability. Independent atomic variables can still interact when they share a cache line, producing false sharing and unnecessary cache-coherence traffic.
Performance analysis should therefore include queue length, lock wait time, cache misses, context switches, and tail latency in addition to CPU utilization. In low-latency systems, p95/p99 and worst-case behavior can matter as much as average throughput.
Fundamental Differences Between C and C++
C and C++ share syntax and low-level capabilities, but they encourage different abstractions. C commonly models state through structures, functions and explicit ownership conventions. C++ adds deterministic object lifetime, constructors/destructors, references, overloading, templates, exceptions, generic containers and compile-time abstraction mechanisms.
A C program converted mechanically to C++ does not automatically become idiomatic C++. Conversely, C++ abstractions should not hide performance or ownership properties that are important to a systems-level design. The useful common principle is to make lifetime, representation, control flow and error behavior explicit enough that the code remains predictable.
Reference types
A reference is an alias bound to an object:
int value = 10;
int& ref = value;References are useful for mandatory non-owning access, while pointers naturally express nullable or reseatable relationships. The semantic distinction is more important than the syntax.
Designing Value Semantics, RAII, and Ownership Together
Reliable modern C++ design is not centered solely on classes and inheritance. It makes object lifetime and ownership visible through types. Resource Acquisition Is Initialization (RAII) binds resource cleanup to object lifetime so that normal returns and exception unwinding follow the same release rule.
Rule of Zero and Rule of Five
When a class stores resources through RAII types such as std::vector, std::string, and std::unique_ptr, it often needs no hand-written destructor, copy/move constructor, or assignment operators. This is the Rule of Zero.
If a class must directly manage a special resource, copy and move behavior must be designed as one contract. Is copying deep, prohibited, or reference-like? What state remains after a move? These decisions are connected rather than independent boilerplate.
unique_ptr, shared_ptr, and raw pointers
std::unique_ptr<T> expresses unique ownership; std::shared_ptr<T> expresses reference-counted shared ownership. shared_ptr is not a universal safe default: it adds reference-counting/ownership-block costs and can create ownership cycles. std::weak_ptr can represent a non-owning relationship into a shared ownership graph.
Raw pointers remain valid tools in modern C++, especially for nullable non-owning observation. The core problem is not the syntax T*; it is an unclear ownership contract.
Copy and move semantics
A move usually transfers ownership of an expensive resource. It does not inherently perform some magical byte-level optimization. A moved-from object remains valid but its value is constrained by the relevant type contract. std::move itself does not move anything; it changes the value category so that a move overload can be selected.
Containers can use information such as a noexcept move constructor when preserving strong exception guarantees during reallocation. noexcept can therefore affect generic-container behavior rather than being a cosmetic annotation.
Polymorphism and destruction
If an object can be deleted through a base-class pointer, the base needs an appropriate virtual destructor. Otherwise destruction through the base can produce undefined behavior. Conversely, adding a virtual destructor to every class by habit changes semantics and representation without justification; the requirement comes from the polymorphic ownership model.
Composition and inheritance
Public inheritance is stronger than a code-reuse mechanism: it expresses an is-a/substitutability relationship. Choosing inheritance only to reuse implementation can produce fragile hierarchies. Composition often states ownership and collaboration more directly.
Generic programming and concepts
Templates allow algorithms and data structures to be parameterized by types and values. C++20 concepts make template requirements explicit and improve diagnostics by constraining candidate types closer to the interface contract.
This illustrates a central C++ design goal: build higher-level abstractions without hiding their essential lifetime and performance costs.
Ownership, RAII, and exception safety
Modern C++ design is fundamentally about ownership. RAII ties resources such as memory, files, locks, and sockets to object lifetime so cleanup follows the same rule on normal and exceptional exits.
std::unique_ptr expresses single ownership. std::shared_ptr expresses shared ownership, but should not be the default because ownership cycles and unclear lifetime can result. Non-owning observations should remain distinguishable from owners.
Exception-safe operations should at least preserve valid invariants when they fail. Stronger designs leave observable state unchanged on failure. Move semantics, swap-based updates, and narrowly scoped classes make these guarantees easier to implement.
Verifiable ownership in C++ design
A readable interface is not sufficient when ownership, lifetime, and exception behavior are unclear. If there is one owner, std::unique_ptr may express that directly; value semantics may remove ownership machinery entirely; std::shared_ptr should reflect genuine shared lifetime rather than convenience.
Verification can combine unit tests, sanitizers, static analysis, different optimization levels, and tests that exercise copy/move paths. Benchmarks should record compiler version, flags, and data size.
For RAII and exception safety, the central question is which invariants remain valid after failure. Once that contract is explicit, design quality can be judged by observable behavior rather than style alone.
Boundaries among Inheritance, Polymorphism, and Object Lifetime
Inheritance in C++ is not merely a mechanism for reducing duplicated code; when used well, it represents an “is-a” relationship. A class should not derive from another class only because they share a few fields. If their lifecycles or reasons for change differ, composition often gives a clearer design.
Runtime polymorphism requires virtual dispatch through a base-class interface. Copying a derived object by value into a base-class object, however, causes object slicing and discards the derived portion. Polymorphic objects are therefore commonly used through references or smart pointers that express an appropriate ownership model.
A polymorphic base class that may be used to delete derived objects needs a virtual destructor:
struct Base {
virtual ~Base() = default;
virtual void run() = 0;
};The purpose is not syntactic style but correct destruction of resources owned by the complete derived object. RAII generalizes this lifetime discipline: resource acquisition is tied to construction and release to destruction, so normal return, exceptions, and early exits follow the same cleanup rule.
Copy and move semantics must also be considered together with ownership. If a class does not directly manage a resource, the Rule of Zero is often the safest design. If resource management truly belongs to the class, copy, move, and destruction behaviour must form a coherent policy.
C++ object-oriented reasoning is therefore clearer when three questions are kept together rather than memorizing isolated keywords: What is the dynamic type of the object, who owns its resources, and when does its lifetime end?
References
- ISO/IEC. ISO/IEC 14882:2014 Information Technology — Programming Languages — C++. International Organization for Standardization, 2014. https://www.iso.org/standard/64029.html
- ISO/IEC. ISO/IEC 14882:2024 Programming Languages — C++. International Organization for Standardization, 2024. https://www.iso.org/standard/83626.html
- ISO/IEC. ISO/IEC DIS 14882 Programming Languages — C++. Draft International Standard, 2026. https://www.iso.org/standard/91179.html
- Meyers, S. Effective Modern C++. O'Reilly Media, 2014.
- Stroustrup, B. A Tour of C++. Addison-Wesley, 2013.
- Stroustrup, B. The C++ Programming Language. Addison-Wesley, 2013.