Pointers and References
Pointers and references are how C++ programs work directly with memory addresses. Mastering them is essential — they underpin dynamic memory allocation, pass-by-reference semantics, polymorphism, and the entire STL iterator model. Modern C++ minimises raw pointer use through smart pointers and references, but you must understand the foundation.
Pointers
A pointer is a variable that stores a memory address. The * operator declares a pointer and dereferences it. The & operator takes the address of a variable:
#include <iostream>
using namespace std;
int main() {
int x = 42;
int* ptr = &x; // ptr stores the address of x
cout << x << "\n"; // 42 — value of x
cout << &x << "\n"; // 0x... — address of x
cout << ptr << "\n"; // 0x... — same address stored in ptr
cout << *ptr << "\n"; // 42 — value at the address (dereference)
// Modify x through the pointer
*ptr = 100;
cout << x << "\n"; // 100 — x was modified through ptr
return 0;
}Pointer Declarations
int* ip; // Pointer to int
double* dp; // Pointer to double
char* cp; // Pointer to char
// Declare multiple pointers on one line — '*' binds to the NAME, not the type
int *p1, *p2; // Both pointers to int
int* p3, p4; // p3 is pointer, p4 is a plain int! (common mistake)
// Pointer to const (the data cannot be changed through this pointer)
const int* cptr = &x;
// *cptr = 10; // ERROR
// Const pointer (the pointer itself cannot be reassigned)
int* const pconst = &x;
pconst = &y; // ERROR
*pconst = 10; // OK — data can be changed
// Const pointer to const (neither can change)
const int* const cpconst = &x;nullptr
Always initialise pointers — use nullptr (C++11) when there's no valid target:
#include <iostream>
using namespace std;
int main() {
int* p = nullptr; // Does not point to anything
// Always check before dereferencing
if (p != nullptr) {
cout << *p << "\n";
} else {
cout << "Pointer is null\n";
}
// nullptr converts to false in boolean context
if (!p) {
cout << "Null pointer\n";
}
// Assigning after declaration
int x = 99;
p = &x;
cout << *p << "\n"; // 99
return 0;
}Warning: Dereferencing a null or invalid pointer causes undefined behaviour — typically a segmentation fault crash. Always initialise pointers to nullptr and check before use.
Pointers and Arrays
Arrays and pointers are closely related — an array name decays to a pointer to its first element:
#include <iostream>
using namespace std;
int main() {
int arr[] = {10, 20, 30, 40, 50};
int* p = arr; // Points to arr[0]
cout << *p << "\n"; // 10 — arr[0]
cout << *(p + 1) << "\n"; // 20 — arr[1]
cout << *(p + 4) << "\n"; // 50 — arr[4]
cout << p[2] << "\n"; // 30 — arr[2] (same as *(p+2))
// Pointer arithmetic — moves by sizeof(type) bytes
for (int* q = arr; q < arr + 5; q++) {
cout << *q << " ";
}
cout << "\n"; // 10 20 30 40 50
// Difference between pointers
int* first = &arr[0];
int* last = &arr[4];
cout << "Distance: " << (last - first) << "\n"; // 4
return 0;
}References
A reference is an alias for an existing variable — once bound, it cannot be rebound:
#include <iostream>
using namespace std;
int main() {
int x = 10;
int& ref = x; // ref is an alias for x
cout << ref << "\n"; // 10
ref = 99; // Modifies x through ref
cout << x << "\n"; // 99
// ref and x are the same memory location
cout << (&ref == &x) << "\n"; // 1 (true)
// References must be initialised at declaration
// int& bad; // ERROR — unbound reference
// Const reference — can bind to temporaries and literals
const int& cref = 42; // Extends lifetime of the temporary
cout << cref << "\n"; // 42
return 0;
}Pointers vs References
| Feature | Pointer | Reference |
|---|---|---|
| Null allowed | Yes (nullptr) | No — must bind to a valid object |
| Rebindable | Yes — can point to different objects | No — always refers to the original |
| Syntax to dereference | *ptr | Transparent (just use ref) |
| Arithmetic | Yes — ptr + n | No |
| Use for optional parameters | Yes | No |
| Use for pass-by-reference | Both work | References are cleaner |
| Use in data structures | Yes | Limited (cannot be stored in containers) |
Pointer to Pointer
#include <iostream>
using namespace std;
int main() {
int x = 42;
int* p = &x; // Points to x
int** pp = &p; // Points to p (pointer to pointer)
cout << x << "\n"; // 42
cout << *p << "\n"; // 42
cout << **pp << "\n"; // 42
**pp = 100; // Modify x through two levels of indirection
cout << x << "\n"; // 100
return 0;
}void* Pointer
A void* holds a pointer to any type — used in generic C APIs:
#include <iostream>
using namespace std;
void printBytes(void* data, size_t count) {
unsigned char* bytes = static_cast<unsigned char*>(data);
for (size_t i = 0; i < count; i++) {
cout << hex << (int)bytes[i] << " ";
}
cout << dec << "\n";
}
int main() {
int n = 0xDEADBEEF;
printBytes(&n, sizeof(n)); // Shows the bytes in memory order
return 0;
}Function Pointers
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// A regular function
int square(int x) { return x * x; }
int cube(int x) { return x * x * x; }
// Function taking a function pointer
void applyAndPrint(int x, int (*transform)(int)) {
cout << "Result: " << transform(x) << "\n";
}
int main() {
// Declare a function pointer
int (*fp)(int) = square;
cout << fp(5) << "\n"; // 25
fp = cube;
cout << fp(5) << "\n"; // 125
applyAndPrint(4, square); // Result: 16
applyAndPrint(4, cube); // Result: 64
// Array of function pointers
int (*ops[])(int) = { square, cube };
for (auto op : ops) cout << op(3) << " ";
cout << "\n"; // 9 27
return 0;
}In modern C++, prefer std::function or lambdas over raw function pointers — they are cleaner and more flexible.
Practical Example: Linked List with Pointers
#include <iostream>
using namespace std;
struct Node {
int data;
Node* next;
Node(int val) : data(val), next(nullptr) {}
};
class LinkedList {
Node* head;
public:
LinkedList() : head(nullptr) {}
// Insert at front
void push(int val) {
Node* node = new Node(val);
node->next = head;
head = node;
}
// Remove and return front
int pop() {
if (!head) throw runtime_error("Empty list");
int val = head->data;
Node* temp = head;
head = head->next;
delete temp;
return val;
}
void print() const {
Node* curr = head;
while (curr) {
cout << curr->data << " -> ";
curr = curr->next;
}
cout << "null\n";
}
bool contains(int val) const {
Node* curr = head;
while (curr) {
if (curr->data == val) return true;
curr = curr->next;
}
return false;
}
// Destructor — free all nodes
~LinkedList() {
Node* curr = head;
while (curr) {
Node* next = curr->next;
delete curr;
curr = next;
}
}
};
int main() {
LinkedList list;
list.push(1);
list.push(2);
list.push(3);
list.print(); // 3 -> 2 -> 1 -> null
cout << list.pop() << "\n"; // 3
list.print(); // 2 -> 1 -> null
cout << list.contains(2) << "\n"; // 1 (true)
cout << list.contains(5) << "\n"; // 0 (false)
return 0;
}Summary
- A pointer stores a memory address; dereference with
*, take address with& - Always initialise pointers — use
nullptrwhen there's no valid target - Pointer arithmetic steps by
sizeof(type)—ptr + 1moves to the next element, not the next byte - A reference is a permanent alias — it cannot be null, cannot be rebound, and has cleaner syntax
- Use references for function parameters when you want to avoid copying but not allow null
- Use pointers when you need nullability or rebindability, or when working with arrays
const int*— pointer to const data;int* const— const pointer to mutable data;const int* const— both- Function pointers store the address of a function; in modern C++ prefer
std::functionor lambdas
Next, we'll learn about dynamic memory — new, delete, heap allocation, and how memory leaks happen.