Skip to content

C++ Pointers

Pointers are a core feature of C++, providing direct memory access and enabling capabilities like dynamic memory allocation and efficient data structures. However, raw pointers require careful handling to avoid common pitfalls like memory leaks and dangling pointers.

Every variable stored in memory has a unique memory address. You can obtain the address of a variable using the address-of operator (&).

#include <iostream>
#include <string>
int main() {
int score = 100;
std::string playerName = "Alice";
// Print the memory addresses of the variables
// The exact address values will vary each time you run the program.
std::cout << "Address of score: " << &score << std::endl;
std::cout << "Address of playerName: " << &playerName << std::endl;
// Note: For std::string, &playerName gives the address of the string object itself,
// not necessarily the start of the character data it manages internally.
return 0;
}

Sample Output (addresses will differ):

Address of score: 0x7ffc4f3b1a14
Address of playerName: 0x7ffc4f3b1a20

A pointer is a variable specifically designed to store a memory address. It “points to” the location of another piece of data.

You declare a pointer using the asterisk (*) symbol between the data type it points to and the variable name:

type* pointerName;

Examples:

int* intPtr; // Declares a pointer to an integer
double* doublePtr; // Declares a pointer to a double
std::string* strPtr; // Declares a pointer to a std::string object
char* charPtr; // Declares a pointer to a character
// It's good practice to initialize pointers:
int* safeIntPtr = nullptr; // Initialize to nullptr (the null pointer value in modern C++)

The type specified (int, double, etc.) tells the compiler what kind of data resides at the address the pointer will hold. This is crucial for dereferencing.

The primary operations involving pointers are:

  1. Assignment: Storing the address of a variable in the pointer using the address-of operator (&).
  2. Dereferencing: Accessing the value at the address stored in the pointer using the dereference operator (*).
#include <iostream>
#include <string>
int main() {
int score = 95;
int* scorePtr = nullptr; // Declare and initialize pointer
// 1. Assignment: Store the address of 'score' in 'scorePtr'
scorePtr = &score;
std::cout << "Value of score: " << score << std::endl;
std::cout << "Address stored in scorePtr: " << scorePtr << std::endl; // Prints the memory address
std::cout << "Address of score variable: " << &score << std::endl; // Should be the same address
// 2. Dereferencing: Access the value pointed to by scorePtr
std::cout << "Value pointed to by scorePtr (*scorePtr): " << *scorePtr << std::endl;
// Modify the original variable's value through the pointer
*scorePtr = 100;
std::cout << "New value of score (after *scorePtr = 100): " << score << std::endl;
return 0;
}

Output (addresses will differ):

Value of score: 95
Address stored in scorePtr: 0x7ffeeabc1a0c
Address of score variable: 0x7ffeeabc1a0c
Value pointed to by scorePtr (*scorePtr): 95
New value of score (after *scorePtr = 100): 100

A pointer that doesn’t point to any valid memory location is called a null pointer. In modern C++ (C++11 and later), the keyword nullptr should be used to represent null pointers. It’s type-safe and preferred over the older NULL macro or literal 0.

int* ptr1 = nullptr;
// double* ptr2 = NULL; // Old style, avoid
// char* ptr3 = 0; // Old style, avoid
if (ptr1 == nullptr) {
std::cout << "ptr1 is a null pointer." << std::endl;
}
// Dereferencing a null pointer leads to undefined behavior (often a crash!)
// *ptr1 = 10; // DANGER! CRASH!

Always check if a pointer is nullptr before dereferencing it if there’s a possibility it might be null.

Dynamic Memory Allocation (new and delete)

Section titled “Dynamic Memory Allocation (new and delete)”

Pointers are essential for dynamic memory allocation, where memory is requested from the operating system at runtime (from the heap or free store), rather than being allocated automatically on the stack.

The new operator allocates memory and returns a pointer to it. The delete operator releases memory allocated with new. For arrays allocated with new[], you must use delete[].

// Allocate a single integer on the heap
int* heapIntPtr = new int;
*heapIntPtr = 42;
// Allocate an array of 10 doubles on the heap
double* heapDoubleArray = new double[10];
heapDoubleArray[0] = 3.14;
std::cout << "Heap integer: " << *heapIntPtr << std::endl;
std::cout << "First element of heap array: " << heapDoubleArray[0] << std::endl;
// CRUCIAL: Release the allocated memory
delete heapIntPtr; // Use delete for single objects
delete[] heapDoubleArray; // Use delete[] for arrays
heapIntPtr = nullptr; // Good practice to nullify pointers after delete
heapDoubleArray = nullptr;

