Tutorials Logic, IN +91 8092939553 info@tutorialslogic.com
Navigation
Home About Us Contact Us Blogs FAQs
Tutorials
All Tutorials
Services
Academic Projects Resume Writing Interview Questions Website Development
Compiler Tutorials

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

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.