βš™οΈ Unit 09 β€’ C++ Systems Engineering

Classes & Objects: OOP Architectural Foundations

Core principles of Object-Oriented Systems in C++: class blueprints vs instance lifecycles, data encapsulation boundaries (public, private, protected), member function linkage, arrays of objects, class-scoped static members, friend functions and friend classes, and the implicit 'this' pointer mechanics.

πŸ›οΈ Paradigm: Encapsulation, State & Invariants
πŸ”’ Access Control: public, private, protected
⚑ Static Lifetime: Global Class-Scoped State
πŸ‘‰ Implicit Context: ClassName* const this
01

Introduction to OOP: Classes, Objects & Memory Footprint

Object-Oriented Programming (OOP) is an architectural methodology that organizes software design around discrete data entities (Objects) rather than isolated procedural functions and raw memory structures. An object combines both state (attributes stored in data members) and behavior (operations executed via member functions).

Class Blueprint

A class is an abstract, user-defined type specification. It defines the structural layout and behavioral contracts of an entity. Declaring a class allocates zero memory in RAM for dataβ€”it is purely a compile-time schema.

Object Instance

An object is a concrete, physical instance of a class instantiated in runtime memory (either on the call stack or on the dynamic heap). Each object occupies a unique memory address and maintains its own private copy of instance variables.

C++ Process Memory Partitioning for Classes:
Text (Code) Segment (Shared)
Member function executable machine code resides here. Exactly one copy exists for all instances! Objects do not replicate function code.
Stack / Heap: Object A
Address 0x1000: Contains only data members (id, balance). Size = sum of member sizes + alignment padding.
Stack / Heap: Object B
Address 0x2000: Completely independent memory block holding Object B's distinct data members.
πŸ“ The Empty Class Size Axiom: Why sizeof(EmptyClass) == 1?

In C++, an empty class (e.g., class Empty {};) has a sizeof of 1 byte (not 0 bytes). C++ requires that every distinct object in memory possess a unique memory address. If an object had a size of 0, two consecutive objects in an array would share the exact same address, breaking pointer arithmetic and object identity!

02

Access Specifiers & Member Function Definition

Encapsulation is enforced through three access specifiers that dictate where class members can be viewed or mutated:

Access Specifier Accessible Inside Class Accessible in Derived (Sub) Classes Accessible from Outside World
public Yes Yes Yes (Direct access via obj.member)
private Yes No No (Enforces internal data isolation)
protected Yes Yes (Enables inheritance access) No (Hidden from general callers)

Member functions can be defined either inside the class declaration (which automatically hints inline expansion to the compiler) or outside the class declaration using the Scope Resolution Operator (::):

class_anatomy.cpp
#include <iostream>
#include <string>

class BankAccount {
private:
    // Encapsulated data members: Hidden from unauthorized mutation
    std::string accountNumber;
    double balance;

public:
    // Member function defined INSIDE class (implicitly inline)
    void setAccount(const std::string& acc, double initialDeposit) {
        accountNumber = acc;
        balance = initialDeposit;
    }

    // Member function prototypes: Defined OUTSIDE using scope resolution (::)
    void deposit(double amount);
    bool withdraw(double amount);
    void display() const;
};

// Outside definition using Scope Resolution Operator (::)
void BankAccount::deposit(double amount) {
    if (amount > 0) {
        balance += amount;
    }
}

bool BankAccount::withdraw(double amount) {
    if (amount > 0 && balance >= amount) {
        balance -= amount;
        return true;
    }
    return false;
}

void BankAccount::display() const {
    std::cout << "Account: " << accountNumber 
              << " | Balance: $" << balance << "\n";
}

int main() {
    BankAccount account1;
    account1.setAccount("DEV-9082", 1500.0);
    account1.deposit(350.0);
    account1.withdraw(200.0);
    account1.display();

    // account1.balance = 50000; // COMPILE ERROR: 'balance' is private!
    return 0;
}
03

Arrays of Objects & The Implicit this Pointer

Arrays of Objects

