⚙️ Unit 07 • C++ Systems Engineering

Pointers & Dynamic Memory Architecture

Mastering direct address manipulation, memory mechanics, and heap management in C++: address-of (&), dereference (*), pointer arithmetic, array indexing equivalences, pointers to functions, multi-level indirection (**), dynamic allocation (new/delete), heap fragmentation, and leak mitigation.

📍 Addressing: Direct 64-bit Virtual RAM Addressing
➕ Arithmetic: Scaled by sizeof(T) Bytes
🧱 Heap Management: new / delete & Free Store
🛡️ Systems Safety: Dangling Pointers & RAII Paradigm
01

Introduction to Pointers: Address-of (&) & Dereference (*)

In modern systems architectures, random-access memory (RAM) is treated as a vast, linear array of byte-addressable cells, each marked by a unique hexadecimal address. While standard variables bind human-readable identifiers to concrete data values, a pointer is a specialized variable whose stored value is itself the memory address of another variable or object.

Address-of Operator: &

The unary ampersand (&) extracts the physical or virtual address where an lvalue resides in memory. For example, if variable count occupies memory at location 0x7ffd98, evaluating &count produces that hexadecimal memory coordinate.

Dereference (Indirection) Operator: *

The unary asterisk (*) accesses the value stored at the address currently held by a pointer. Dereferencing *ptr instructs the CPU to load the memory address held in ptr, follow it to the target location, and read or mutate the data stored there.

Physical Memory Cell Visualization (64-bit Architecture):
0x7ffd50
int value = 42 [Occupies 4 Bytes]
0x7ffd58
int* ptr = 0x7ffd50 [Holds address of `value` → points to 0x7ffd50]
pointer_basics.cpp
#include <iostream>

int main() {
    int target = 100;
    
    // Declaration & Initialization:
    // 'int*' designates a pointer to an integer
    int* ptr = &target; 

    std::cout << "Value of target: " << target << "\n";        // 100
    std::cout << "Address of target (&target): " << &target << "\n"; // e.g. 0x7ffd50
    std::cout << "Address stored in ptr: " << ptr << "\n";    // 0x7ffd50
    std::cout << "Dereferencing ptr (*ptr): " << *ptr << "\n"; // 100

    // Modifying the value via the pointer
    *ptr = 250;
    std::cout << "Updated target value: " << target << "\n";  // 250

    return 0;
}
⚠️ The Uninitialized Pointer Hazard (Wild Pointers)

Declaring a pointer without an initial address (e.g., int* p;) leaves it populated with arbitrary garbage bits from the stack. Dereferencing or writing to a wild pointer triggers Undefined Behavior, memory corruption, or an operating system segmentation fault (SIGSEGV). In modern C++, always initialize unassigned pointers to nullptr: int* p = nullptr;.

02

Pointer Arithmetic: Scaled Stride Mechanics

Pointer arithmetic differs fundamentally from standard integer arithmetic. Adding 1 to a pointer does not increment its underlying numerical address by 1 single byte; instead, it advances the pointer by 1 × sizeof(DataType) bytes. The compiler automatically scales arithmetic based on the size of the referenced type:

Pointer Type Initial Address Operation Byte Stride Formula Resulting Address
char* cp 0x1000 cp + 1 + (1 × 1 byte) 0x1001
short* sp 0x1000 sp + 1 + (1 × 2 bytes) 0x1002
int* ip 0x1000 ip + 1 + (1 × 4 bytes) 0x1004
double* dp 0x1000 dp + 1 + (1 × 8 bytes) 0x1008
int* ip 0x1000 ip + 3 + (3 × 4 bytes = +12) 0x100C
📐 Pointer Subtraction (Distance Between Elements)

Subtracting two pointers pointing to elements of the same array (e.g., ptr2 - ptr1) yields the number of elements separating them (of type std::ptrdiff_t), calculated as: (Address2 - Address1) / sizeof(T).

03

Pointers & Arrays: Identity Equivalence & C-Strings

In C++, an array name acts as an immutable (constant) pointer to its first element. When an array name is evaluated in an expression, it undergoes Array-to-Pointer Decay:

The Fundamental Array-Pointer Axiom

arr[i] ≡ *(arr + i) ≡ *(i + arr) ≡ i[arr]

