6 Pointers and Memory Management
7 Pointers and Memory Management
Week 2, Session 2. CSS 342.
7.1 Warmup
Palindrome Number (LeetCode #9). Return true if an integer reads the same backward as forward.
class Solution {
public:
bool isPalindrome(int x) {
if (x < 0) return false;
long reversed = 0;
long original = x;
while (x > 0) {
reversed = reversed * 10 + x % 10;
x /= 10;
}
return reversed == original;
}
};We will revisit isPalindrome for vector<int>, string, and ListNode* over the next several chapters. Same logical question, different containers.
7.2 Learning objectives
- Distinguish pass-by-value from pass-by-reference and predict the output of each.
- Declare and use pointers to read, write, and navigate memory addresses.
- Allocate and correctly deallocate heap memory using
new/delete. - Identify the three most common memory errors (leak, dangling pointer, double-free) from code samples.
- Draw a stack-frame / heap diagram for a short C++ program.
7.3 Pass-by-value vs. pass-by-reference
Here is the most important code in this chapter:
void swapByValue(int a, int b) {
int tmp = a;
a = b;
b = tmp;
}
void swapByRef(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
int main() {
int x = 1, y = 2;
swapByValue(x, y);
cout << x << " " << y << endl; // 1 2 -- unchanged
swapByRef(x, y);
cout << x << " " << y << endl; // 2 1 -- swapped
}swapByValue receives copies of x and y. Changes to a and b inside the function do not propagate back. swapByRef receives aliases — a and b literally are x and y for the duration of the call.
The & in the parameter list means “reference, not copy.”
Roughly 90% of the bugs my students hit in week 3 trace back to confusing value semantics with reference semantics. Read this section twice.
7.3.1 const reference — the third option
For large objects you want to avoid copying but you do not want to allow mutation. The idiom is const T&:
double average(const vector<int>& v) { // no copy, no mutation
int sum = 0;
for (int x : v) sum += x;
return (double)sum / v.size();
}This is the default way to pass any non-trivial object. By the end of 342 you should be doing this without thinking.
7.4 Pointers
A pointer is a variable whose value is the address of another variable.
int x = 5;
int* p = &x; // p holds the address of x
cout << x << endl; // 5
cout << p << endl; // some address like 0x7ffd...
cout << *p << endl; // 5 -- dereference: "what's at that address"
*p = 10; // write through the pointer
cout << x << endl; // 10Three symbols:
int* p— declarespas a pointer to int.&x— produces the address ofx.*p— produces the value at the addressp(dereferences).
* is overloaded. int* p (in a declaration) and *p (in an expression) mean different things. In int* p = &x; the * is part of the type. In *p = 10; the * is the dereference operator.
7.4.1 nullptr
A pointer that points to “nothing” should be nullptr:
int* p = nullptr;
if (p == nullptr) cout << "p points to nothing" << endl;Older code uses NULL (a macro from C). C++11 introduced nullptr which is type-safe. Use nullptr.
7.5 Stack vs. heap
When you declare a variable inside a function, it lives on the stack. When the function returns, the stack frame is popped and the variable is gone.
When you use new, the variable lives on the heap. It persists until you call delete. No automatic cleanup. If you forget the delete, that memory leaks — your program keeps it allocated until termination.
void example() {
int local = 5; // stack: cleaned up automatically
int* heap = new int(42); // heap: lives until delete
cout << *heap << endl;
delete heap; // free the memory
} // local goes away here automatically7.5.1 Heap arrays
int* arr = new int[5]; // five ints on the heap
arr[0] = 10;
arr[1] = 20;
// ...
delete[] arr; // note the brackets!delete vs. delete[]: if you new an array, you must delete[] it. Mixing new[] with delete (or vice versa) is undefined behavior — it may work, may crash, may corrupt memory. Match them.
7.6 The three classic memory errors
7.6.1 1. Memory leak — forgetting delete
void leaky() {
int* p = new int(5);
cout << *p << endl;
} // <-- p goes out of scope but the int it pointed to is still on the heapThe 4 bytes are gone. Forever. Until your program ends. Now imagine this happening in a loop a million times.
7.6.2 2. Dangling pointer — using after delete
int* p = new int(5);
delete p;
cout << *p << endl; // UNDEFINED BEHAVIOR -- may print 5, may crashAfter delete, the memory might be reused, zeroed, or left alone. The pointer still holds the address, but the address now belongs to nobody (or to someone else). Use after free.
7.6.3 3. Double-free — delete twice
int* p = new int(5);
delete p;
delete p; // CRASH (usually)Most runtimes detect this and abort. Some do not, and you corrupt the heap.
7.7 Catching these with sanitizers
The fastest way to find memory bugs is to let the tools find them. Compile with AddressSanitizer:
g++ -Wall -std=c++17 -fsanitize=address -g main.cpp -o main
./mainWhen you have a leak, AddressSanitizer prints something like:
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 4 byte(s) in 1 object(s) allocated from:
#0 0x... in operator new[](unsigned long)
#1 0x... in main main.cpp:7
That tells you the file and line. Use this. Every. Time.
I have students who go an entire quarter without compiling with -fsanitize=address. Their P2 grading scripts will tell them they have memory leaks and they will spend hours hunting for them by reading code. The tool will find them in two seconds. Compile with sanitizers. I am not subtle about this.
7.8 A complete worked example
#include <iostream>
using namespace std;
class Student {
public:
string name;
int score;
Student(const string& n, int s) : name(n), score(s) {
// cout << "Student(" << name << ") constructed" << endl;
}
~Student() {
// cout << "Student(" << name << ") destroyed" << endl;
}
};
int main() {
Student* alice = new Student("Alice", 95);
cout << alice->name << ": " << alice->score << endl;
// arrow operator: alice->name is shorthand for (*alice).name
delete alice;
// alice is now a dangling pointer
alice = nullptr; // good practice: zero out after delete
}Two things to notice. First, alice->name is shorthand for (*alice).name. The arrow operator dereferences and accesses a member in one step. Use it whenever you have a pointer to an object. Second, setting alice = nullptr after delete makes a later accidental use crash loudly instead of silently misbehaving.
7.9 Try it
Predict the output of this program, then run it. (Do not compile with -fsanitize=address for the first run — let it look “fine”.)
#include <iostream>
using namespace std;
int main() {
int* arr = new int[3];
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
cout << arr[0] + arr[1] + arr[2] << endl;
delete arr; // <-- bug! should be delete[]
}Then compile with -fsanitize=address. Did it catch the bug?
7.10 Self-check
delete p;, what is the safest next assignment?nullptr means a future accidental dereference will crash visibly rather than silently misbehave. Option a is a double-free.
void f(vector<int> v) vs. void f(const vector<int>& v): what changes?const T& avoids the copy and signals "I will not change this." Default to it for non-trivial parameters.
7.11 Challenges
- Remove Element (#27) — two-pointer in-place.
- Reverse String (#344) — two pointers from the ends.
- Merge Sorted Array (#88) — in-place from the back.
- Linked List Cycle (#141) — fast/slow pointers on
ListNode*.
7.12 Where this fits
Pointers are the prerequisite for classes that own memory, which is the prerequisite for the Rule of Three, which is the prerequisite for linked lists and trees. Everything from here through Chapter 18 leans on what you just learned.
| You are here | Coming up |
|---|---|
| Pass-by-ref, pointers, heap allocation, leaks/dangling/double-free | Chapter 5: classes, constructors, destructors |