Problem: Manually managing memory with new and delete is error-prone. Forgetting delete causes memory leaks. Using memory after delete (a dangling pointer) causes undefined behavior. Incorrectly using delete instead of delete[] (or vice-versa) is also undefined behavior.

To manage dynamic memory safely and automatically, modern C++ strongly advocates using smart pointers, defined in the <memory> header. They follow the RAII (Resource Acquisition Is Initialization) principle, automatically releasing the managed memory when the smart pointer goes out of scope.

  • std::unique_ptr: Represents exclusive ownership of a dynamically allocated object. Only one unique_ptr can own the object at a time. When the unique_ptr is destroyed (e.g., goes out of scope), it automatically deletes the managed object. It’s lightweight and the preferred choice by default.
  • std::shared_ptr: Represents shared ownership. Multiple shared_ptr instances can point to the same object. The object is deleted only when the last shared_ptr pointing to it is destroyed. It uses reference counting internally.
  • std::weak_ptr: A non-owning pointer that holds a ‘weak’ reference to an object managed by std::shared_ptr. It doesn’t affect the reference count and can be used to break circular dependencies between shared_ptr instances.
#include <iostream>
#include <memory> // Required for smart pointers
#include <vector>
class MyResource {
public:
MyResource() { std::cout << "Resource Acquired\n"; }
~MyResource() { std::cout << "Resource Released\n"; } // Destructor
void use() { std::cout << "Using Resource\n"; }
};
int main() {
// Using unique_ptr (preferred for single ownership)
{
auto uniqueRes = std::make_unique<MyResource>(); // Preferred way to create unique_ptr
uniqueRes->use();
} // uniqueRes goes out of scope here, ~MyResource() is called automatically
std::cout << "----\n";
// Using shared_ptr (for shared ownership)
std::shared_ptr<MyResource> sharedRes1;
{
auto sharedRes = std::make_shared<MyResource>(); // Preferred way to create shared_ptr
sharedRes1 = sharedRes; // Both pointers now share ownership
std::cout << "Use count: " << sharedRes1.use_count() << std::endl;
sharedRes->use();
} // sharedRes goes out of scope, but sharedRes1 still exists
std::cout << "Use count after inner scope: " << sharedRes1.use_count() << std::endl;
} // sharedRes1 goes out of scope here, resource is released

Output:

Resource Acquired
Using Resource
Resource Released
----
Resource Acquired
Use count: 2
Using Resource
Use count after inner scope: 1
Resource Released

Guideline: Prefer smart pointers (std::unique_ptr, std::shared_ptr) over raw pointers (new/delete) for managing dynamic memory ownership in modern C++.

ConceptDescription
Pointer ArithmeticBasic arithmetic (+, -, ++, —) can be performed on pointers, typically used with arrays. ptr + n moves the pointer n elements forward (considering the size of the pointed-to type). Use with caution; prefer iterators or range-based loops with containers.
Pointers and ArraysAn array name often decays into a pointer to its first element in expressions. int arr[10]; int* p = arr; makes p point to arr[0]. *(p + i) is equivalent to arr[i].
Array of PointersYou can declare arrays where each element is a pointer: std::string* strArray[5];
Pointer to PointerA pointer can point to another pointer: int x = 10; int* ptr1 = &x; int** ptr2 = &ptr1;. **ptr2 accesses x.
Pointers and FunctionsPointers can be passed to functions to allow the function to modify the original variable (pass-by-pointer). Returning pointers from functions requires care, especially with dynamically allocated memory (ownership transfer is critical – smart pointers help).
Function PointersPointers can also store the address of functions, allowing functions to be passed as arguments or stored.
const and Pointersconst can apply to the data pointed to (const int* p) or the pointer itself (int* const p), or both (const int* const p). Understanding const-correctness with pointers is vital.

While powerful, raw pointers require discipline. Modern C++ practices emphasize safer alternatives like references, standard library containers, and smart pointers whenever possible.