Module 3.2

Pointers in C++

Unlock the power of direct memory access with C++ pointers. Learn pointer declaration, dereferencing, pointer arithmetic, and dynamic memory allocation to write efficient, low-level code.

50 min read
Intermediate
Hands-on Examples
What You'll Learn
  • Pointer declaration & initialization
  • Dereferencing & address-of operator
  • Pointer arithmetic
  • Pointers with arrays & functions
  • Dynamic memory (new/delete)
Contents
01

Introduction to Pointers

Pointers are one of the most powerful features in C++. They store memory addresses rather than values directly, allowing you to manipulate memory, create dynamic data structures, and write highly efficient code.

Concept

Pointer

A pointer is a variable that stores the memory address of another variable. Instead of holding data directly, it "points to" where data is stored in memory.

Key Operators: & (address-of) and * (dereference)

Why Use Pointers?

Direct Memory

Access and manipulate memory directly

Data Structures

Build linked lists, trees, graphs

Efficiency

Pass large data without copying

Dynamic Memory

Allocate memory at runtime

Memory Visualization

#include <iostream>
using namespace std;

int main() {
    int value = 42;        // Variable holding value 42
    int* ptr = &value;     // Pointer holding address of value
    
    // Memory visualization:
    // ┌─────────────────────────────────────────┐
    // │ Address    │ Variable │ Value           │
    // ├─────────────────────────────────────────┤
    // │ 0x1000     │ value    │ 42              │
    // │ 0x1008     │ ptr      │ 0x1000 (address)│
    // └─────────────────────────────────────────┘
    
    cout << "value = " << value << endl;           // 42
    cout << "Address of value: " << &value << endl; // 0x1000 (example)
    cout << "ptr = " << ptr << endl;                // 0x1000
    cout << "*ptr = " << *ptr << endl;              // 42
    
    return 0;
}
02

Pointer Declaration & Initialization

To declare a pointer, use the asterisk (*) after the data type. To get the address of a variable, use the address-of operator (&).

#include <iostream>
using namespace std;

int main() {
    // Pointer declaration syntax: type* pointerName;
    int* intPtr;       // Pointer to int
    double* dblPtr;    // Pointer to double
    char* charPtr;     // Pointer to char
    
    // Initialize with address of a variable
    int num = 100;
    intPtr = #     // intPtr now holds address of num
    
    // Declare and initialize in one line
    int value = 50;
    int* ptr = &value;
    
    // Style variations (all equivalent)
    int *p1;           // * next to variable name
    int* p2;           // * next to type (preferred)
    int * p3;          // * with spaces
    
    cout << "num = " << num << endl;           // 100
    cout << "Address: " << intPtr << endl;     // Memory address
    cout << "Value at address: " << *intPtr << endl;  // 100
    
    return 0;
}

Multiple Pointer Declarations

#include <iostream>
using namespace std;

int main() {
    // Careful! Only first variable is a pointer
    int* p1, p2;       // p1 is int*, p2 is int (NOT a pointer!)
    
    // To declare multiple pointers:
    int *ptr1, *ptr2;  // Both are int pointers
    
    // Or better, declare separately:
    int* ptrA;
    int* ptrB;
    
    // Pointer to pointer (double pointer)
    int value = 10;
    int* ptr = &value;
    int** pptr = &ptr;   // Pointer to pointer
    
    cout << "value = " << value << endl;      // 10
    cout << "*ptr = " << *ptr << endl;        // 10
    cout << "**pptr = " << **pptr << endl;    // 10
    
    return 0;
}

Practice Questions: Declaration

Task: Declare a double variable with value 3.14, then create a pointer that points to it.

Show Solution
double pi = 3.14;
double* ptr = π

Task: Create an int variable, a pointer to it, and a pointer to that pointer. Print the value using the double pointer.

Show Solution
int num = 42;
int* ptr = #
int** pptr = &ptr;

cout << **pptr << endl;  // 42
03

Dereferencing Pointers

Dereferencing means accessing the value at the memory address stored in a pointer. Use the dereference operator (*) to read or modify the value.

