Raw Pointers
A pointer is a variable that stores a memory address, allowing indirect access to other variables.
Pointer = Address, not the data itself
&gets address →&xgives address of x*dereferences →*ptraccesses value at address- Pointers enable dynamic memory, data structures, polymorphism
Memory Representation
Stack Memory:
┌─────────────┐
│ value: 42 │ ← Address: 0x1000
├─────────────┤
│ ptr: 0x1000 │ ← Stores address of value
└─────────────┘
*ptr = 42 (dereferences to get value)
ptr = 0x1000 (the address itself)
Pointer Basics
Pointers hold memory addresses as their values. You create pointers using the * symbol in the declaration, and you access what they point to using the dereference operator *.
int value = 42;
int* ptr = &value; // ptr stores address of value
// Three ways to interact with pointers:
std::cout << ptr; // Address: 0x7fff5fbff5ac
std::cout << *ptr; // Dereference: 42
std::cout << &ptr; // Address of pointer itself
*ptr = 100; // Modify through pointer
std::cout << value; // 100 (changed!)
int* p1, p2; // p1 is pointer, p2 is int!
int *p3, *p4; // Both are pointers
int* p5; // Best: one per line
int* p6;
Null Pointers
A null pointer doesn't point to any valid object. Always check pointers before dereferencing to avoid crashes.
int* bad; // garbage address
int* ptr = nullptr; // C++11: null pointer literal
if (ptr) {
*ptr = 42; // Safe: checked first
}
*ptr = 42; // Crash! Dereferencing null pointer
nullptris type-safe null (C++11)- Dereferencing null = crash (segmentation fault)
- Uninitialized pointers are worse (random corruption)
Pointer Operations
Pointers support several operations beyond basic dereferencing, including member access and address arithmetic for adjacent memory.
struct Point {
int x, y;
};
Point p = {10, 20};
Point* ptr = &p;
// Member access
(*ptr).x = 30; // Dereference then access (verbose)
ptr->x = 30; // Arrow operator (preferred)
std::cout << ptr->x; // Prints 30
Pointer Comparison
Pointers can be compared to check if they point to the same location or to compare their relative positions in memory.
int a = 10, b = 20;
int* p1 = &a;
int* p2 = &a;
int* p3 = &b;
if (p1 == p2) { // True: both point to a
std::cout << "Same address\n";
}
if (p1 != p3) { // True: point to different objects
std::cout << "Different addresses\n";
}
Pointers and Arrays
Array names decay to pointers to their first element in most contexts. This allows passing arrays to functions efficiently but loses size information.
int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr; // Decays to pointer to first element
std::cout << *ptr; // 1 (first element)
std::cout << *(ptr+1); // 2 (second element)
std::cout << ptr[2]; // 3 (subscript works on pointers)
// Array name is (mostly) equivalent to pointer
arr[2]; // 3
*(arr+2); // 3 (same thing)
Array vs Pointer Differences
Despite appearing similar, arrays and pointers are different types with different semantics, particularly regarding sizeof and assignment.
int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr;
sizeof(arr); // 20 bytes (5 * 4)
sizeof(ptr); // 8 bytes (pointer size on 64-bit)
// arr = ptr; // Error: can't assign to array
ptr = arr; // OK: pointer can be reassigned
ptr[i] is identical to *(ptr + i)
- Compiler scales by
sizeof(type)automatically ptr + 1moves by 4 bytes forint*, not 1 byte
Dynamic Memory
Pointers are essential for dynamic memory allocation on the heap, where object lifetimes extend beyond their creating scope.
// Allocation
int* ptr = new int(42); // Single object
int* arr = new int[100]; // Array
// Usage
*ptr = 100;
arr[0] = 10;
// Cleanup - YOUR RESPONSIBILITY
delete ptr; // Single object
delete[] arr; // Array (must match allocation!)
- Every
newneeds matchingdelete - Every
new[]needs matchingdelete[] - Missing
delete= memory leak - Wrong delete form = undefined behavior
- Double delete = crash
The full new/delete mechanics (arrays, allocation failure, alignment, placement) are in
new and delete. In modern code you rarely call
them directly — smart pointers own the allocation and delete it for you.
Common Dangers
Dangling Pointers
A dangling pointer points to memory that has been deallocated or is no longer valid. Dereferencing creates undefined behavior.
int* dangling = new int(42);
delete dangling;
*dangling = 100; // Undefined behavior: use-after-free
// Set to null after delete to catch errors
delete dangling;
dangling = nullptr;
if (dangling) {
*dangling = 100; // Won't execute
}
Returning Pointers to Locals
Returning a pointer to a local variable creates a dangling pointer because locals are destroyed when the function returns.
int* dangerous() {
int x = 42;
return &x; // Dangling: x destroyed when function returns
}
int* ptr = dangerous();
*ptr = 100; // Undefined behavior
// Correct: return pointer to dynamic memory
int* safe() {
return new int(42); // Caller must delete
}
Use-After-Free
int* ptr = new int(42);
delete ptr;
*ptr = 100; // Undefined behavior
// Solution: nullify after delete
delete ptr;
ptr = nullptr; // Now safe to check
if (ptr) {
*ptr = 100; // Won't execute
}
Memory Leaks
void leak() {
int* ptr = new int(42);
// Never deleted - memory leaked
}
void correct() {
int* ptr = new int(42);
// Use ptr...
delete ptr; // Cleaned up
}
Dangling pointers, use-after-free, double-delete, and leaks are exactly the bugs that RAII and smart pointers eliminate — the owner's destructor deletes exactly once, on every exit path. Use raw owning pointers only at the lowest level; raw non-owning pointers (just observing) remain fine.
Pointer to Pointer
Pointers can point to other pointers, creating multiple levels of indirection useful for modifying pointers themselves or creating 2D structures.
int value = 42;
int* ptr = &value;
int** ptr_to_ptr = &ptr; // Pointer to pointer
std::cout << **ptr_to_ptr; // 42 (double dereference)
**ptr_to_ptr = 100; // Modifies value through double indirection
std::cout << value; // 100
// Common use: modifying a pointer
void allocate(int** pp) {
*pp = new int(42); // Modifies caller's pointer
}
int* p = nullptr;
allocate(&p); // p now points to allocated memory
Double pointers are particularly useful when you need a function to allocate memory and modify the caller's pointer variable. The function receives the address of the pointer variable itself, allowing it to change where that pointer points.
Void Pointers
A void* is a generic pointer that can point to any type but must be cast before dereferencing. It's used for type-agnostic memory operations.
int x = 42;
void* vptr = &x; // Can point to anything
// *vptr; // Error: can't dereference void*
int* iptr = static_cast<int*>(vptr); // Cast back to specific type
*iptr = 100; // OK after cast
// Used in C APIs
void* memcpy(void* dest, const void* src, size_t n);
Function Pointers
Pointers can store addresses of functions, enabling callbacks, plugin systems, and strategy patterns.
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
// Function pointer declaration
int (*operation)(int, int);
operation = add;
std::cout << operation(5, 3); // 8
operation = subtract;
std::cout << operation(5, 3); // 2
// Modern alternative: std::function (better)
std::function<int(int, int)> op = add;
Function pointer syntax is notoriously difficult to read: int (*ptr)(int, int) declares a pointer to a function taking two ints and returning int. Modern C++ prefers std::function which provides a cleaner syntax and can store lambdas, not just function pointers.
Memory Address: Pointers store addresses, not values. The value lives elsewhere.
Dereferencing: *ptr follows the address to access the actual data.
Null Safety: Always initialize to nullptr and check before dereferencing.
Ownership: Who deletes the memory? Unclear with raw pointers → use smart pointers.
Lifetime: Pointers don't extend object lifetime. Locals die at scope end even if pointers exist.
Summary
Core concepts:
- Pointer = variable storing memory address
*dereferences,&gets addressnullptrfor null, always initialize
Memory management:
newallocates,deletefreesnew[]withdelete[]for arrays- Missing delete = leak, double delete = crash
Dangers:
- Dangling pointers (pointing to destroyed data)
- Use-after-free (accessing deleted memory)
- Memory leaks (forgetting to delete)
- Uninitialized pointers (garbage addresses)
Modern practice:
- Use smart pointers (
unique_ptr,shared_ptr) - Raw pointers only for non-owning references
- Let compiler manage lifetime automatically