The subscript syntax arr[i] is merely syntactic sugar for pointer arithmetic: add i strides to the base address and dereference the computed coordinate.

pointers_arrays_strings.cpp
#include <iostream>

int main() {
    int nums[4] = {10, 20, 30, 40};
    int* p = nums; // Points to nums[0]

    // 1. Array Traversal via Pointer Arithmetic
    for (int i = 0; i < 4; ++i) {
        std::cout << "Element at index " << i << ": "
                  << *(p + i) << " (Address: " << (p + i) << ")\n";
    }

    // 2. Incrementing the Pointer Directly
    int* walker = nums;
    std::cout << "First: " << *walker++ << "\n";  // Prints 10, then advances walker
    std::cout << "Second: " << *walker << "\n";   // Prints 20

    // 3. Pointers and C-Strings
    // String literal resides in Read-Only Data Segment (.rodata)
    const char* str = "DevAscent"; 
    
    // Traversing string via pointer until null-terminator '\0'
    const char* charPtr = str;
    std::cout << "String characters: ";
    while (*charPtr != '\0') {
        std::cout << *charPtr << " ";
        charPtr++;
    }
    std::cout << "\n";

    return 0;
}
1
Base Address: charPtr = 0x5000
Points to 'D'. *charPtr != '\0' is TRUE → Print 'D' → charPtr++
2
Stride Forward: charPtr = 0x5001
Points to 'e'. *charPtr != '\0' is TRUE → Print 'e' → charPtr++
3
Null-Terminator Encountered: charPtr = 0x5009
Points to '\0' (Byte value 0). Loop condition terminates immediately.
04

Pointers & Functions & Multi-Level Indirection (**ptr)

Passing pointers into functions enables Pass-by-Address, allowing a called function to directly mutate variables belonging to the caller's stack frame without incurring data copying costs.

Pass-by-Address

The function receives a copy of the memory address. Dereferencing allows direct mutation of the original variable in the caller frame:

void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}
Pointer-to-Pointer (**ptr)

A variable holding the memory address of another pointer variable. Double pointers are essential when dynamically allocating 2D arrays or modifying the address a caller's pointer references:

int val = 99;
int* p = &val;
int** pp = &p; // Address of pointer p
Two-Level Indirection Resolution Sequence:
0x1000
int val = 99
0x2000
int* p = 0x1000 [Points to `val` at 0x1000]
0x3000
int** pp = 0x2000 [Points to `p` at 0x2000 | **pp yields 99]
05

Dynamic Memory Allocation: The new & delete Operators

C++ partitions process memory into two primary runtime zones: the fast, compiler-managed Call Stack (automatic storage duration) and the flexible, application-managed Free Store (Heap) (dynamic storage duration):

Attribute Stack Memory (Automatic) Heap Memory (Free Store / Dynamic)
Lifetime Scope-bound; automatically destroyed when leaving block Manual; persists until explicitly released with delete
Allocation Speed Near-instantaneous; single CPU instruction (moves stack pointer) Slower; OS heap manager scans free lists / memory buckets
Size Flexibility Fixed compile-time size; risk of Stack Overflow Arbitrary runtime sizing, bounded only by system virtual RAM
Operator Local variable declaration: int x; Dynamic operators: new and delete
dynamic_allocation.cpp
#include <iostream>

