Tutorials Logic
Tutorials Logic, IN info@tutorialslogic.com
Navigation
Home About Us Contact Us Blogs FAQs
Tutorials
All Tutorials
Services
Academic Projects Resume Writing Website Development
Practice
Quiz Challenge Interview Questions Certification Practice
Tools
Online Compiler JSON Formatter Regex Tester CSS Unit Converter Color Picker
Compiler Tools

C++ Pointers and References — Smart Pointers Guide

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).

ConceptSyntaxMeaning
Declare pointerint *ptr;ptr can hold address of an int
Get addressptr = &x;Store address of x in ptr
Dereference*ptrAccess value at the address ptr holds
Null pointerptr = nullptr;Pointer points to nothing (safe)
Member via pointerptr->memberEquivalent to (*ptr).member
Pointer Basics "” Declare, Address, Dereference
#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.

Pointer Arithmetic
#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.

DeclarationPointer can change?Value can change?Use case
const int *pYesNoRead-only access to data
int * const pNoYesFixed pointer, mutable data
const int * const pNoNoFully immutable
const Pointers "” All Three Forms
#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.

Pointer to Pointer and Dynamic 2D Array
#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.

Function Pointers and std::function
#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.

FeaturePointerReference
Can be nullYes (nullptr)No "” must bind to a variable
Can be reseatedYesNo "” always refers to same variable
Syntax to access*ptr or ptr->Direct "” no special syntax
ArithmeticYesNo
Use in function paramsWhen null is valid or reseating neededPreferred for non-null params
Initialized when declaredOptionalMandatory
References "” Pass by Ref, Return by Ref, Rvalue Ref
#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.

new / delete "” Heap Allocation
#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 PointerOwnershipUse when
unique_ptrSole owner "” cannot be copiedSingle owner, most common case
shared_ptrShared "” reference countedMultiple owners needed
weak_ptrNon-owning observerBreak circular references
unique_ptr, shared_ptr, weak_ptr
// 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.

Common Pointer Bugs "” Dangling, Leak, Double-free
#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;
}

Ready to Level Up Your Skills?

Explore 500+ free tutorials across 20+ languages and frameworks.