Pointers and Dynamic Memory
Pointers expose the address-based side of C++. They make it possible to create objects whose lifetime is controlled explicitly, build dynamic arrays, share objects without copying them, and implement linked data structures. Savitch introduces pointers after arrays because array parameters already behave like addresses; pointers make that mechanism explicit.
The power of pointers is balanced by responsibility. A pointer can point to a valid object, point to nothing, point to an object that has already been destroyed, or be uninitialized. Correct pointer programs maintain ownership and lifetime invariants as carefully as numeric programs maintain arithmetic invariants.
Definitions
A pointer is a value that stores the address of an object. A pointer variable has a pointer type.
int value = 42;
int* ptr = &value;
The address-of operator & produces the address of an object. The dereference operator * accesses the object pointed to by a pointer.
*ptr = 100; // changes value
The same symbol * appears in several contexts:
| Syntax | Meaning |
|---|---|
int* p | declaration of pointer to int |
*p | dereference pointer p |
a * b | multiplication |
A dynamic variable is created at run time with new.
int* p = new int;
*p = 17;
The freestore or heap is the memory area used for dynamically allocated objects. Use delete to destroy a single dynamic object:
delete p;
p = nullptr;
A dynamic array is created with new[] and destroyed with delete[].
int* values = new int[count];
delete[] values;
A dangling pointer points to storage whose object has been destroyed. A memory leak occurs when dynamic storage is no longer reachable and therefore cannot be deleted.
The arrow operator -> combines dereference and member access.
struct Point {
double x;
double y;
};
Point* point = new Point{1.0, 2.0};
std::cout << point->x << '\n'; // same as (*point).x
delete point;
Key results
Pointer assignment copies an address, not the object being pointed to. After this code, p and q point to the same dynamic integer:
int* p = new int(10);
int* q = p;
*q = 20;
Now *p is also 20. There is still only one dynamic integer.
Dynamic arrays connect naturally to pointer arithmetic and array indexing. If a points to the first element, a[i] means the element i positions after a. Built-in array variables act like fixed pointer values to their first elements in many expressions, but an array variable itself cannot be reassigned.
int fixed[3] = {1, 2, 3};
int* p = fixed; // OK
// fixed = p; // not OK
When a class owns dynamic memory, the default copy behavior is usually wrong. Default memberwise copying copies pointer values, causing two objects to point to the same dynamic storage. That can cause double deletion, accidental sharing, or stale data. This is why destructors, copy constructors, and assignment operators matter.
Modern C++ often uses standard containers and smart pointers instead of raw owning pointers. Savitch's raw-pointer treatment remains essential because it explains how arrays, dynamic allocation, linked lists, and class copy semantics work underneath.
Visual
Pointer assignment copies the address:
int* p = new int(42);
int* q = p;
+-----+
p ---> | 42 |
+-----+
q -----^
*q = 99 changes the shared object:
+-----+
p ---> | 99 |
+-----+
q -----^
Worked example 1: tracing pointer assignment
Problem: Predict the output.
#include <iostream>
int main() {
int* p1 = new int;
int* p2 = new int;
*p1 = 10;
*p2 = 20;
std::cout << *p1 << " " << *p2 << '\n';
p1 = p2;
std::cout << *p1 << " " << *p2 << '\n';
*p1 = 30;
std::cout << *p1 << " " << *p2 << '\n';
delete p2;
}
Method:
- Two dynamic integers are created.
*p1 = 10,*p2 = 20.- First output is
10 20. p1 = p2copies the address stored inp2.- Now both pointers point to the integer that contains
20. - The original integer containing
10has no pointer to it, so it is leaked. - Second output is
20 20. *p1 = 30changes the object shared byp1andp2.- Third output is
30 30.
Checked answer:
10 20
20 20
30 30
The code intentionally demonstrates a leak. A production version would delete the object pointed to by p1 before overwriting p1.
Worked example 2: dynamic array average
Problem: Read n, allocate an array of n exam scores, and compute the average.
Method:
- Read
n. - Validate
n > 0. - Allocate
new double[n]. - Fill indexes
0throughn - 1. - Sum all values.
- Divide by
n. - Release with
delete[].
#include <iostream>
int main() {
int n;
std::cout << "Number of scores: ";
std::cin >> n;
if (n <= 0) {
std::cerr << "Need at least one score.\n";
return 1;
}
double* scores = new double[n];
double total = 0.0;
for (int i = 0; i < n; ++i) {
std::cout << "Score " << i + 1 << ": ";
std::cin >> scores[i];
total += scores[i];
}
double average = total / n;
std::cout << "Average: " << average << '\n';
delete[] scores;
scores = nullptr;
}
Checked answer: for inputs 3, 80, 90, 100, the total is:
and the average is:
The array is released with delete[], matching new[].
Code
This class owns a dynamic array and follows the basic rule of three: destructor, copy constructor, and assignment operator.
#include <algorithm>
#include <iostream>
class IntBuffer {
public:
explicit IntBuffer(int size)
: size_(size), data_(new int[size]) {
std::fill(data_, data_ + size_, 0);
}
IntBuffer(const IntBuffer& other)
: size_(other.size_), data_(new int[other.size_]) {
std::copy(other.data_, other.data_ + size_, data_);
}
IntBuffer& operator=(const IntBuffer& rhs) {
if (this == &rhs) {
return *this;
}
int* newData = new int[rhs.size_];
std::copy(rhs.data_, rhs.data_ + rhs.size_, newData);
delete[] data_;
data_ = newData;
size_ = rhs.size_;
return *this;
}
~IntBuffer() {
delete[] data_;
}
int& operator[](int index) {
return data_[index];
}
int size() const {
return size_;
}
private:
int size_;
int* data_;
};
int main() {
IntBuffer a(3);
a[0] = 10;
a[1] = 20;
a[2] = 30;
IntBuffer b = a;
b[1] = 99;
std::cout << a[1] << " " << b[1] << '\n';
}
Common pitfalls
- Dereferencing an uninitialized pointer.
- Using a pointer after
delete. - Using
deletefor memory allocated withnew[], ordelete[]for memory allocated withnew. - Losing the only pointer to dynamic memory and leaking it.
- Assuming pointer assignment copies the pointed-to object.
- Forgetting self-assignment checks in assignment operators that delete old storage.
- Returning a pointer to a local automatic variable.
- Writing outside a dynamic array because the allocated size is not tracked.
Ownership checks:
- Write down who owns each dynamically allocated object. If the answer is "several parts of the program," the design is probably ambiguous unless shared ownership is deliberately modeled.
- Initialize pointers immediately. A pointer should either hold a valid address or
nullptr; an uninitialized pointer contains an indeterminate address and must not be dereferenced or deleted. - After
delete, set the pointer tonullptrwhen the pointer will remain in scope and might be tested later. This does not fix all aliasing problems, but it prevents accidental reuse through that variable. - Prefer one allocation and one deallocation path. Functions with many early returns are safer when dynamic storage is owned by a class object rather than manually released at every exit.
- Distinguish pointer equality from value equality.
p == qasks whether two pointers hold the same address;*p == *qasks whether the pointed-to values compare equal. - For dynamic arrays, store the size next to the pointer in the owning object. A raw pointer alone does not remember how many elements were allocated.
- Use standard containers when the array size is the only dynamic feature. A
vector<int>usually expresses a growable array more safely thanint*plus manual capacity management.
Quick self-test: replace every raw pointer in a small program with the phrase "address of ...". If the phrase is not meaningful, the pointer probably does not have a clear role. For example, int* scores might be "address of the first element of a dynamic array of scores," but that sentence also reveals that the size must be stored somewhere else.
When reviewing a function, pair every allocation with the cleanup path that owns it. If the allocation happens in one function and deletion in another, document that transfer clearly or wrap the resource in an object whose destructor makes the transfer unnecessary.
A final review question is whether the pointer expresses observation or ownership. Observing pointers may point to objects owned elsewhere and must not delete them. Owning pointers are responsible for cleanup and should usually be wrapped in a class so destruction is automatic.
Extended practice: draw memory as two regions, stack objects and dynamic objects. Local pointer variables live on the stack, while objects created with new live in dynamic storage. Deleting the dynamic object does not remove the pointer variable; it only makes the stored address unsafe to use.