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.
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.
&
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.
*
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.
#include <iostream>
int main() {
int target = 100;
// Declaration & Initialization:
// 'int*' designates a pointer to an integer
int* ptr = ⌖
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;
}
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;.
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 |
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).
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:
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.
#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;
}
charPtr = 0x5000*charPtr != '\0' is TRUE → Print 'D' → charPtr++charPtr = 0x5001*charPtr != '\0' is TRUE → Print 'e' → charPtr++charPtr = 0x5009'\0' (Byte value 0). Loop condition terminates immediately.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.
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;
}
**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
new & delete OperatorsC++ 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 |
#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;
}
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!
Manual memory management places total responsibility upon the systems engineer. Violating heap etiquette produces two primary memory flaws:
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.
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.
int* p = new int(100);0x8000. Stack pointer p holds 0x8000.p = new int(200); (FATAL LEAK!)p is overwritten with 0x8050. The address 0x8000 is lost permanently with no way to deallocate it!delete p; p = nullptr; ensures the free store recovers memory before acquiring new blocks.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.
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).
int* p = (int*)0x2000;, what address does p + 4 evaluate to on a 64-bit system?
sizeof(int) == 4 bytes. Therefore, p + 4 evaluates to:0x2000 + (4 × 4 bytes) = 0x2000 + 16 (0x10 hex) = 0x2010.
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.
nullptr right after calling delete (e.g., delete ptr; ptr = nullptr;).
void* pointer disallowed without explicit casting?
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)).
T**)?
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.