C++ Pointers and References — Smart Pointers Guide | Tutorials Logic
What is a Pointer?
A pointer is a variable that stores the memory address of another variable. Every variable in C++ lives at a specific address in RAM — a pointer lets you store and manipulate that address directly. This is one of C++'s most powerful (and dangerous) features.
Three key operators: & (address-of), * (dereference — get value at address), and -> (member access through pointer).
| Concept | Syntax | Meaning |
|---|---|---|
| Declare pointer | int *ptr; | ptr can hold address of an int |
| Get address | ptr = &x; | Store address of x in ptr |
| Dereference | *ptr | Access value at the address ptr holds |
| Null pointer | ptr = nullptr; | Pointer points to nothing (safe) |
| Member via pointer | ptr->member | Equivalent to (*ptr).member |
#include <iostream>
using namespace std;
int main() {
int x = 42;
int *ptr = &x; // ptr stores the address of x
cout << "Value of x: " << x << endl; // 42
cout << "Address of x: " << &x << endl; // e.g. 0x7fff...
cout << "ptr holds: " << ptr << endl; // same address
cout << "*ptr (deref): " << *ptr << endl; // 42
// Modify x through the pointer
*ptr = 100;
cout << "x after *ptr=100: " << x << endl; // 100
// nullptr — safe null pointer (C++11, replaces NULL)
int *p = nullptr;
if (p == nullptr) cout << "p is null" << endl;
// Pointer to different types
double d = 3.14;
double *dp = &d;
cout << *dp << endl; // 3.14
// void* — generic pointer, can hold any address
void *vp = &x;
cout << *static_cast<int*>(vp) << endl; // 100
return 0;
}
Pointer Arithmetic
When you add or subtract an integer from a pointer, it moves by sizeof(type) bytes, not by 1 byte. This is how arrays and pointers are connected — an array name is essentially a pointer to its first element.
#include <iostream>
using namespace std;
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *ptr = arr; // points to arr[0]
// Pointer arithmetic moves by sizeof(type) bytes
cout << *ptr << endl; // 10 (arr[0])
cout << *(ptr + 1) << endl; // 20 (arr[1])
cout << *(ptr + 2) << endl; // 30 (arr[2])
// Increment pointer
ptr++;
cout << *ptr << endl; // 20
// Pointer difference
int *start = arr;
int *end = arr + 5;
cout << "Elements: " << (end - start) << endl; // 5
// Traverse array using pointer
ptr = arr; // reset
for (int i = 0; i < 5; i++) {
cout << *(ptr + i) << " ";
}
cout << endl; // 10 20 30 40 50
// Comparison
int *p1 = arr;
int *p2 = arr + 3;
cout << (p1 < p2) << endl; // 1 (true)
return 0;
}
const Pointers — Three Combinations
The placement of const relative to * determines what is constant — the value being pointed to, the pointer itself, or both. This is a common source of confusion.
| Declaration | Pointer can change? | Value can change? | Use case |
|---|---|---|---|
const int *p | Yes | No | Read-only access to data |
int * const p | No | Yes | Fixed pointer, mutable data |
const int * const p | No | No | Fully immutable |
#include <iostream>
using namespace std;
int main() {
int x = 10, y = 20;
// 1. Pointer to const — cannot change the VALUE through pointer
const int *pc = &x;
// *pc = 99; // ERROR: cannot modify value
pc = &y; // OK: can change what pointer points to
cout << *pc << endl; // 20
// 2. Const pointer — cannot change WHERE pointer points
int * const cp = &x;
*cp = 99; // OK: can modify value
// cp = &y; // ERROR: cannot reseat pointer
cout << *cp << endl; // 99
// 3. Const pointer to const — cannot change either
const int * const cpc = &x;
// *cpc = 5; // ERROR
// cpc = &y; // ERROR
cout << *cpc << endl; // 99
// Common use: pass array to function read-only
auto printArr = [](const int *arr, int n) {
for (int i = 0; i < n; i++) cout << arr[i] << " ";
cout << endl;
};
int nums[] = {1, 2, 3, 4, 5};
printArr(nums, 5);
return 0;
}
Pointer to Pointer (Double Pointer)
A pointer to a pointer stores the address of another pointer. This is used for dynamic 2D arrays, modifying pointer arguments in functions, and working with C-style string arrays.
#include <iostream>
using namespace std;
int main() {
int x = 42;
int *ptr = &x; // pointer to int
int **pptr = &ptr; // pointer to pointer to int
cout << x << endl; // 42
cout << *ptr << endl; // 42
cout << **pptr << endl; // 42
// Modify x through double pointer
**pptr = 100;
cout << x << endl; // 100
// Practical use: dynamic 2D array
int rows = 3, cols = 4;
int **matrix = new int*[rows];
for (int i = 0; i < rows; i++) {
matrix[i] = new int[cols];
for (int j = 0; j < cols; j++)
matrix[i][j] = i * cols + j;
}
// Print
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++)
cout << matrix[i][j] << " ";
cout << endl;
}
// Free
for (int i = 0; i < rows; i++) delete[] matrix[i];
delete[] matrix;
return 0;
}
Function Pointers
A function pointer stores the address of a function. This enables callbacks, strategy patterns, and dispatch tables — passing behaviour as a parameter. Modern C++ prefers std::function and lambdas, but function pointers are still common in C interop and performance-critical code.
#include <iostream>
#include <functional>
using namespace std;
// Regular functions
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; }
// Function that takes a function pointer
void apply(int a, int b, int (*op)(int, int)) {
cout << "Result: " << op(a, b) << endl;
}
int main() {
// Declare function pointer: return_type (*name)(param_types)
int (*fp)(int, int) = add;
cout << fp(3, 4) << endl; // 7
fp = sub;
cout << fp(10, 3) << endl; // 7
// Pass function pointer to another function
apply(5, 3, add); // Result: 8
apply(5, 3, sub); // Result: 2
apply(5, 3, mul); // Result: 15
// Array of function pointers
int (*ops[])(int, int) = {add, sub, mul};
string names[] = {"add", "sub", "mul"};
for (int i = 0; i < 3; i++) {
cout << names[i] << "(6,2) = " << ops[i](6, 2) << endl;
}
// Modern C++: std::function (more flexible)
function<int(int,int)> f = add;
cout << f(10, 5) << endl; // 15
f = [](int a, int b) { return a * b; }; // lambda
cout << f(4, 5) << endl; // 20
return 0;
}
References — C++'s Safer Alias
A reference is an alias for an existing variable. Unlike pointers, references must be initialized when declared, cannot be null, and cannot be reseated to point to a different variable. They are the preferred way to pass large objects to functions.
| Feature | Pointer | Reference |
|---|---|---|
| Can be null | Yes (nullptr) | No — must bind to a variable |
| Can be reseated | Yes | No — always refers to same variable |
| Syntax to access | *ptr or ptr-> | Direct — no special syntax |
| Arithmetic | Yes | No |
| Use in function params | When null is valid or reseating needed | Preferred for non-null params |
| Initialized when declared | Optional | Mandatory |
#include <iostream>
#include <string>
using namespace std;
// Pass by value — copy made, original unchanged
void byValue(int x) { x *= 2; }
// Pass by reference — works on original
void byRef(int &x) { x *= 2; }
// Pass by const reference — read-only, no copy (efficient for large objects)
void printStr(const string &s) {
cout << s << endl;
}
// Return by reference (be careful — never return local variable ref!)
int& getElement(int arr[], int i) { return arr[i]; }
int main() {
int n = 10;
byValue(n);
cout << n << endl; // 10 (unchanged)
byRef(n);
cout << n << endl; // 20 (changed!)
string name = "Alice";
printStr(name); // no copy made
// Reference as alias
int x = 5;
int &ref = x;
ref = 99;
cout << x << endl; // 99 — same variable
// Return by reference — modify array element
int arr[] = {1, 2, 3, 4, 5};
getElement(arr, 2) = 99; // arr[2] = 99
cout << arr[2] << endl; // 99
// Rvalue reference (C++11) — move semantics
int &&rref = 42; // binds to temporary
cout << rref << endl; // 42
return 0;
}
Dynamic Memory — new and delete
C++ lets you allocate memory at runtime on the heap using new. You are responsible for freeing it with delete (single) or delete[] (array). Forgetting to delete causes a memory leak. In modern C++, prefer smart pointers instead.
#include <iostream>
using namespace std;
int main() {
// Allocate single value on heap
int *p = new int(42);
cout << *p << endl; // 42
delete p; // MUST free — avoids memory leak
p = nullptr; // good practice: set to null after delete
// Allocate array on heap
int n = 5;
int *arr = new int[n]{10, 20, 30, 40, 50};
for (int i = 0; i < n; i++) cout << arr[i] << " ";
cout << endl; // 10 20 30 40 50
delete[] arr; // use delete[] for arrays (not delete)
arr = nullptr;
// new with error handling
try {
int *big = new int[1000000000]; // may throw bad_alloc
delete[] big;
} catch (const bad_alloc &e) {
cout << "Allocation failed: " << e.what() << endl;
}
// nothrow version — returns nullptr on failure
int *safe = new(nothrow) int[1000000000];
if (!safe) cout << "Allocation failed (nothrow)" << endl;
return 0;
}
Smart Pointers — Modern C++ Memory Management
Smart pointers (C++11, from <memory>) wrap raw pointers and automatically free memory when the pointer goes out of scope. They eliminate memory leaks and dangling pointers without any manual delete.
| Smart Pointer | Ownership | Use when |
|---|---|---|
unique_ptr | Sole owner — cannot be copied | Single owner, most common case |
shared_ptr | Shared — reference counted | Multiple owners needed |
weak_ptr | Non-owning observer | Break circular references |
// C++11 Smart Pointers — automatic memory management
#include <iostream>
#include <memory>
#include <string>
using namespace std;
struct Resource {
string name;
Resource(string n) : name(n) { cout << "Created: " << name << endl; }
~Resource() { cout << "Destroyed: " << name << endl; }
void use() { cout << "Using: " << name << endl; }
};
int main() {
// unique_ptr — sole owner, auto-deleted when out of scope
{
auto up = make_unique<Resource>("UniqueRes");
up->use();
// up2 = up; // ERROR: cannot copy unique_ptr
auto up2 = move(up); // OK: transfer ownership
// up is now null
} // up2 destroyed here automatically
cout << "---" << endl;
// shared_ptr — reference-counted, multiple owners
{
auto sp1 = make_shared<Resource>("SharedRes");
cout << "Count: " << sp1.use_count() << endl; // 1
{
auto sp2 = sp1; // both own the resource
cout << "Count: " << sp1.use_count() << endl; // 2
sp2->use();
} // sp2 destroyed, count drops to 1
cout << "Count: " << sp1.use_count() << endl; // 1
} // sp1 destroyed, resource freed
cout << "---" << endl;
// weak_ptr — non-owning observer, breaks circular references
auto sp = make_shared<Resource>("WeakRes");
weak_ptr<Resource> wp = sp;
cout << "Expired: " << wp.expired() << endl; // 0 (false)
if (auto locked = wp.lock()) { // get shared_ptr if still alive
locked->use();
}
sp.reset(); // release ownership
cout << "Expired: " << wp.expired() << endl; // 1 (true)
return 0;
}
Common Pointer Bugs and How to Avoid Them
Pointer bugs are among the hardest to debug in C++. Here are the most common ones and their fixes.
#include <iostream>
#include <memory>
using namespace std;
int main() {
// BUG 1: Dangling pointer — pointing to freed memory
int *p = new int(42);
delete p;
// cout << *p; // UNDEFINED BEHAVIOUR — p is dangling!
p = nullptr; // FIX: set to null after delete
// BUG 2: Memory leak — forgetting to delete
// int *leak = new int(10);
// (never deleted — memory lost until program ends)
// FIX: use smart pointers
auto safe = make_unique<int>(10); // auto-freed
// BUG 3: Double delete
int *q = new int(5);
delete q;
// delete q; // UNDEFINED BEHAVIOUR — double free!
q = nullptr; // FIX: null after delete prevents double-free
// BUG 4: Buffer overflow
int arr[5] = {1,2,3,4,5};
// arr[10] = 99; // UNDEFINED BEHAVIOUR — out of bounds!
// FIX: use std::array or std::vector with at()
// BUG 5: Returning address of local variable
// int* bad() { int x = 5; return &x; } // DANGLING!
// FIX: return by value, or allocate on heap
// BUG 6: Uninitialized pointer
// int *uninit;
// *uninit = 5; // CRASH — points to random memory!
// FIX: always initialize
int *init = nullptr;
cout << "All bugs avoided!" << endl;
return 0;
}
Level Up Your C plus plus Skills
Master C plus plus with these hand-picked resources