#include <iostream>
using namespace std;

int main() {
    int num = 25;
    int* ptr = #
    
    // Reading value through pointer (dereferencing)
    cout << "Value: " << *ptr << endl;  // 25
    
    // Modifying value through pointer
    *ptr = 100;
    cout << "num is now: " << num << endl;  // 100
    
    // Both num and *ptr refer to the same memory location
    num = 200;
    cout << "*ptr is now: " << *ptr << endl;  // 200
    
    return 0;
}

The Two Meanings of *

#include <iostream>
using namespace std;

int main() {
    // * in declaration: creates a pointer type
    int* ptr;          // ptr is a pointer to int
    
    int value = 50;
    ptr = &value;
    
    // * in expression: dereferences (gets value at address)
    cout << *ptr << endl;  // 50 (dereferencing)
    
    // Summary:
    // int* ptr   → Declaration: ptr is a pointer to int
    // *ptr       → Expression: value at address stored in ptr
    // &value     → Expression: address of value
    
    return 0;
}

Swapping Values Using Pointers

#include <iostream>
using namespace std;

void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 10, y = 20;
    
    cout << "Before: x=" << x << ", y=" << y << endl;
    
    swap(&x, &y);  // Pass addresses
    
    cout << "After: x=" << x << ", y=" << y << endl;
    // Output: x=20, y=10
    
    return 0;
}

Practice Questions: Dereferencing

Task: Create an integer with value 5, point to it, and double its value using the pointer.

Show Solution
int num = 5;
int* ptr = #
*ptr = *ptr * 2;  // or *ptr *= 2;

cout << num << endl;  // 10

Task: Write a function that takes two int pointers and returns a pointer to the larger value.

Show Solution
int* larger(int* a, int* b) {
    if (*a > *b) return a;
    return b;
}

// Usage:
int x = 10, y = 20;
int* result = larger(&x, &y);
cout << *result << endl;  // 20
04

Pointer Arithmetic

Pointers support arithmetic operations. When you add 1 to a pointer, it moves to the next element of its type (not just one byte). This is essential for working with arrays.

#include <iostream>
using namespace std;

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int* ptr = arr;  // Points to first element
    
    cout << "ptr points to: " << *ptr << endl;    // 10
    
    ptr++;  // Move to next int (advances by sizeof(int) bytes)
    cout << "After ptr++: " << *ptr << endl;      // 20
    
    ptr += 2;  // Move forward by 2 elements
    cout << "After ptr+=2: " << *ptr << endl;     // 40
    
    ptr--;  // Move back one element
    cout << "After ptr--: " << *ptr << endl;      // 30
    
    return 0;
}

Pointer Arithmetic Operations

#include <iostream>
using namespace std;

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int* p1 = &arr[0];
    int* p2 = &arr[3];
    
    // Pointer + integer: moves pointer forward
    cout << *(p1 + 2) << endl;  // 30 (arr[0+2])
    
    // Pointer - integer: moves pointer backward
    cout << *(p2 - 1) << endl;  // 30 (arr[3-1])
    
    // Pointer - pointer: number of elements between them
    cout << (p2 - p1) << endl;  // 3
    
    // Comparison
    if (p1 < p2) {
        cout << "p1 comes before p2" << endl;
    }
    
    // Size matters!
    cout << "Size of int: " << sizeof(int) << " bytes" << endl;
    // ptr+1 moves by sizeof(int) bytes, not 1 byte
    
    return 0;
}

Iterating with Pointer Arithmetic

#include <iostream>
using namespace std;

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int size = sizeof(arr) / sizeof(arr[0]);
    
    // Method 1: Index notation
    cout << "Using index: ";
    for (int i = 0; i < size; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;
    
    // Method 2: Pointer arithmetic
    cout << "Using pointer: ";
    for (int* p = arr; p < arr + size; p++) {
        cout << *p << " ";
    }
    cout << endl;
    
    // Method 3: Pointer with offset
    cout << "Using offset: ";
    int* ptr = arr;
    for (int i = 0; i < size; i++) {
        cout << *(ptr + i) << " ";
    }
    cout << endl;
    
    return 0;
}