int main() {
    // 1. Single Object Allocation on the Heap
    int* ptr = new int(42); // Allocates 4 bytes on heap initialized to 42
    std::cout << "Heap integer value: " << *ptr << "\n";
    
    // Deallocate single object
    delete ptr;
    ptr = nullptr; // Neutralize dangling pointer!

    // 2. Dynamic 1D Array Allocation at Runtime
    size_t size;
    std::cout << "Enter dynamic array size: ";
    size = 5; // Demonstration constant

    int* arr = new int[size]{1, 2, 3, 4, 5}; // Initialized dynamic array

    for (size_t i = 0; i < size; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << "\n";

    // Deallocate array: MUST use array delete []!
    delete[] arr; 
    arr = nullptr;

    return 0;
}
🚨 The delete vs delete[] Mismatch Rule

Allocating with new T requires delete ptr.
Allocating with new T[N] strictly requires delete[] ptr.
Using plain delete on an array causes Undefined Behavior: in arrays of non-primitive objects, only the first element's destructor will be invoked, leaving the remaining $N-1$ objects undestroyed and corrupting the heap allocator's internal boundary tags!

06

Memory Leaks, Dangling Pointers & Safety Practices

Manual memory management places total responsibility upon the systems engineer. Violating heap etiquette produces two primary memory flaws:

Memory Leak

Occurs when heap memory is allocated with new, but all pointer references to that address are lost or reassigned before delete is called. The allocated RAM remains reserved by the OS until process termination, causing progressive memory exhaustion.

Dangling Pointer

Occurs when a pointer continues to store the memory address of an object that has already been deallocated by delete. Subsequent reads or writes access invalid memory, resulting in data corruption or crashes.

1
Allocation: int* p = new int(100);
Heap allocates 4 bytes at address 0x8000. Stack pointer p holds 0x8000.
2
Reassignment: p = new int(200); (FATAL LEAK!)
p is overwritten with 0x8050. The address 0x8000 is lost permanently with no way to deallocate it!
3
Remediation: Always Delete Prior to Reassignment
delete p; p = nullptr; ensures the free store recovers memory before acquiring new blocks.
07

Viva Voce & Engineering Interview Questions

Q1 • Fundamentals What is the fundamental difference between NULL and nullptr in C++?
NULL is an inherited C preprocessor macro expanding to the integer literal 0 (or ((void*)0)). Because it is an integral type, it causes dangerous ambiguity in function overloading (e.g., calling f(int) instead of f(void*)). C++11 introduced nullptr, which has a distinct type std::nullptr_t that implicitly converts to any raw pointer type without ever converting to an integer.
Q2 • Pointer Constness Distinguish between const int* ptr and int* const ptr.
const int* ptr (pointer to constant data): The memory address held by ptr can change, but the data being pointed to is read-only (*ptr = 10; is illegal).
int* const ptr (constant pointer to mutable data): The pointer address is fixed and immutable after initialization, but the value stored at that address can be modified (*ptr = 10; is allowed, but ptr = &other; is illegal).
Q3 • Arithmetic Given int* p = (int*)0x2000;, what address does p + 4 evaluate to on a 64-bit system?
Pointer arithmetic scales by the size of the target data type. In C++, sizeof(int) == 4 bytes. Therefore, p + 4 evaluates to:
0x2000 + (4 × 4 bytes) = 0x2000 + 16 (0x10 hex) = 0x2010.
Q4 • Memory Systems What are the internal differences between C's malloc/free and C++'s new/delete?
malloc() and free() are standard C library functions that solely allocate raw byte blocks without type awareness or object construction. C++'s new and delete are built-in language operators: new computes type sizes automatically, allocates memory, and invokes the class constructor; delete invokes the class destructor before releasing memory back to the OS free store.
Q5 • Safety What is a Dangling Pointer, and how can it be neutralized?
A dangling pointer is a pointer that points to a memory location that has been deallocated (freed). Accessing it triggers undefined behavior. It can be neutralized by immediately assigning the pointer to nullptr right after calling delete (e.g., delete ptr; ptr = nullptr;).
Q6 • Architecture Why is dereferencing a void* pointer disallowed without explicit casting?
A void* is a generic pointer that stores an address with no associated data type. Because the compiler does not know the data type, it has no knowledge of the object's size in bytes or how to decode the bit pattern. Therefore, dereferencing *voidPtr is a compile-time error until cast (e.g., via static_cast<int*>(voidPtr)).
Q7 • Multi-Indirection What is the primary practical purpose of a pointer-to-pointer (T**)?
Double pointers are primarily used to: (1) Dynamically allocate two-dimensional matrices on the heap as an array of pointers to row arrays, and (2) Allow a function to modify the address held by a pointer in the caller's stack frame (pass-by-address of a pointer).
Q8 • Modern C++ How does the RAII (Resource Acquisition Is Initialization) idiom eliminate manual memory leaks?
RAII binds the lifecycle of a heap resource to the lifetime of a stack-allocated wrapper object (such as std::unique_ptr or std::vector). When the wrapper object goes out of scope, its destructor is automatically called by the C++ runtime, ensuring that delete is invoked deterministically even in the presence of early returns or thrown exceptions.