C++ Pointers
A pointer is a variable that stores the memory address of another variable, rather than a value like 5 or 'a' directly. Pointers are one of the most powerful and distinctive features of C++, letting you manipulate memory directly, build efficient data structures, pass large objects to functions without copying them, and manage dynamic memory. They are also one of the biggest sources of bugs when misused, so understanding exactly how they work is essential to becoming a competent C++ programmer.
Overview / How it works
Every variable in a running program lives somewhere in memory, and that location has a numeric address (something like 0x7ffd3a2b3c1c). A normal variable, such as int age = 25;, lets you work with the value 25. A pointer variable instead stores the address where that value lives. Internally, a pointer is just an integer-sized value (typically 4 or 8 bytes depending on the platform) that the compiler interprets as a memory address, tagged with a type so the compiler knows how many bytes to read/write and how to interpret them at that address.
Because a pointer knows the type of data it points to, the compiler can perform type-safe operations through it: dereferencing an int* reads/writes 4 bytes as an integer, while dereferencing a double* reads/writes 8 bytes as a floating-point number. This is why you cannot silently assign an int* to a double* without a cast — the compiler is protecting you from misinterpreting raw bytes.
Pointers matter because they let you: (1) modify a variable from inside a function (pass-by-address), (2) work with arrays and strings efficiently, since array names decay into pointers to their first element, (3) build dynamic data structures like linked lists and trees, whose size isn’t known at compile time, and (4) allocate memory at runtime with new/delete that outlives the function that created it.
Syntax
type* pointerName; // declare a pointer to 'type'
pointerName = &variable; // store the address of 'variable'
*pointerName; // dereference: access the value at that address
| Symbol | Name | Meaning |
|---|---|---|
type* |
Pointer declaration | Declares a variable that holds an address of a value of type |
& |
Address-of operator | Produces the memory address of a variable |
* |
Dereference operator | Accesses (reads or writes) the value stored at the address a pointer holds |
nullptr |
Null pointer literal | A pointer that intentionally points to nothing |
Note that * is overloaded in C++: in a declaration like int* p, it means "p is a pointer to int". In an expression like *p = 5;, it means "dereference p and assign to what it points to". Beginners often confuse these two uses.
Examples
Example 1: Declaring, addressing, and dereferencing
#include <iostream>
using namespace std;
int main() {
int age = 25;
int* agePtr = &age;
cout << "Value of age: " << age << endl;
cout << "Address of age: " << &age << endl;
cout << "Value stored in agePtr: " << agePtr << endl;
cout << "Value pointed to by agePtr: " << *agePtr << endl;
*agePtr = 30;
cout << "New value of age: " << age << endl;
return 0;
}
Output:
Value of age: 25
Address of age: 0x7ffd3a2b3c1c
Value stored in agePtr: 0x7ffd3a2b3c1c
Value pointed to by agePtr: 25
New value of age: 30
The exact address printed will differ every time you run the program — it depends on where the operating system placed the variable in memory. What matters is that agePtr stores the same address as &age, and that writing through *agePtr changes age itself, because they refer to the same memory location.
Example 2: Pointer arithmetic with arrays
#include <iostream>
using namespace std;
int main() {
int scores[5] = {90, 82, 76, 88, 95};
int* p = scores; // array decays to a pointer to its first element
for (int i = 0; i < 5; i++) {
cout << "Element " << i << ": " << *(p + i) << endl;
}
int sum = 0;
for (int* ptr = scores; ptr < scores + 5; ptr++) {
sum += *ptr;
}
cout << "Sum using pointer: " << sum << endl;
return 0;
}
Output:
Element 0: 90
Element 1: 82
Element 2: 76
Element 3: 88
Element 4: 95
Sum using pointer: 431
When you write scores in most expressions, it decays into a pointer to its first element (equivalent to &scores[0]). Adding an integer to a pointer, like p + i, does not add i raw bytes — it advances the pointer by i elements, automatically scaled by sizeof(int). This is why *(p + i) is equivalent to scores[i]; in fact, a[i] is defined in C++ as syntactic sugar for *(a + i).
Example 3: Pointers with functions and dynamic memory
#include <iostream>
using namespace std;
void swapValues(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 5, y = 10;
cout << "Before swap: x = " << x << ", y = " << y << endl;
swapValues(&x, &y);
cout << "After swap: x = " << x << ", y = " << y << endl;
int* dynamicArr = new int[3]{1, 2, 3};
cout << "Dynamic array: ";
for (int i = 0; i < 3; i++) {
cout << dynamicArr[i] << " ";
}
cout << endl;
delete[] dynamicArr;
return 0;
}
Output:
Before swap: x = 5, y = 10
After swap: x = 10, y = 5
Dynamic array: 1 2 3
By default, C++ passes arguments by value, meaning swapValues would normally receive copies of x and y and any changes would be lost. Passing pointers to x and y instead lets the function reach back into the caller’s memory and modify the originals directly. The second half of the example shows new, which allocates memory on the heap that survives beyond the current scope; that memory must be released with delete[] (for arrays) or delete (for single objects) once it’s no longer needed, or it leaks.
How it works step by step / Under the hood
- When you declare
int age = 25;, the compiler reserves 4 bytes on the stack and records the value 25 there. - When you write
int* agePtr = &age;, the compiler reserves separate storage (typically 8 bytes on a 64-bit system) foragePtr, and stores the numeric address ofage‘s 4 bytes inside it. - Dereferencing with
*agePtrtells the CPU: "go to the address stored inagePtr, and read (or write)sizeof(int)bytes there as an integer." This is a single indirection — one extra memory lookup compared to accessingagedirectly. - Pointer arithmetic (
p + 1) is scaled by the size of the pointed-to type, so it always lands on the next element boundary, never in the middle of an element. - Heap allocations via
newask the operating system/runtime for a block of memory that is not tied to any function’s stack frame, so it persists until explicitly freed withdelete.
Common Mistakes
Mistake 1: Dereferencing an uninitialized or null pointer
int* p; // uninitialized - points to garbage memory
cout << *p; // undefined behavior, likely a crash
An uninitialized pointer holds whatever random bits were already in that memory, which is almost never a valid address. Always initialize pointers, and use nullptr when you have nothing to point to yet:
int* p = nullptr;
if (p != nullptr) {
cout << *p;
} else {
cout << "p has no target yet";
}
Mistake 2: Dangling pointers and memory leaks
int* makePointer() {
int local = 42;
return &local; // BUG: local's memory is gone once the function returns
}
Returning the address of a local (stack) variable creates a dangling pointer — the memory is reclaimed the moment the function ends, so using the returned pointer is undefined behavior. Use dynamically allocated memory (and manage its lifetime carefully, or better, use smart pointers) if the data must outlive the function:
int* makePointer() {
int* heapValue = new int(42);
return heapValue; // caller is now responsible for delete
}
Symmetrically, forgetting to call delete/delete[] on heap memory you allocated causes a memory leak — the memory stays reserved for the life of the program even though nothing uses it anymore.
Best Practices
- Always initialize pointers — use
nullptrif there is no valid address yet, never leave a pointer uninitialized. - Check a pointer against
nullptrbefore dereferencing it if there’s any chance it wasn’t assigned a valid address. - Match every
newwith exactly onedelete, and everynew[]with exactly onedelete[]. - Prefer references (
&) over pointers for function parameters when the argument cannot logically be "absent" — references cannot be null and have cleaner syntax. - In modern C++, prefer smart pointers (
std::unique_ptr,std::shared_ptr) over rawnew/deleteso memory is freed automatically; learn raw pointers first because they explain how smart pointers work underneath. - Never dereference a pointer after the object it points to has been deleted or has gone out of scope.
- Be careful mixing pointer arithmetic with array bounds — walking past the end of an array via a pointer is undefined behavior even though the compiler won’t stop you.
Practice Exercises
- Exercise 1: Write a program that declares an integer, a pointer to it, and prints the variable’s value, its address, and the value obtained by dereferencing the pointer.
- Exercise 2: Write a function
void doubleValue(int* n)that doubles the value pointed to byn. Call it frommainon a variable initialized to 7, and print the result before and after the call (expected: 7, then 14). - Exercise 3: Write a program that dynamically allocates an array of 5 integers with
new, fills it with the squares of 1 through 5 (1, 4, 9, 16, 25) using a pointer, prints them, and then correctly frees the memory withdelete[].
Summary
- A pointer is a variable that stores a memory address rather than an ordinary value.
&retrieves the address of a variable;*dereferences a pointer to access the value it points to.- Array names decay into pointers to their first element, which is why
a[i]and*(a + i)are equivalent. - Passing pointers to functions lets the function modify the caller’s original variables.
new/deletemanage dynamic (heap) memory, which persists until explicitly freed.- Uninitialized, null, and dangling pointers are the most common sources of bugs — always initialize pointers and free every allocation exactly once.