Practice Questions: Pointer Arithmetic

Task: Given an array int arr[] = {5, 10, 15, 20, 25};, access the third element (15) using pointer arithmetic.

Show Solution
int arr[] = {5, 10, 15, 20, 25};
int* ptr = arr;

cout << *(ptr + 2) << endl;  // 15

Task: Write a function that calculates the sum of an array using only pointer arithmetic (no index notation).

Show Solution
int sumArray(int* arr, int size) {
    int sum = 0;
    int* end = arr + size;
    
    for (int* p = arr; p < end; p++) {
        sum += *p;
    }
    return sum;
}

// Usage:
int arr[] = {1, 2, 3, 4, 5};
cout << sumArray(arr, 5) << endl;  // 15
05

Pointers & Arrays

Arrays and pointers are closely related in C++. An array name can be used as a pointer to its first element, and pointer notation can access array elements.

#include <iostream>
using namespace std;

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    
    // Array name IS a pointer to first element
    cout << "arr = " << arr << endl;        // Address
    cout << "&arr[0] = " << &arr[0] << endl; // Same address!
    
    // These are equivalent:
    cout << arr[0] << endl;     // 10 (index notation)
    cout << *arr << endl;       // 10 (pointer notation)
    cout << *(arr+0) << endl;   // 10 (explicit)
    
    // These are also equivalent:
    cout << arr[2] << endl;     // 30
    cout << *(arr+2) << endl;   // 30
    
    // Important: arr is a CONSTANT pointer
    // arr = arr + 1;  // Error! Can't modify array name
    
    // But you can use a pointer variable:
    int* ptr = arr;
    ptr++;  // OK - ptr can be modified
    cout << *ptr << endl;  // 20
    
    return 0;
}

Array vs Pointer Differences

#include <iostream>
using namespace std;

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int* ptr = arr;
    
    // sizeof difference
    cout << "sizeof(arr): " << sizeof(arr) << endl;  // 20 (5 * 4 bytes)
    cout << "sizeof(ptr): " << sizeof(ptr) << endl;  // 8 (pointer size)
    
    // Can modify ptr, not arr
    ptr = ptr + 1;   // OK
    // arr = arr + 1;  // Error!
    
    // Both can be used with [] operator
    cout << ptr[0] << endl;  // 2 (ptr now points to arr[1])
    cout << arr[0] << endl;  // 1
    
    return 0;
}

Passing Arrays to Functions

#include <iostream>
using namespace std;

// Arrays decay to pointers when passed to functions
// These declarations are equivalent:
void printArray(int arr[], int size);
void printArray(int* arr, int size);

void printArray(int* arr, int size) {
    for (int i = 0; i < size; i++) {
        cout << arr[i] << " ";
        // or: cout << *(arr + i) << " ";
    }
    cout << endl;
}

void doubleElements(int* arr, int size) {
    for (int i = 0; i < size; i++) {
        arr[i] *= 2;  // Modifies original array!
    }
}

int main() {
    int nums[] = {1, 2, 3, 4, 5};
    int size = sizeof(nums) / sizeof(nums[0]);
    
    printArray(nums, size);     // 1 2 3 4 5
    
    doubleElements(nums, size);
    printArray(nums, size);     // 2 4 6 8 10
    
    return 0;
}

Practice Questions: Pointers & Arrays

Task: Print an array in reverse order using pointer arithmetic.

Show Solution
int arr[] = {1, 2, 3, 4, 5};
int size = 5;

for (int* p = arr + size - 1; p >= arr; p--) {
    cout << *p << " ";
}
// Output: 5 4 3 2 1

Task: Write a function that reverses an array in-place using two pointers.

Show Solution
void reverseArray(int* arr, int size) {
    int* start = arr;
    int* end = arr + size - 1;
    
    while (start < end) {
        // Swap
        int temp = *start;
        *start = *end;
        *end = temp;
        
        start++;
        end--;
    }
}

