📖 Chapter 2.7: Pointers and Memory
⏱ Estimated reading time: 90 minutes | Difficulty: 🟡 Beginner to Intermediate (the key to understanding C++)
📝 Prerequisites: Chapter 2.3 (functions, arrays, pass-by-reference), Chapter 2.4 (structs and classes)
Many beginners feel that pointers are "mysterious," "crash-prone," and "best avoided." But the truth is: pointers are the key to understanding how C++ uses memory. Once you build the mental picture that "variables live in memory, and every home has a house number," concepts that recur throughout the rest of the book—pointers, references, arrays, dynamic memory, linked lists, trees—suddenly click into place.
This chapter does not ask you to become a memory-management expert. Instead, it helps you build the correct mental model and master the pointer skills you will actually use in competitive programming.
Prerequisites
- Chapter 2.3: functions, arrays, pass-by-reference (
¶meters) - Chapter 2.4: structs and classes (pointers are heavily used with structs in linked lists/trees)
- Know that a variable is a "container that stores data"
🎯 Learning Objectives
After studying this chapter, you will be able to:
- Understand the relationship between "memory," "address," and "pointer," and build a correct mental picture
- Use the address-of operator
&and the dereference operator* - Distinguish between pointers and references, and know when to use each
- Understand the relationship between array names and pointers, and read pointer arithmetic
- Use
new/deletefor dynamic memory allocation, and understand what a memory leak is - Use pointers to build the most basic linked list and binary tree nodes
- Know when to use pointers and when not to in competitive programming
- Understand multi-level pointers (
int**), read dynamic 2D arrays and "pointers to pointers" - Use pointers to manipulate C-style strings (
char*), hand-writestrlen/strcpy - Know the basic syntax of function pointers, read callbacks and the mechanism behind custom sorting
2.7.1 Memory, Addresses, and Pointers: Build the Picture First
🏨 The Hotel Analogy
Imagine computer memory as a hotel with many rooms:
- Each room is one byte (or a group of bytes) of storage
- Each room has a room number—this is the memory address
- When you write
int x = 42;, the compiler assigns variablexa room (say, room 1000) holding 42 - A pointer is a note with a "room number" written on it. It is not the data in the room itself, but information about "where a certain room is"
💡 Core sentence: A normal variable stores data; a pointer variable stores the address of another variable.
Address-of & and Dereference *
C++ provides two operators that are inverses of each other:
| Operator | Name | Effect | How to read |
|---|---|---|---|
&x | address-of | get the memory address of variable x | "the address of x" |
*p | dereference | access the variable that pointer p points to | "the thing p points to" |
📄 View code: First encounter with pointers
#include <bits/stdc++.h>
using namespace std;
int main() {
int x = 42;
int* p = &x; // p stores the address of x (p "points to" x)
cout << x << "\n"; // 42 —— the data in x
cout << &x << "\n"; // 0x... —— the address of x (may differ each run)
cout << p << "\n"; // 0x... —— same as &x; this is what p stores
cout << *p << "\n"; // 42 —— dereference: follow the address to x, read 42
*p = 100; // modify x through the pointer (follow the room number, change content)
cout << x << "\n"; // 100 —— x really changed!
return 0;
}
🤔 Are the
*inint* pand the*in*pthe same thing? No! This is the biggest source of confusion for beginners:
- In a declaration, the
*inint* pmeans "p is a pointer to an int" (part of the type)- In usage, the
*in*pis the dereference operator, meaning "fetch the value p points to" Same symbol, two meanings, disambiguated by context.
Diagram: A pointer pointing to a variable
🎬 Interactive: See & and * Step by Step
This visualization breaks "take address → dereference-read → dereference-write" into 5 steps. Click "Next" and watch the memory rooms and the pointer arrow to see, with your own eyes, how *p = 100 reaches through the pointer to actually change x:
2.7.2 Null Pointers and nullptr
If a pointer does not yet point to any valid object, you should make it point to "nothing." Modern C++ uses nullptr to represent a null pointer:
int* p = nullptr; // p points to nothing
if (p == nullptr) {
cout << "p is null, cannot dereference\n";
}
// *p; // ❌ Dangerous! Dereferencing a null pointer → crash (segfault)
⚠️ Do not use the old
NULLor0. Old code often writesint* p = NULL;, butNULLis essentially0and causes ambiguity in overload/template scenarios. Modern C++ always usesnullptr—type-safe and semantically clear.
🐛 The number-one crash cause: Dereferencing a null pointer or a "wild pointer" (one pointing to an invalid address). This is the most common source of
Segmentation fault. Always verify a pointer is valid before dereferencing.
🧩 Distinguishing: & and * Each Have Two Faces
Both & and * are "one symbol, several meanings," disambiguated by context. Once a beginner separates these clearly, half of pointers is understood:
| Symbol | Context | Meaning | Example |
|---|---|---|---|
& | before a variable (unary) | address-of | int* p = &x; |
& | after a type (declaration) | reference (alias, see Ch. 2.3) | int& r = x; |
& | between two numbers (binary) | bitwise AND (see Ch. 2.6) | a & b |
* | after a type (declaration) | "this is a pointer" | int* p; |
* | before a pointer (unary) | dereference to get the value | *p = 100; |
* | between two numbers (binary) | multiplication | a * b |
💡 One-line memory aid: standing alone next to a variable/pointer → address-of / dereference; squeezed between two numbers → AND / multiply; following a type → reference / pointer declaration.
2.7.3 Pointer vs Reference: Which One Should You Use?
In Chapter 2.3 you already learned about references (int& r = x;). Both references and pointers can "indirectly access" another variable, but they have important differences.
📄 View code: Reference vs pointer side-by-side
int x = 10, y = 20;
// ---- Reference ----
int& r = x; // r is an alias for x, must be bound at declaration
r = 100; // directly changes x, x == 100
// r = y; // this does NOT "rebind r to y"; it assigns y's value to x!
// once bound, a reference can never be rebound
// ---- Pointer ----
int* p = &x; // p points to x
*p = 100; // changes x, x == 100
p = &y; // OK! a pointer can be reassigned to y
*p = 200; // now changes y, y == 200
p = nullptr; // a pointer can be null
Comparison Table
| Feature | Reference int& | Pointer int* |
|---|---|---|
| Must be initialized | Yes (bound at declaration) | Can be left uninitialized (but dangerous) |
| Can change target | No (bound to one variable for life) | Yes (can repoint anytime) |
| Can be null | No | Yes (nullptr) |
| Syntax | used directly like a variable r | needs dereference *p |
| Safety | safer, less error-prone | more flexible, but more error-prone |
⚡ Rule of thumb for competitive programming:
- Prefer references. For passing parameters and avoiding copies of large objects, references are safer and cleaner (see
const vector<int>&in Chapter 2.3).- The few cases that need pointers: ① the target needs to change (e.g., traversing a linked list); ② you need to represent "no target" (
nullptr, e.g., a tree's empty child); ③ dynamic memory allocation (new).
2.7.4 Pointers and Arrays: They Are Relatives
In C++, arrays and pointers are closely related. In most contexts, an array name "decays" into a pointer to its first element.
📄 View code: An array name is the address of its first element
#include <bits/stdc++.h>
using namespace std;
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr; // same as int* p = &arr[0]; the name decays to a first-element pointer
cout << *p << "\n"; // 10 —— arr[0]
cout << *(p+1) << "\n"; // 20 —— arr[1]
cout << *(p+2) << "\n"; // 30 —— arr[2]
// The following four are completely equivalent:
cout << arr[2] << "\n"; // 30
cout << *(arr+2) << "\n"; // 30
cout << p[2] << "\n"; // 30
cout << *(p+2) << "\n"; // 30
return 0;
}
🤔 Why is
arr[i]equivalent to*(arr + i)? Because the subscript operationarr[i]is essentially "offsetielements from the base addressarr, then dereference." This also explains why array indices start at 0—the index is the "offset," and the first element has offset 0.
Pointer Arithmetic
Pointer +1 does not mean address +1 byte; it means advance by one element's size:
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr;
// Assume int is 4 bytes and arr starts at address 1000:
// p points to 1000 (arr[0])
// p+1 points to 1004 (arr[1]) —— automatically skips one int's 4 bytes
// p+2 points to 1008 (arr[2])
📄 View code: Traversing an array with a pointer
int arr[5] = {10, 20, 30, 40, 50};
// Method 1: traditional index
for (int i = 0; i < 5; i++) cout << arr[i] << " ";
cout << "\n";
// Method 2: pointer traversal (good to understand; rarely written this way in contests)
for (int* p = arr; p < arr + 5; p++) {
cout << *p << " ";
}
cout << "\n";
⚡ Contest tip: In STL calls like
sort(arr, arr + n)andlower_bound(arr, arr + n, x),arrandarr + nare exactly the "first-element pointer" and "past-the-end pointer." Once you understand array decay, you understand why these functions take such arguments.
2.7.5 Pointers as Function Parameters
In Chapter 2.3 we used references to let a function modify the original variable. You can also do this with pointers—the classic C style, still common in low-level code and linked-list/tree operations.
📄 View code: Swap two variables using pointers
#include <bits/stdc++.h>
using namespace std;
// Using pointers: pass addresses, modify originals via dereference
void swapByPointer(int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
// Using references: cleaner (recommended)
void swapByRef(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
int main() {
int x = 1, y = 2;
swapByPointer(&x, &y); // must explicitly pass addresses &x, &y
cout << x << " " << y << "\n"; // 2 1
swapByRef(x, y); // pass variables directly
cout << x << " " << y << "\n"; // 1 2
return 0;
}
💡 How to choose? In C++, use references when you can (clean syntax, can't be null). Typical cases that need pointers: the parameter might be
nullptr("optional" semantics), or you are writing a linked list/tree—structures naturally expressed with pointers.
2.7.6 Dynamic Memory: new and delete
So far, our array sizes were either known at compile time or handled by vector. There is a third way: manually request memory from the system at runtime, called dynamic memory allocation.
Stack Memory vs Heap Memory
📄 View code: new and delete basics
#include <bits/stdc++.h>
using namespace std;
int main() {
// Dynamically allocate a single int
int* p = new int; // request one int on the heap
*p = 42;
cout << *p << "\n"; // 42
delete p; // must free after use! otherwise memory leak
// Dynamically allocate an array (size known only at runtime)
int n;
cin >> n;
int* arr = new int[n]; // request n ints on the heap
for (int i = 0; i < n; i++) arr[i] = i * i;
for (int i = 0; i < n; i++) cout << arr[i] << " ";
cout << "\n";
delete[] arr; // free an array with delete[] (with brackets)!
return 0;
}
⚠️ Three Iron Rules
newpairs withdelete,new[]pairs withdelete[]. If you allocate an array withnew[], you must free it withdelete[], otherwise behavior is undefined.- Every
newmust have a matchingdelete. Otherwise you get a memory leak—allocated memory is never returned. - Do not use a pointer after
delete(it becomes a "dangling pointer"); setp = nullptr;right afterdelete.
🤔 Do you actually use
new/deletein contests? In the vast majority of cases: no! In contests the program exits right after running, and the OS reclaims all memory, so "memory leaks" are usually harmless; plusnew/deleteare verbose and error-prone.
- Need a dynamic array? → use
vector(auto-managed memory, see Chapter 2.3)- Need a dynamic string? → use
stringThe places you really hand-writeneware mainly linked lists and binary trees that create nodes one by one (see the next section)—and even then, many competitors use array simulation or a static node pool to avoidnew(see Chapters 5.5, 5.7).
2.7.7 The Killer Application of Pointers: Linked List and Tree Nodes
Where pointers are truly irreplaceable is in building data structures where nodes connect to each other. The most classic are linked lists and trees.
Singly Linked List Node
Each node is represented by a struct: holding data, plus a pointer to the next node.
📄 View code: Hand-written singly linked list
#include <bits/stdc++.h>
using namespace std;
struct Node {
int data;
Node* next; // pointer to the next node; the last node is nullptr
};
int main() {
// Manually build 1 → 2 → 3
Node* head = new Node{1, nullptr};
head->next = new Node{2, nullptr};
head->next->next = new Node{3, nullptr};
// Traverse the list
for (Node* cur = head; cur != nullptr; cur = cur->next) {
cout << cur->data << " "; // 1 2 3
}
cout << "\n";
// Free (delete node by node from head to tail)
while (head != nullptr) {
Node* tmp = head;
head = head->next;
delete tmp;
}
return 0;
}
🤔 What is the
cur->datasyntax?curis a pointer, andcur->datais equivalent to(*cur).data, meaning "follow the pointer to the node, then access itsdatamember."->is the dedicated "access member through pointer" operator, nicer than(*cur).data. This syntax appears everywhere in linked lists and trees.
Binary Tree Node
A tree is the natural extension of the linked-list idea—each node has not just one "next," but two child pointers, left and right:
📄 View code: Binary tree node definition
struct TreeNode {
int val;
TreeNode* left; // left child (nullptr if none)
TreeNode* right; // right child (nullptr if none)
};
// Create a node
TreeNode* root = new TreeNode{1, nullptr, nullptr};
root->left = new TreeNode{2, nullptr, nullptr};
root->right = new TreeNode{3, nullptr, nullptr};
// 1
// / \
// 2 3
🔗 Bridging ahead: The full algorithms for linked lists and trees (traversal, insertion, deletion, BST, balancing) are covered in detail in Chapter 5.5 (Binary Trees and Tree Algorithms). In this chapter you only need to understand the core mechanism of "how pointers string nodes together."
🆚 Pointer Version vs Array-Simulation Version (Contest Focus)
Competitors often avoid new and use array indices as "pointers"—this is called array simulation (static allocation / node pool). Building this comparison early makes Chapters 5.5/5.7 feel familiar:
| Dimension | Pointer version | Array-simulation version |
|---|---|---|
| Node definition | struct Node{int v; Node* nxt;}; | int val[N], nxt[N]; |
| "Points to" | a real pointer Node* | an array index (int, e.g. nxt[i] holds the next index) |
| Null pointer | nullptr | an agreed special value, e.g. -1 or 0 |
| New node | new Node{...} (heap alloc, slow) | ++cnt grabs the next free slot (no alloc overhead) |
| Freeing | delete (easy to miss → leak) | no freeing; reuse the whole array |
| Speed/safety | slower, easier to get wrong | faster, more robust, the contest mainstream |
📄 View code: The same list written two ways
// ---- Pointer version ----
struct Node { int v; Node* nxt; };
Node* head = new Node{1, nullptr};
head->nxt = new Node{2, nullptr};
// ---- Array-simulation version (common in contests) ----
const int N = 100005;
int val[N], nxt[N], cnt = 0; // index 0 reserved as "null"; real nodes start at 1
int head = 0; // 0 means empty list
int newNode(int v) { // "new" a node → return its index
++cnt; val[cnt] = v; nxt[cnt] = 0; return cnt;
}
// Build 1 -> 2
head = newNode(1);
nxt[head] = newNode(2);
// Traverse: indices replace pointers, 0 replaces nullptr
for (int i = head; i != 0; i = nxt[i]) cout << val[i] << " ";
📌 Extension (just be aware): In production code, smart pointers
unique_ptr/shared_ptrare commonly used to manage memory automatically (auto-deletewhen leaving scope, no leaks). But they are almost never used in contests—they add overhead, and a contest program reclaims memory on exit anyway. Just know they exist; in contests keep using raw pointers or array simulation.
2.7.8 const and Pointers: Read-Only Protection
When const combines with a pointer, different positions mean different things. This is a high-frequency topic in interviews and when reading source code:
int x = 10, y = 20;
const int* p1 = &x; // pointer to const: cannot change value through p1, but can repoint
// *p1 = 99; ❌ cannot change value
p1 = &y; // ✓ can repoint
int* const p2 = &x; // const pointer: can change value, but cannot repoint
*p2 = 99; // ✓ can change value
// p2 = &y; ❌ cannot repoint
const int* const p3 = &x; // neither can change
💡 Reading mnemonic: Read right to left.
const int* preads as "p is a pointer to a const int";int* const preads as "p is a const pointer to an int." Whateverconstis closest to is what it governs.
2.7.9 Multi-Level Pointers: Pointers to Pointers
A pointer itself is also a variable—it lives in some room in memory. So—a pointer also has an address. A pointer that points to another "pointer" is a multi-level pointer; the most common is the double pointer int**.
Why Do We Need Pointers to Pointers?
Three classic scenarios:
- Modifying a pointer itself inside a function (e.g., making an
int*parameter point to newly allocated memory) - Dynamic 2D arrays (
int**points to an array ofint*, eachint*points to a row) - Command-line arguments
argv(main's second parameter ischar**)
📄 View code: Double pointer modifies the pointer itself
#include <bits/stdc++.h>
using namespace std;
// Want a function to change an external pointer's target? Pass a double pointer!
void allocate(int** pp, int n) {
*pp = new int[n]; // dereference the double pointer to modify the external pointer
}
int main() {
int* arr = nullptr;
allocate(&arr, 5); // pass the address of arr (int**)
for (int i = 0; i < 5; i++) arr[i] = i * i;
for (int i = 0; i < 5; i++) cout << arr[i] << " ";
cout << "\n";
delete[] arr;
return 0;
}
💡 You can also use a reference to a pointer (
int*&) to achieve the same effect with cleaner syntax:void allocate(int*& p, int n) { p = new int[n]; }But double pointers are the lower-level understanding and a common pattern in legacy and low-level code.
Dynamic 2D Arrays
When both the number of rows and columns are known only at runtime, you can use int** to build a 2D array:
📄 View code: Building a dynamic 2D array with double pointers
#include <bits/stdc++.h>
using namespace std;
int main() {
int n = 3, m = 4;
// Allocate n int* (row pointers)
int** mat = new int*[n];
for (int i = 0; i < n; i++) {
mat[i] = new int[m]; // allocate m ints for each row
}
// Assign values
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
mat[i][j] = i * m + j;
// Print
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++)
cout << setw(3) << mat[i][j];
cout << "\n";
}
// Free: free each row first, then the row-pointer array
for (int i = 0; i < n; i++) delete[] mat[i];
delete[] mat;
return 0;
}
⚡ Contest tip: The
int**2D array above is almost never used in contests—it requires multiplenewcalls, memory is not contiguous (cache-unfriendly), and freeing is tedious. For dynamic 2D arrays in contests, always usevector<vector<int>>(see Chapter 2.3)—safe and concise. The double-pointer example here is mainly to build your mental picture of "pointer to pointer," so you can read low-level or legacy code.
Diagram: Double Pointer Hierarchy
📌 In one sentence:
int x→int* p = &x(single-level pointer, points to a variable) →int** pp = &p(double pointer, points to a pointer). Each level of dereference*moves one step forward along the arrow:*ppgivesp,**ppgivesx.
2.7.10 Pointers and C-Style Strings
Before C++ was born, C used null-terminated (\0) character arrays to represent strings. These "C-style strings" have type char* or const char*. Although std::string is the absolute mainstream in modern C++, understanding char* helps with reading low-level code, contest templates, and deepening your pointer skills.
String Literals
const char* s = "hello"; // s points to the string literal "hello\0"
// s[0] = 'H'; ❌ String literals live in read-only memory—modifying is undefined behavior!
⚠️ The string literal
"hello"has typeconst char[6](including the trailing\0) and is stored in read-only memory. So you must useconst char*to point to it. Old code may writechar* s = "hello";—this is not recommended in modern C++ (and may not even compile).
Traversing C-Style Strings with Pointers
C-style strings end with \0, so you don't need the length to iterate:
📄 View code: Pointer-based strlen and strcpy
#include <bits/stdc++.h>
using namespace std;
// Hand-written strlen: count until '\0'
int myStrlen(const char* s) {
int len = 0;
while (*s != '\0') { // equivalent to while (*s)
len++;
s++; // advance pointer to next character
}
return len;
}
// Hand-written strcpy: copy src into dst
void myStrcpy(char* dst, const char* src) {
while (*src != '\0') {
*dst = *src; // copy character by character
dst++;
src++;
}
*dst = '\0'; // don't forget the trailing '\0'!
}
// More "pointer-ish" one-liner:
void myStrcpyV2(char* dst, const char* src) {
while ((*dst++ = *src++)); // assign + check + advance, all in one
}
int main() {
const char* s = "hello";
cout << myStrlen(s) << "\n"; // 5
char buf[100];
myStrcpy(buf, s);
cout << buf << "\n"; // hello
return 0;
}
💡 The one-liner
while ((*dst++ = *src++));is a classic C-style pointer exercise. It compresses assignment, pointer advance, and condition check into a single line—the essence of pointer operations. If you're a beginner, work through the step-by-step version first, then come back to this one-liner—you'll find pointers much less mysterious.
C-Style Strings vs std::string
| Feature | char* / char[] | std::string |
|---|---|---|
| Terminator | \0 (manual) | auto-managed |
| Length | strlen(s) O(n) | s.length() O(1) |
| Concatenation | strcat (manual, dangerous) | s1 + s2 (safe) |
| Memory management | manual | automatic |
| When to use | Reading old code, low-level interfaces, argv | 99% of contest scenarios |
⚡ Contest iron rule: prefer
std::string. Only deal withchar*inmain(int argc, char* argv[])command-line arguments or when calling certain C library functions. The goal of this section is not to make you go back to C-style strings, but to let you read them when you see them.
2.7.11 Introduction to Function Pointers
In C++, functions also have addresses. A function pointer is a pointer that points to a function—you can pass "what operation to perform" just like passing data. This is the foundation of the callback mechanism.
Basic Syntax
Function pointer declarations have a distinctive syntax—you must wrap the * and the pointer name in parentheses:
// A function: takes two ints, returns a bool
bool cmp(int a, int b) { return a > b; }
// The corresponding function pointer type: bool (*)(int, int)
bool (*fp)(int, int) = cmp; // fp points to cmp
// ↑ ↑ ↑
// return name(must have parentheses) parameter type list
// Calling a function pointer: two equivalent ways
cout << fp(3, 5) << "\n"; // false (3 > 5)
cout << (*fp)(3, 5) << "\n"; // false (same, more explicit)
Typical Contest Application: Custom Sorting
std::sort's third argument is a function pointer (or function object):
📄 View code: Custom sorting with a function pointer
#include <bits/stdc++.h>
using namespace std;
bool cmpAbs(int a, int b) {
return abs(a) < abs(b); // sort by ascending absolute value
}
int main() {
vector<int> a = {-5, 3, -1, 4, -2};
sort(a.begin(), a.end(), cmpAbs); // cmpAbs passed as a function pointer
for (int x : a) cout << x << " "; // -1 -2 3 4 -5
cout << "\n";
return 0;
}
💡 In C++, lambda expressions or function objects (functors) are preferred over raw function pointers—they are more type-safe and easier for the compiler to inline. But function pointers are the foundation for understanding these advanced features—lambdas are essentially syntactic sugar over function pointers or function objects.
Function Pointer Arrays
When you need to dynamically choose among multiple operations, function pointer arrays are very handy:
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int (*ops[3])(int, int) = {add, sub, mul}; // array of function pointers
int op; cin >> op;
cout << ops[op](10, 5) << "\n"; // dynamically choose the operation
⚡ Contest tip: Function pointers are not commonly used in contests (often replaced by lambdas,
std::function, or direct overloading), but they are essential knowledge for understanding callbacks, event-driven programming, and C standard library functions likeqsort. Know they exist; no need to go deep.
⚠️ Chapter 2.7 Common Mistakes
| # | Mistake | Example | What's wrong | Fix |
|---|---|---|---|---|
| 1 | Dereferencing null/wild pointer | int* p; *p = 5; | p is uninitialized, points to a random address → segfault | Point p to a valid object first, or init to nullptr and check |
| 2 | Freeing new[] with delete | int* a = new int[n]; delete a; | Arrays must use delete[], otherwise undefined behavior | Change to delete[] a; |
| 3 | Memory leak | only new, never delete | Allocated heap memory is never returned | Match every new with a delete; in contests prefer vector |
| 4 | Dangling pointer | delete p; cout << *p; | After delete the memory is freed, p points to an invalid region | After delete p;, immediately p = nullptr; |
| 5 | Confusing declaration * with dereference * | int *p = *x; | Wanted address-of but wrote dereference | Use &x for address: int* p = &x; |
| 6 | Returning address of a local variable | int* f(){ int t=5; return &t; } | After the function returns, t is destroyed and the address is invalid | Return the value itself, or use new/a reference |
| 7 | Uninitialized reference | int& r; | A reference must be bound at declaration | Bind at declaration: int& r = x; |
Chapter Summary
📌 Key Takeaways
| Concept | Key point | Why it matters |
|---|---|---|
| Address | Every variable has a room number in memory | The foundation of pointers |
Pointer int* p | Stores "the address of another variable" | Basis of indirect access and dynamic structures |
&x address-of | Gets the address of variable x | Assigning to pointers, passing by pointer |
*p dereference | Follows the address to access the pointed variable | Reading/writing the data a pointer points to |
nullptr | Represents "points to nothing" | Null state, null-check protection |
| Reference vs pointer | References are safe, can't rebind; pointers are flexible, nullable, rebindable | Prefer references in contests |
| Array decay | Array name ≈ first-element pointer; arr[i] == *(arr+i) | Understanding STL range params (arr, arr+n) |
new / delete | Manual allocate/free on the heap | Building linked-list/tree nodes; prefer vector for ordinary arrays |
-> operator | p->data equals (*p).data | Standard syntax for linked-list and tree operations |
Multi-level pointer int** | Pointer to a pointer; used to modify pointers themselves or for dynamic 2D arrays | Understanding low-level code, argv, dynamic 2D arrays |
C-style string char* | Null-terminated character array; traversable/operable with pointers | Reading legacy code and low-level interfaces; understanding string internals |
| Function pointer | Pointer to a function; lets you pass "operations" like data | Understanding callbacks, sort custom comparators, qsort |
❓ Frequently Asked Questions
Q1: Pointers are so dangerous—are they still worth learning for contests?
A: Yes, but keep priorities straight. For 90% of problems,
vector,string, and references are enough—no raw pointers needed. But when you reach linked lists, binary trees, the "chained forward star" graph representation, or read others' template code, you cannot move an inch without understanding pointers. The goal of this chapter is to let you "read and use them correctly," not to hand-writeneweverywhere.
Q2: Reference or pointer—which should I prefer?
A: Prefer references. To avoid copies when passing parameters → use
const T&; to modify the original variable → useT&. Only when "the target changes" (traversing a linked list), "there might be no target" (a tree's empty childnullptr), or "dynamic allocation" (new) do you need pointers.
Q3: I keep mixing up *p, &x, and p->m. What do I do?
A: Remember three phrases:
&xis "where x lives" (address-of);*pis "follow p to look at that room" (dereference);p->mis "follow p to the struct, then access its member m." Write out linked-list traversal a few times and the muscle memory forms.
Q4: Do I really have to manually delete in contests? Will I lose points if I forget?
A: Usually not. When the program ends, the OS auto-reclaims all memory, so memory leaks in contests are generally harmless. What you really need to watch is repeated
newduring execution causing MLE (memory limit exceeded)—which is rare. Conclusion: usevector/stringinstead of manual memory management whenever you can.Here is a counter-example that triggers MLE—repeatedly
newinside a big loop without everdelete:// ❌ Dangerous: allocate on the heap every iteration, never free for (int i = 0; i < 10000000; i++) { int* p = new int[1000]; // 4KB each time, ~40 GB total → MLE! // ... done with p but forgot delete[] p; } // ✅ Fix: either allocate once outside the loop and reuse, or delete[] p; after use, // safest is just vector<int> p(1000); (auto-reclaimed when leaving scope)
Q5: Why do some competitors' tree/linked-list code use no pointers at all?
A: They use array simulation (also called static allocation / a node pool): three arrays
int left[N], right[N], val[N];serve as nodes, with indices acting as "pointers." This is faster, less error-prone, and avoidsnew's overhead—the mainstream approach in contests (see Chapters 5.5, 5.7). Understand the pointer version first, and the array version becomes clearer.
Q6: Do I really need multi-level pointers and function pointers in contests?
A: You won't write them often, but understanding them greatly improves your code-reading ability. Double pointers help you understand
argv(command-line arguments) and the low-level of dynamic 2D arrays; function pointers help you understandsort's custom comparator andqsortcallbacks. More importantly, these concepts are the foundation for later advanced features like STL function objects and lambda expressions. In one sentence: you don't have to write them every day, but you cannot afford to be unable to read them.
🔗 Connections to Later Chapters
- Chapter 5.5 (Binary Trees and Tree Algorithms): the
TreeNodestruct here is the starting point for tree traversal and BSTs - Chapter 5.6 (Union-Find), Chapter 5.7 (Segment Trees): you will see the trade-offs between the "pointer version" and the "array-simulation version"
- Chapter 5.1 (Introduction to Graphs): the chained forward star essentially uses arrays to simulate a linked list to store a graph
- The "memory and address" mental model built here deepens your understanding of
vectorresizing,stringinternals, and reference parameter passing
Practice Problems
🌡️ Warm-Up Problems
Warm-Up 2.7.1 — Modify Through a Pointer
Declare int x = 5;, define a pointer p to x, change x to 99 through *p, then print x.
Expected output: 99
💡 Solution (click to expand)
Idea: Assign to the pointer with &x, then assign through *p.
#include <bits/stdc++.h>
using namespace std;
int main() {
int x = 5;
int* p = &x; // p points to x
*p = 99; // modify x through the pointer
cout << x << "\n"; // 99
return 0;
}
Key points:
int* p = &x;—&takes the address*p = 99;—*dereferences, modifyingxitself
Warm-Up 2.7.2 — Pointer Swap
Write a function void swp(int* a, int* b) that swaps two variables using pointers. In main, read two integers, swap them, and print.
Sample input: 3 7 → Sample output: 7 3
💡 Solution (click to expand)
#include <bits/stdc++.h>
using namespace std;
void swp(int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
int main() {
int x, y;
cin >> x >> y;
swp(&x, &y); // remember to pass addresses
cout << x << " " << y << "\n";
return 0;
}
Key points:
- The parameters are
int*, so you must pass addresses&x, &ywhen calling - Inside the function, use
*a,*bto access the originals
Warm-Up 2.7.3 — Arrays and Pointers
Declare int arr[5] = {10,20,30,40,50};, define a pointer p = arr, and print the third element using *(p+2).
Expected output: 30
💡 Solution (click to expand)
#include <bits/stdc++.h>
using namespace std;
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr; // the array name decays to a first-element pointer
cout << *(p + 2) << "\n"; // 30, equivalent to arr[2]
return 0;
}
Key points:
p = arris equivalent top = &arr[0]*(p+2)equalsp[2],arr[2]—pointer arithmetic advances by element size
🏋️ Core Problems
Problem 2.7.4 — Find the Max and Return a Pointer
Read N integers into an array, write a function that returns a pointer to the maximum element, and in main print the max through that pointer.
Sample input:
5
3 9 2 9 4
Sample output: 9
💡 Solution (click to expand)
Idea: The function scans the array, records the address of the max element, and returns it.
#include <bits/stdc++.h>
using namespace std;
int* findMax(int* arr, int n) {
int* best = arr; // assume the first is the max
for (int i = 1; i < n; i++) {
if (arr[i] > *best) best = &arr[i];
}
return best; // return a pointer to the max element
}
int main() {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
int* p = findMax(a.data(), n); // a.data() gives the underlying array pointer
cout << *p << "\n";
return 0;
}
Key points:
- We return a pointer "into an element of the array"; the array is still alive, so this is safe (not returning the address of a local variable!)
a.data()—vectorprovides a pointer to its underlying contiguous arraybestalways holds the address of the current maximum element
Problem 2.7.5 — Reverse an Array In Place Using Pointers Read N integers and reverse the array in place using two pointers (one from the front, one from the back), then print.
Sample input:
5
1 2 3 4 5
Sample output: 5 4 3 2 1
💡 Solution (click to expand)
Idea: Two pointers, swapping from both ends toward the middle—the pointer version of the reversal algorithm from Chapter 2.3.
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
int* left = &a[0];
int* right = &a[n - 1];
while (left < right) { // pointers can be compared directly
swap(*left, *right);
left++;
right--;
}
for (int x : a) cout << x << " ";
cout << "\n";
return 0;
}
Key points:
- Pointers into the same array can be compared with
<,>to compare positions left++/right--advance/retreat the pointers by element- The condition
left < rightensures we stop when they meet at the midpoint
Problem 2.7.6 — Hand-Written Linked List Sum
Without vector, use struct Node{int data; Node* next;} to manually build a linked list of N integers, traverse to compute the sum, and print it.
Sample input:
4
10 20 30 40
Sample output: 100
💡 Solution (click to expand)
Idea: Append new nodes at the tail as you read, then traverse from the head to sum.
#include <bits/stdc++.h>
using namespace std;
struct Node {
int data;
Node* next;
};
int main() {
int n;
cin >> n;
Node* head = nullptr;
Node* tail = nullptr;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
Node* node = new Node{x, nullptr};
if (head == nullptr) { // the first node
head = tail = node;
} else {
tail->next = node; // attach to the tail
tail = node; // update the tail pointer
}
}
long long sum = 0;
for (Node* cur = head; cur != nullptr; cur = cur->next) {
sum += cur->data;
}
cout << sum << "\n";
// free
while (head) { Node* t = head; head = head->next; delete t; }
return 0;
}
Key points:
- Use two pointers
headandtailfor O(1) tail insertion cur = cur->nextis the standard linked-list traversal idiomdeletenode by node after use (can be omitted in contests; memory is auto-reclaimed on exit)
🏆 Challenge Problems
Challenge 2.7.7 — Reverse a Linked List (Classic interview & contest fundamental)
Build a linked list 1→2→3→4→5, reverse it into 5→4→3→2→1, then print it. Use only pointer operations, no arrays.
Expected output: 5 4 3 2 1
💡 Solution (click to expand)
Idea: Three-pointer method—prev, cur, next. Each step points cur->next back to prev, then shifts all three pointers right.
#include <bits/stdc++.h>
using namespace std;
struct Node {
int data;
Node* next;
};
int main() {
// Build 1→2→3→4→5
Node* head = nullptr;
for (int i = 5; i >= 1; i--) { // head-insert in reverse to get ascending order
head = new Node{i, head};
}
// Reverse
Node* prev = nullptr;
Node* cur = head;
while (cur != nullptr) {
Node* next = cur->next; // 1. save the next node first
cur->next = prev; // 2. point the current node back to prev
prev = cur; // 3. advance prev
cur = next; // 4. advance cur
}
head = prev; // prev becomes the new head
// Print
for (Node* p = head; p; p = p->next) cout << p->data << " ";
cout << "\n";
return 0;
}
Key points:
- The crux of reversal is "back up the next node with
nextfirst, then changecur->next"; the order must not be wrong, or the chain breaks and you lose the rest - The three pointers
prev / cur / nextshift right together, O(N) time and O(1) extra space - This problem is the "litmus test" for understanding pointer operations—be sure to draw it out and walk through by hand
Challenge 2.7.8 — Detect a Cycle in a Linked List (Classic fast-slow pointer application)
Given a possibly cyclic linked list, determine whether it has a cycle. Print YES or NO.
Hint: Use "fast-slow pointers"—the slow pointer moves 1 step at a time, the fast pointer 2 steps. If they meet, there is a cycle (this is Floyd's cycle-finding algorithm).
💡 Solution (click to expand)
Idea: Fast-slow pointers. With no cycle, the fast pointer reaches nullptr first; with a cycle, the fast pointer catches up to the slow one inside the loop.
#include <bits/stdc++.h>
using namespace std;
struct Node {
int data;
Node* next;
};
bool hasCycle(Node* head) {
Node* slow = head;
Node* fast = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next; // slow: 1 step
fast = fast->next->next; // fast: 2 steps
if (slow == fast) return true; // they meet → cycle
}
return false; // fast reached the end → no cycle
}
int main() {
// Build 1→2→3→4, then make 4 point back to 2 to form a cycle
Node* n1 = new Node{1, nullptr};
Node* n2 = new Node{2, nullptr};
Node* n3 = new Node{3, nullptr};
Node* n4 = new Node{4, nullptr};
n1->next = n2; n2->next = n3; n3->next = n4;
n4->next = n2; // create a cycle!
cout << (hasCycle(n1) ? "YES" : "NO") << "\n"; // YES
return 0;
}
Key points:
- The loop condition
fast && fast->nextmust double-check for null—becausefastmoves two steps, you must ensurefast->next->nextdoes not run past a null pointer - Fast-slow pointers are a general tool for cycle detection, finding the middle, and finding the K-th node from the end
- With a cycle, the fast pointer gains 1 step on the slow one each round, and must eventually meet it inside the loop—O(N) time, O(1) space
Challenge 2.7.9 — Cows Lining Up (USACO Bronze style) (Simulate dynamic queue-jumping with a linked list + pointers)
Farmer John's N cows line up in a queue (numbered 1..N, front to back). Then M events occur, each of the form x y: cow number x cuts in directly behind cow number y (x and y differ; x is currently in the queue and is first removed from its old spot, then inserted). After all events, print the whole queue from front to back.
Sample input:
4 2
2 4
1 3
(initial queue 1 2 3 4; event 1: 2 behind 4 → 1 3 4 2; event 2: 1 behind 3 → 3 1 4 2)
Sample output: 3 1 4 2
💡 Solution (click to expand)
Idea: Frequent "remove + insert" is exactly what a doubly linked list excels at—it does deletion and insertion anywhere in O(1), whereas an array needs O(N) shifting. Here we use arrays to simulate a doubly linked list (the contest-mainstream style, echoing the comparison table in 2.7.7): pre[i] and nxt[i] store the left/right neighbor of number i, with sentinel 0 marking before-front / after-back.
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int pre[N], nxt[N]; // pre[i]/nxt[i]: predecessor/successor of number i; 0 is the sentinel
void detach(int x) { // detach x from its current position
nxt[pre[x]] = nxt[x];
pre[nxt[x]] = pre[x];
}
void insertAfter(int y, int x) { // insert x directly behind y
int z = nxt[y]; // y's original successor
nxt[y] = x; pre[x] = y;
nxt[x] = z; pre[z] = x;
}
int main() {
int n, m;
cin >> n >> m;
// initial queue 1 2 ... n, stitched at both ends with sentinel 0
pre[1] = 0; nxt[0] = 1; // 0 → 1
for (int i = 1; i < n; i++) { nxt[i] = i + 1; pre[i + 1] = i; }
nxt[n] = 0; pre[0] = n; // n → 0 (back to sentinel = end of queue)
while (m--) {
int x, y;
cin >> x >> y;
detach(x); // first remove x from its old spot
insertAfter(y, x); // then insert it behind y
}
// start at the first node after the sentinel, follow nxt to print the queue
for (int i = nxt[0]; i != 0; i = nxt[i]) {
cout << i << (nxt[i] ? " " : "\n");
}
return 0;
}
Key points:
- Why a linked list, not an array? Array cutting-in shifts all following elements, O(N) per op, O(NM) total; a doubly linked list does both delete and insert in O(1), O(N+M) total—a huge difference when N, M reach 10^5
- Sentinel 0 removes front/back special cases:
nxt[0]is the real front,pre[0]is the real back - Using the number itself as the index (the number IS the "pointer") is the classic array-simulated-list usage—exactly matching the comparison table in 2.7.7
detachmust come beforeinsertAfter, otherwise x's old neighbor links are not yet detached
Problem 2.7.10 — Hand-Write myStrlen with Pointers (Classic exercise in traversing C-style strings with pointers)
Without using any library functions, implement int myStrlen(const char* s) using pure pointer operations. Return the string length (excluding \0). Test in main.
Expected output (input "hello"): 5
💡 Solution (click to expand)
Idea: Start from the first character, advance character by character, stop at \0. The number of steps taken is the length.
#include <bits/stdc++.h>
using namespace std;
int myStrlen(const char* s) {
int len = 0;
while (*s != '\0') { // equivalent to while (*s)
len++;
s++; // advance pointer to next character
}
return len;
}
int main() {
const char* s = "hello";
cout << myStrlen(s) << "\n"; // 5
return 0;
}
Key points:
*sdereferences to get the current character;s++advances the pointer (pointer arithmetic)- The loop condition
*s != '\0'—C-style strings are terminated by\0 - This exercise lets you feel that "pointer + loop = string operation" is the essence
Problem 2.7.11 — Hand-Write myStrcpy with Pointers (Classic exercise in pointer-based assignment)
Without using any library functions, implement void myStrcpy(char* dst, const char* src) using pure pointer operations. Copy the string pointed to by src into dst. Test in main.
Expected output (input "world"): world
💡 Solution (click to expand)
Idea: Two pointers advance in sync, copying character by character, and finally manually append \0.
#include <bits/stdc++.h>
using namespace std;
void myStrcpy(char* dst, const char* src) {
while (*src != '\0') {
*dst = *src; // copy character by character
dst++;
src++;
}
*dst = '\0'; // don't forget the trailing '\0'!
}
// Compact version (read after understanding the principle):
void myStrcpyV2(char* dst, const char* src) {
while ((*dst++ = *src++)); // assign + check + advance, all in one
}
int main() {
const char* src = "world";
char dst[100];
myStrcpy(dst, src);
cout << dst << "\n"; // world
return 0;
}
Key points:
const char* src—the source isconst-qualified to prevent accidental modification- You must manually append
\0, otherwisedstis not a valid C string - The compact
(*dst++ = *src++)uses postfix++: first assign, then advance; the loop stops when the assignment result is\0
Problem 2.7.12 — Merge Two Sorted Linked Lists (Classic pointer operation, high-frequency interview problem)
Given two ascending linked lists L1 and L2, merge them into a single ascending linked list and return the head pointer. Only modify pointers—do not create new nodes (no new).
Sample input (two lists): 1→3→5 and 2→4→6
Sample output: 1 2 3 4 5 6
💡 Solution (click to expand)
Idea: Use a "sentinel" dummy node to simplify head handling, and maintain a tail pointer. Compare the current heads of both lists each iteration, detach the smaller one and attach it after tail, then advance the source list's head. Finally, attach whichever list remains.
#include <bits/stdc++.h>
using namespace std;
struct Node {
int data;
Node* next;
};
Node* mergeTwoLists(Node* l1, Node* l2) {
Node dummy{0, nullptr}; // dummy node simplifies head handling
Node* tail = &dummy; // tail always points to the end of the result list
while (l1 != nullptr && l2 != nullptr) {
if (l1->data < l2->data) {
tail->next = l1; // detach l1's current node
l1 = l1->next; // advance l1
} else {
tail->next = l2; // detach l2's current node
l2 = l2->next; // advance l2
}
tail = tail->next; // tail stays at the end
}
// Attach the remaining list (at most one is non-empty)
tail->next = (l1 != nullptr) ? l1 : l2;
return dummy.next; // skip the dummy, return the real head
}
int main() {
// Build L1: 1→3→5
Node* l1 = new Node{1, new Node{3, new Node{5, nullptr}}};
// Build L2: 2→4→6
Node* l2 = new Node{2, new Node{4, new Node{6, nullptr}}};
Node* merged = mergeTwoLists(l1, l2);
for (Node* p = merged; p; p = p->next)
cout << p->data << " ";
cout << "\n"; // 1 2 3 4 5 6
return 0;
}
Key points:
- The dummy node is a classic linked-list trick—it avoids special-casing when the head is empty
tailalways points to the last node of the result list, so attaching a new node is O(1)- The core of the pointer operation: after changing
tail->next, you must synchronously updatetailand the source list's head - This is the fundamental sub-operation for merge and merge sort on linked lists
Problem 2.7.13 — Find the Middle of a Linked List (Another classic fast-slow pointer application)
Given a singly linked list, use fast-slow pointers to find its middle node. If the length is even, return the second middle node.
Sample input: 1→2→3→4→5
Sample output: 3 (the 3rd node is the middle)
Sample input: 1→2→3→4
Sample output: 3 (even length, return the second of the two middles)
💡 Solution (click to expand)
Idea: The slow pointer moves 1 step at a time, the fast pointer 2 steps. When the fast pointer reaches the end, the slow pointer is exactly at the middle.
#include <bits/stdc++.h>
using namespace std;
struct Node {
int data;
Node* next;
};
Node* findMiddle(Node* head) {
Node* slow = head;
Node* fast = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next; // slow: 1 step
fast = fast->next->next; // fast: 2 steps
}
return slow; // when fast reaches the end, slow is at the middle
}
int main() {
// Test 1: 1→2→3→4→5 (odd)
Node* head1 = new Node{1, new Node{2, new Node{3,
new Node{4, new Node{5, nullptr}}}}};
cout << findMiddle(head1)->data << "\n"; // 3
// Test 2: 1→2→3→4 (even)
Node* head2 = new Node{1, new Node{2, new Node{3,
new Node{4, nullptr}}}};
cout << findMiddle(head2)->data << "\n"; // 3
return 0;
}
Key points:
- Fast-slow pointers are a universal tool for linked list "middle," "K-th from end," and "cycle detection"—O(N) time, O(1) space
- The loop condition
fast && fast->next—since fast moves two steps, both null-checks are essential - When
fast == nullptr(odd length) orfast->next == nullptr(even length), the loop exits andslowis exactly at the middle - To get the first middle node (for even length), let fast start one step ahead:
fast = head->next
🔗 USACO Practice Problems
The pointer / linked-list ideas in this chapter map directly to the following official USACO problems. Try implementing them with this chapter's "pointer version" or "array-simulation version" first, then submit on usaco.org to verify:
| Problem | Difficulty | Topic | Connection to this chapter |
|---|---|---|---|
| Reordering the Cows (USACO 2014 March Bronze P1) | 🟢 Bronze | permutation cycles / following "pointers" | Treat A[i] as "i points to A[i]" and hop along indices—exactly the "array index is a pointer" idea from 2.7.4, and the array version of Floyd's cycle finding |
| Cow Line (USACO 2009 Open Silver) | 🟡 Silver | doubly linked list / deque simulation | Frequent insert/remove at both ends—a beefed-up version of 2.7.9 "Cows Lining Up"; use an array-simulated doubly linked list, or deque directly (Chapter 3.6) |
💡 Solving hints:
- Reordering the Cows: each cow must travel from position
ito its target; following "where this cow should go, and where the cow originally there should go" traces out one or more "cycles." This "hop along the pointers" traversal is the same mindset as linked-list traversal and Floyd's cycle finding.- Cow Line: both ends need O(1) insert/remove—the standard tool is a deque. Having understood this chapter's doubly linked list (the
pre/nxt"pointers" in both directions), you can see why adequeachieves O(1) at both ends internally.
📌 A more systematic problem list: USACO Guide organizes official problems by difficulty and topic (including Stacks/Queues and Linked Lists)—an authoritative roadmap for pre-contest practice.