C++ for Beginners Part 6: Pointers and Memory Management
Pointers are the most misunderstood part of C++ — and the key to understanding how memory actually works. This guide demystifies them from scratch: addresses, dereferencing, pointer arithmetic, dynamic allocation with new/delete, and how smart pointers make memory safe.
C++ for Beginners: A Complete Guide
An eight-part beginner-friendly journey through C++: from writing your first program and understanding variables, through control flow, functions, and arrays, to the heart of C++ — pointers, classes, inheritance, and polymorphism. Each article is self-contained and builds on the previous, giving you both a quick reference and a progressive path to mastery.
- 4 C++ for Beginners Part 4: Functions
- 5 C++ for Beginners Part 5: Arrays, Strings, and std::vector
- 6 C++ for Beginners Part 6: Pointers and Memory Management
- 7 C++ for Beginners Part 7: Classes and Object-Oriented Programming
- 8 C++ for Beginners Part 8: Inheritance and Polymorphism
Table of Contents
- What is memory?
- The address-of operator &
- Declaring a pointer
- The dereference operator *
- Null pointers
- Pointers and arrays
- Pointers vs references
- The stack vs the heap
- The stack
- The heap
- Dynamic memory: new and delete
- Allocating a single value
- Allocating an array
- Common memory errors
- Smart pointers (C++11) — the modern solution
- std::unique_ptr — exclusive ownership
- std::shared_ptr — shared ownership
- When to use each
- Practical example: dynamic array
- Key takeaways
What is memory?
When your program runs, the operating system gives it a region of RAM. Every byte in that region has a unique address — a number like 0x7ffd3e8c.
Variables are stored at addresses:
int age = 25;
// stored somewhere in RAM, e.g. at address 0x7ffd1234
A pointer is a variable that stores a memory address.
The address-of operator &
& retrieves the memory address of a variable:
int age = 25;
std::cout << &age; // something like 0x7ffd1234
Declaring a pointer
A pointer type is written as T* — "pointer to T":
int x = 42;
int* ptr = &x; // ptr holds the address of x
std::cout << ptr; // address: 0x7ffd1234
std::cout << *ptr; // value at that address: 42
The dereference operator *
*ptr means "go to the address stored in ptr and give me the value there":
int x = 10;
int* ptr = &x;
*ptr = 99; // change the value at the address
std::cout << x; // 99 — we changed x through the pointer
Think of a pointer as a remote control: ptr is the remote, *ptr is pressing the button that actually changes the TV.
Null pointers
A pointer that doesn't point to anything should be set to nullptr:
int* ptr = nullptr;
if (ptr == nullptr) {
std::cout << "Pointer is null
";
}
// NEVER dereference a null pointer:
// *ptr = 5; // crash: segmentation fault
Always check for nullptr before dereferencing a pointer you received from outside your code.
Pointers and arrays
Array names decay to a pointer to their first element:
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr; // p points to arr[0]
std::cout << *p; // 10 (arr[0])
std::cout << *(p+1); // 20 (arr[1])
std::cout << *(p+2); // 30 (arr[2])
This is pointer arithmetic — adding an integer to a pointer moves it forward by that many elements (not bytes). p + 1 is not address + 1; it's address + sizeof(int).
Pointers vs references
| Pointer | Reference | |
|---|---|---|
| Syntax | int* p |
int& r |
| Can be null | Yes | No (must bind to a valid object) |
| Can be reassigned | Yes | No (always refers to same object) |
Needs * to access |
Yes | No (transparent) |
Use references when possible — they're safer. Use pointers when you need optional values (nullable) or when you're managing memory manually.
The stack vs the heap
C++ has two main memory regions:
The stack
- Automatic, managed by the compiler
- Variables created inside a function live here
- Freed automatically when the function returns
- Fixed size (~1–8 MB)
void foo() {
int x = 10; // on the stack
} // x is automatically destroyed here
The heap
- Manual memory, managed by the programmer
- Allocated with
new, freed withdelete - Large (limited by available RAM)
- Lives until you explicitly free it
Dynamic memory: new and delete
Allocating a single value
int* p = new int(42); // allocate an int on the heap, initialize to 42
std::cout << *p; // 42
delete p; // free the memory
p = nullptr; // good practice: null the pointer after delete
Allocating an array
int size = 10;
int* arr = new int[size]; // heap array
for (int i = 0; i < size; i++) arr[i] = i * 2;
delete[] arr; // note: delete[] for arrays, not delete
arr = nullptr;
Common memory errors
Memory leak — forgot to delete
void leak() {
int* p = new int(100);
// forgot delete p;
// 4 bytes never returned to the OS
}
// Each call to leak() permanently loses 4 bytes
Use after free
int* p = new int(5);
delete p;
std::cout << *p; // undefined behaviour — p points to freed memory
Double free
int* p = new int(5);
delete p;
delete p; // crash — freeing already-freed memory
Smart pointers (C++11) — the modern solution
Smart pointers are wrappers that automatically delete memory when it goes out of scope. They live in <memory>.
std::unique_ptr — exclusive ownership
#include <memory>
std::unique_ptr<int> p = std::make_unique<int>(42);
std::cout << *p; // 42
// p automatically calls delete when it goes out of scope — no manual delete needed
Only one unique_ptr can own a given object. You can move ownership but not copy it:
auto p1 = std::make_unique<int>(10);
auto p2 = std::move(p1); // p1 is now null, p2 owns the int
std::shared_ptr — shared ownership
#include <memory>
auto sp1 = std::make_shared<int>(99);
auto sp2 = sp1; // both sp1 and sp2 own the int
std::cout << sp1.use_count(); // 2
// Memory is freed when the last shared_ptr goes out of scope
When to use each
unique_ptr |
shared_ptr |
|
|---|---|---|
| Ownership | One owner | Multiple owners |
| Overhead | Zero | Reference counter |
| Default choice | Yes | Only when sharing |
Rule of thumb: Default to unique_ptr. Use shared_ptr only when multiple objects genuinely need to share ownership.
Practical example: dynamic array
#include <iostream>
#include <memory>
int main() {
int size;
std::cout << "How many numbers? ";
std::cin >> size;
auto arr = std::make_unique<int[]>(size);
for (int i = 0; i < size; i++) {
std::cout << "Enter number " << (i+1) << ": ";
std::cin >> arr[i];
}
int sum = 0;
for (int i = 0; i < size; i++) sum += arr[i];
std::cout << "Sum: " << sum << "
";
std::cout << "Average: " << (double)sum / size << "
";
// arr is automatically deleted when it goes out of scope
}
Key takeaways
- Every variable lives at a memory address;
&gives you that address. - A pointer holds an address;
*dereferences it to get the value. - Always initialize pointers (to
nullptrif not yet assigned). - Heap memory (
new) must bedeleted — or you have a memory leak. - Use
std::unique_ptrandstd::shared_ptrinstead of rawnew/deletein modern C++.
Next up: object-oriented programming — classes, objects, and encapsulation.