// Usage:
int arr[] = {1, 2, 3, 4, 5};
reverseArray(arr, 5);
// arr is now {5, 4, 3, 2, 1}
06

Pointers & Functions

Pointers allow functions to modify variables from the caller, return multiple values, and work with dynamic data. You can also create pointers to functions for callbacks and flexible code.

Returning Multiple Values

#include <iostream>
using namespace std;

// Return multiple values through pointer parameters
void getMinMax(int arr[], int size, int* min, int* max) {
    *min = *max = arr[0];
    
    for (int i = 1; i < size; i++) {
        if (arr[i] < *min) *min = arr[i];
        if (arr[i] > *max) *max = arr[i];
    }
}

void divideWithRemainder(int dividend, int divisor, int* quotient, int* remainder) {
    *quotient = dividend / divisor;
    *remainder = dividend % divisor;
}

int main() {
    int arr[] = {3, 1, 4, 1, 5, 9, 2, 6};
    int minVal, maxVal;
    
    getMinMax(arr, 8, &minVal, &maxVal);
    cout << "Min: " << minVal << ", Max: " << maxVal << endl;
    // Output: Min: 1, Max: 9
    
    int q, r;
    divideWithRemainder(17, 5, &q, &r);
    cout << "17 / 5 = " << q << " remainder " << r << endl;
    // Output: 17 / 5 = 3 remainder 2
    
    return 0;
}

Function Pointers

#include <iostream>
using namespace std;

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }

int main() {
    // Declare function pointer
    // Syntax: return_type (*pointer_name)(parameter_types)
    int (*operation)(int, int);
    
    // Point to different functions
    operation = add;
    cout << "5 + 3 = " << operation(5, 3) << endl;  // 8
    
    operation = subtract;
    cout << "5 - 3 = " << operation(5, 3) << endl;  // 2
    
    operation = multiply;
    cout << "5 * 3 = " << operation(5, 3) << endl;  // 15
    
    // Array of function pointers
    int (*ops[3])(int, int) = {add, subtract, multiply};
    cout << "Using array: " << ops[0](10, 5) << endl;  // 15
    
    return 0;
}

Callback Functions

#include <iostream>
using namespace std;

// Callback function type
typedef bool (*Predicate)(int);

// Filter function using callback
void filterArray(int arr[], int size, Predicate condition) {
    cout << "Filtered: ";
    for (int i = 0; i < size; i++) {
        if (condition(arr[i])) {
            cout << arr[i] << " ";
        }
    }
    cout << endl;
}

// Predicate functions
bool isEven(int n) { return n % 2 == 0; }
bool isPositive(int n) { return n > 0; }
bool isGreaterThan5(int n) { return n > 5; }

int main() {
    int arr[] = {-2, 1, 4, 7, -3, 8, 6, 2};
    int size = 8;
    
    filterArray(arr, size, isEven);       // -2 4 8 6 2
    filterArray(arr, size, isPositive);   // 1 4 7 8 6 2
    filterArray(arr, size, isGreaterThan5); // 7 8 6
    
    return 0;
}

Practice Questions: Pointers & Functions

Task: Write a function that takes two numbers and returns both their sum and product through pointer parameters.

Show Solution
void sumAndProduct(int a, int b, int* sum, int* product) {
    *sum = a + b;
    *product = a * b;
}

// Usage:
int s, p;
sumAndProduct(4, 5, &s, &p);
cout << "Sum: " << s << ", Product: " << p << endl;
// Sum: 9, Product: 20

Task: Create a calculate function that takes two numbers, an operator character, and uses function pointers to perform the operation.

Show Solution
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; }
int div(int a, int b) { return b != 0 ? a / b : 0; }

int calculate(int a, int b, char op) {
    int (*operation)(int, int);
    
    switch(op) {
        case '+': operation = add; break;
        case '-': operation = sub; break;
        case '*': operation = mul; break;
        case '/': operation = div; break;
        default: return 0;
    }
    
    return operation(a, b);
}

// Usage:
cout << calculate(10, 5, '+') << endl;  // 15
cout << calculate(10, 5, '*') << endl;  // 50
07