An array of objects allocates contiguous memory for multiple instances of a class. The default constructor (or parameter initializer) is invoked sequentially for each element:

BankAccount accounts[3];
for (int i = 0; i < 3; ++i) {
    accounts[i].deposit(100.0 * (i + 1));
}
The this Pointer Architecture

When a non-static member function is called (e.g., obj.deposit(50);), the compiler secretly passes the address of obj as an implicit first parameter:

deposit(&obj, 50); // Compiler transformation

Inside the member function body, this is a constant pointer of type ClassName* const holding the address of the invoking instance.

this_pointer_applications.cpp
#include <iostream>

class Vector2D {
private:
    double x;
    double y;

public:
    // 1. Resolving Name Collisions between parameters and data members
    void setCoordinates(double x, double y) {
        this->x = x; // Disambiguates member x from parameter x
        this->y = y;
    }

    // 2. Method Chaining: Returning *this by reference
    Vector2D& scale(double factor) {
        this->x *= factor;
        this->y *= factor;
        return *this; // Return reference to calling object
    }

    void print() const {
        std::cout << "Vector(" << x << ", " << y << ")\n";
    }
};

int main() {
    Vector2D v;
    v.setCoordinates(2.0, 5.0);

    // Method Chaining via *this reference:
    v.scale(2.0).scale(1.5); 
    v.print(); // Prints: Vector(6, 15)

    return 0;
}
04

Static Data Members & Static Member Functions

Standard data members are replicated for every instantiated object. In contrast, declaring a member static creates a class-level variable that exists independently of any object instance:

Static Data Members
  • Allocated in the Global / Static Data Segment, NOT inside object instances.
  • Exactly one shared copy exists across the entire lifetime of the program.
  • Must be defined and initialized outside the class declaration in file scope.
Static Member Functions
  • Can be invoked directly using class scope without an object: ClassName::function().
  • Do NOT have a this pointer!
  • Can strictly access static data members and other static member functions.
static_members.cpp
#include <iostream>

class ConnectionPool {
private:
    int connectionId;
    
    // Static data member declaration (Class-wide shared counter)
    static int activeConnections;

public:
    ConnectionPool(int id) : connectionId(id) {
        ++activeConnections; // Increments shared static counter
    }

    ~ConnectionPool() {
        --activeConnections;
    }

    // Static member function: Accessible without an object instance
    static int getActiveCount() {
        // connectionId = 0; // COMPILE ERROR: Cannot access non-static member!
        return activeConnections;
    }
};

// MANDATORY: Definition and initialization in file scope:
int ConnectionPool::activeConnections = 0;

int main() {
    std::cout << "Initial count: " << ConnectionPool::getActiveCount() << "\n"; // 0

    ConnectionPool c1(101);
    ConnectionPool c2(102);

    std::cout << "Active after 2 connections: " 
              << ConnectionPool::getActiveCount() << "\n"; // 2

    return 0;
}
05

Friend Functions & Friend Classes

Normally, private and protected members are inaccessible outside the class. However, a class can grant special access privileges by declaring an external function or another class as its friend:

Friend Functions

A non-member function granted full access to private and protected members of a class. Declared inside the class with the friend keyword. Essential for operator overloading (e.g., stream extraction <<).

Friend Classes

When Class A declares friend class B;, all member functions of Class B can freely read and mutate private members of Class A. Commonly used in Iterator and Factory design patterns.

⚠️ The 3 Cardinal Rules of Friendship in C++
  • Friendship is Granted, Not Taken: Class A must explicitly declare Class B as a friend. Class B cannot declare itself a friend of Class A.
  • Friendship is NOT Symmetric: If A is a friend of B, B is NOT automatically a friend of A.
  • Friendship is NOT Transitive: If A is a friend of B, and B is a friend of C, A is NOT a friend of C.
friendship_architecture.cpp
#include <iostream>

class Engine; // Forward declaration

class TelemetryMonitor {
public:
    // Can inspect private members of Engine because it is a friend
    void inspectEngine(const Engine& e);
};

