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\n";
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 << '\n';
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 not merely 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
References
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.
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.
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.