Dynamic Memory Allocation

Dynamic memory allocation lets you request memory at runtime using new and release it with delete. This is essential for creating data structures whose size is unknown at compile time.

#include <iostream>
using namespace std;

int main() {
    // Allocate single variable
    int* ptr = new int;      // Allocate memory for one int
    *ptr = 42;
    cout << "Value: " << *ptr << endl;  // 42
    delete ptr;              // Free memory
    ptr = nullptr;           // Good practice: avoid dangling pointer
    
    // Allocate with initialization
    int* num = new int(100);  // Initialize to 100
    cout << "Initialized: " << *num << endl;  // 100
    delete num;
    
    // Allocate array
    int size = 5;
    int* arr = new int[size];  // Allocate array of 5 ints
    
    for (int i = 0; i < size; i++) {
        arr[i] = (i + 1) * 10;
    }
    
    for (int i = 0; i < size; i++) {
        cout << arr[i] << " ";  // 10 20 30 40 50
    }
    cout << endl;
    
    delete[] arr;  // Use delete[] for arrays!
    arr = nullptr;
    
    return 0;
}

new vs delete Rules

Allocation Deallocation Example
new T delete ptr int* p = new int; delete p;
new T[n] delete[] ptr int* a = new int[5]; delete[] a;

Dynamic 2D Array

#include <iostream>
using namespace std;

int main() {
    int rows = 3, cols = 4;
    
    // Allocate 2D array (array of pointers)
    int** matrix = new int*[rows];
    for (int i = 0; i < rows; i++) {
        matrix[i] = new int[cols];
    }
    
    // Initialize
    int value = 1;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            matrix[i][j] = value++;
        }
    }
    
    // Print
    cout << "Matrix:" << endl;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            cout << matrix[i][j] << "\t";
        }
        cout << endl;
    }
    
    // Deallocate (reverse order!)
    for (int i = 0; i < rows; i++) {
        delete[] matrix[i];
    }
    delete[] matrix;
    
    return 0;
}
Memory Management Rules:
  • Every new must have a matching delete
  • Every new[] must have a matching delete[]
  • Never delete the same memory twice (double-free)
  • Set pointers to nullptr after deleting
  • Memory leaks occur when you forget to delete

Practice Questions: Dynamic Memory

Task: Ask user for array size, dynamically allocate, fill with values 1 to n, print, and properly deallocate.

Show Solution
int size;
cout << "Enter size: ";
cin >> size;

int* arr = new int[size];

for (int i = 0; i < size; i++) {
    arr[i] = i + 1;
}

for (int i = 0; i < size; i++) {
    cout << arr[i] << " ";
}
cout << endl;

delete[] arr;
arr = nullptr;

Task: Write a function that takes a dynamic array and its size, and returns a new array with double the size (copying old elements).

Show Solution
int* resizeArray(int* oldArr, int oldSize, int newSize) {
    int* newArr = new int[newSize];
    
    // Copy old elements
    int copySize = (oldSize < newSize) ? oldSize : newSize;
    for (int i = 0; i < copySize; i++) {
        newArr[i] = oldArr[i];
    }
    
    // Initialize remaining elements to 0
    for (int i = copySize; i < newSize; i++) {
        newArr[i] = 0;
    }
    
    delete[] oldArr;  // Free old array
    return newArr;
}

// Usage:
int* arr = new int[3]{1, 2, 3};
arr = resizeArray(arr, 3, 6);
// arr is now size 6: {1, 2, 3, 0, 0, 0}
08

Null & Void Pointers

Null pointers indicate "no address" and void pointers can point to any type. Understanding these is crucial for safe pointer usage.

Null Pointers

#include <iostream>
using namespace std;

