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

  1. Distinguish pass-by-value from pass-by-reference and predict the output of each.
  2. Declare and use pointers to read, write, and navigate memory addresses.
  3. Allocate and correctly deallocate heap memory using new/delete.
  4. Identify the three most common memory errors (leak, dangling pointer, double-free) from code samples.
  5. 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 aliasesa 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;     // 10

Three symbols:

  • int* p — declares p as a pointer to int.
  • &x — produces the address of x.
  • *p — produces the value at the address p (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 automatically

7.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 heap

The 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 crash

After 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
./main

When 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

1. After delete p;, what is the safest next assignment?
b. Setting to nullptr means a future accidental dereference will crash visibly rather than silently misbehave. Option a is a double-free.
2. void f(vector<int> v) vs. void f(const vector<int>& v): what changes?
d. const T& avoids the copy and signals "I will not change this." Default to it for non-trivial parameters.

7.11 Challenges

  1. Remove Element (#27) — two-pointer in-place.
  2. Reverse String (#344) — two pointers from the ends.
  3. Merge Sorted Array (#88) — in-place from the back.
  4. 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