class Engine {
private:
    int rpm{3500};
    double temperature{92.4};

    // 1. Friend Function Declaration
    friend void diagnosticReport(const Engine& e);

    // 2. Friend Class Declaration
    friend class TelemetryMonitor;
};

// Friend function definition (Global function with private access)
void diagnosticReport(const Engine& e) {
    std::cout << "[Diagnostic Report] RPM: " << e.rpm 
              << " | Temp: " << e.temperature << " C\n";
}

void TelemetryMonitor::inspectEngine(const Engine& e) {
    std::cout << "[Telemetry Monitor] Verifying Temp: " << e.temperature << " C\n";
}

int main() {
    Engine v8;
    diagnosticReport(v8);

    TelemetryMonitor monitor;
    monitor.inspectEngine(v8);

    return 0;
}
1
Invocation: account.deposit(500);
The caller invokes deposit() on an instance located at address 0x7ffd00.
2
Linkage & Parameter Passing
Compiler translates call to BankAccount::deposit(&account, 500);. Address 0x7ffd00 is loaded into register RCX / RDI.
3
Execution via this Pointer
The function reads this->balance, modifying the specific memory block at 0x7ffd00 without duplicating code.
06

Viva Voce & Engineering Interview Questions

Q1 β€’ Architecture Why is sizeof(EmptyClass) equal to 1 byte instead of 0 in C++?
To ensure that every distinct object instance possesses a unique memory address. If the size of an empty class were 0, two consecutive objects declared in an array or on the stack would share the identical memory address, making it impossible to differentiate between them or perform valid pointer arithmetic.
Q2 β€’ Language Internals What is the this pointer in C++, and what is its exact type qualification?
The this pointer is an implicit, hidden parameter passed automatically to all non-static member functions, holding the memory address of the object on which the function was invoked. For a class Widget, its type in a standard member function is Widget* const (a constant pointer to mutable data). In a const member function, its type is const Widget* const (a constant pointer to read-only data).
Q3 β€’ Storage Allocation Where are static data members stored in memory, and how do they differ from normal data members?
Normal data members are allocated inside each individual object instance (on the stack or heap) and replicated for every created object. Static data members are allocated once in the program's Global/Static Data Segment (BSS or Data segment) at program startup; they exist independently of any instance, share a single memory location across all objects, and persist until process exit.
Q4 β€’ Member Functions Why cannot a static member function access non-static data members or call non-static functions?
Static member functions belong to the class as a whole and can be invoked without creating an instance (e.g., Math::sqrt()). Because there is no object instance associated with the call, static member functions do not receive a this pointer. Without a this pointer, the compiler cannot determine which specific object instance's data members to read or mutate.
Q5 β€’ OOP Principles What is the difference between private and protected access specifiers?
private members are accessible solely within the scope of the class that declares them (and its friends); derived (child) classes cannot directly access private members of the base class. protected members remain inaccessible to the outside world, but can be directly accessed and inherited by derived child classes.
Q6 β€’ Encapsulation Does declaring a friend function break the fundamental OOP principle of encapsulation?
No; when used judiciously, friendship enhances encapsulation. By granting fine-grained private access to specific helper functions (such as stream operators << and >>) or tightly coupled classes (like an iterator inspecting a container), the designer avoids the worse alternative of exposing broad public getters and setters that allow arbitrary mutation by any outside caller.
Q7 β€’ Code Optimization What is the performance benefit of defining member functions inside the class body?
Functions defined inside the class body are implicitly treated as inline candidates by the compiler. If the function is short (e.g., a simple getter or setter), the compiler replaces the function call directly with the underlying machine instructions, eliminating the CPU overhead of pushing stack frames, jumping to addresses, and returning.
Q8 β€’ Modern Idioms How does returning *this by reference enable Method Chaining (Fluent Interfaces)?
Returning ClassName& with the statement return *this; returns an lvalue reference to the invoking object itself. This enables callers to immediately chain consecutive member function calls on the same instance in a single statement (e.g., builder.setWidth(10).setHeight(20).render();) without incurring object copy penalties.