int main() {
    // Null pointer - points to nothing
    int* ptr1 = nullptr;   // C++11 way (preferred)
    int* ptr2 = NULL;      // C-style (older)
    int* ptr3 = 0;         // Also works but not recommended
    
    // Always check before dereferencing!
    if (ptr1 != nullptr) {
        cout << *ptr1 << endl;
    } else {
        cout << "ptr1 is null" << endl;
    }
    
    // After delete, set to nullptr
    int* num = new int(42);
    delete num;
    num = nullptr;  // Prevents accidental use of dangling pointer
    
    // Check if allocation succeeded
    int* bigArray = new(nothrow) int[1000000000];
    if (bigArray == nullptr) {
        cout << "Allocation failed!" << endl;
    }
    
    return 0;
}

Void Pointers

#include <iostream>
using namespace std;

int main() {
    // void* can point to any type
    int num = 10;
    double dbl = 3.14;
    char ch = 'A';
    
    void* ptr;
    
    ptr = #
    cout << "int: " << *(static_cast<int*>(ptr)) << endl;  // 10
    
    ptr = &dbl;
    cout << "double: " << *(static_cast<double*>(ptr)) << endl;  // 3.14
    
    ptr = &ch;
    cout << "char: " << *(static_cast<char*>(ptr)) << endl;  // A
    
    // Note: Cannot dereference void* directly
    // cout << *ptr;  // Error!
    // Must cast to specific type first
    
    return 0;
}

Common Pointer Pitfalls

#include <iostream>
using namespace std;

// Pitfall 1: Uninitialized pointer
void pitfall1() {
    int* ptr;       // Garbage value!
    // *ptr = 10;   // Undefined behavior!
    
    // Fix: Initialize to nullptr or valid address
    int* safe = nullptr;
}

// Pitfall 2: Dangling pointer
void pitfall2() {
    int* ptr = new int(42);
    delete ptr;
    // *ptr = 10;  // Dangling pointer! Undefined behavior!
    
    // Fix: Set to nullptr after delete
    ptr = nullptr;
}

// Pitfall 3: Memory leak
void pitfall3() {
    int* ptr = new int(42);
    ptr = new int(100);  // Lost reference to first allocation!
    // Memory leak: can't delete first int
    
    // Fix: Delete before reassigning
    // delete ptr;
    // ptr = new int(100);
}

// Pitfall 4: Returning address of local variable
int* pitfall4() {
    int local = 42;
    return &local;  // Disaster! local is destroyed after function
}

int main() {
    // Don't call these - they demonstrate bad practices
    return 0;
}

Practice Questions: Null & Void Pointers

Task: Write a function that safely prints the value of an int pointer, printing "null" if the pointer is null.

Show Solution
void safePrint(int* ptr) {
    if (ptr != nullptr) {
        cout << *ptr << endl;
    } else {
        cout << "null" << endl;
    }
}

// Usage:
int x = 42;
int* ptr1 = &x;
int* ptr2 = nullptr;

safePrint(ptr1);  // 42
safePrint(ptr2);  // null

Task: Write a generic swap function using void pointers that can swap values of any type.

Show Solution
void genericSwap(void* a, void* b, size_t size) {
    char* temp = new char[size];
    memcpy(temp, a, size);
    memcpy(a, b, size);
    memcpy(b, temp, size);
    delete[] temp;
}

// Usage:
int x = 10, y = 20;
genericSwap(&x, &y, sizeof(int));
// x = 20, y = 10

double d1 = 1.5, d2 = 2.5;
genericSwap(&d1, &d2, sizeof(double));
// d1 = 2.5, d2 = 1.5

Key Takeaways

Pointer Basics

Store addresses with *, get address with &

Dereferencing

*ptr accesses value at address

Pointer Arithmetic

ptr+1 moves by sizeof(type) bytes

Arrays & Pointers

Array name is pointer to first element

Dynamic Memory

new allocates, delete frees memory

Safety

Check nullptr, avoid dangling pointers

Knowledge Check

Quick Quiz

Test what you have learned about C++ pointers

1 What does the & operator do?
2 If int* ptr points to an int, what does ptr + 1 do?
3 How do you deallocate an array created with new int[10]?
4 What is a dangling pointer?
5 Given int arr[5], what is arr equivalent to?
6 What is the modern C++ way to represent a null pointer?
Answer all questions to check your score