Pointers in C++ Explained: Easy Syntax, Examples, and Best Practices

Learning Pointers in C++ is a milestone for any beginner. Pointers let programs work directly with memory: storing addresses, accessing values by reference, and enabling efficient array handling, dynamic memory, and low‑level optimizations. This detailed guide explains what pointers are, how to use them safely, and where they shine—using simple code examples, outputs, and engaging mini‑challenges.

🔹 Quick Reference: Pointer Syntax

SyntaxMeaningExample
int* p;Pointer to int (uninitialized)int* p = nullptr;
int* p = &x;Pointer stores address of x*p dereferences to value of x
const int* pPointer to const int (value read‑only)*p cannot be modified
int* const pConst pointer (address fixed)p = &y; not allowed
const int* const pConst pointer to const intNeither address nor value changeable
int** ppPointer to pointer to int**pp accesses the int

1. What Is a Pointer?

A pointer is a variable that stores a memory address instead of a direct value. The address‑of operator & gives the address of a variable, and the dereference operator * accesses the value at that address.


#include <iostream>
int main() {
    int x = 42;
    int* p = &x;              // p holds the address of x
    std::cout << "x: "   << x   << "\n";
    std::cout << "&x: "  << &x  << "\n";
    std::cout << "p: "   << p   << "\n";
    std::cout << "*p: "  << *p  << "\n"; // value at address in p
    return 0;
}

Output


x: 42
&x: 0x7ff...         (an address)
p:  0x7ff...         (same address as &x)
*p: 42

📝 Try it Yourself: Change the value of x by writing *p = 100; and print x again to see the update.

2. Safe Pointer Initialization (nullptr)

Never leave pointers uninitialized. Use nullptr to clearly represent “points to nothing” and check before dereferencing.


#include <iostream>
int main() {
    int* p = nullptr;  // safe empty pointer
    int y = 10;
    if (!p) {
        p = &y;        // assign when ready
    }
    if (p) {
        std::cout << "*p: " << *p << "\n";  // prints 10
    }
}

📝 Try it Yourself: Create a double variable and a double* pointer. Initialize it to nullptr, assign its address later, and print the value via *ptr.

3. Dereferencing and Modifying Through Pointers

Dereferencing (*) reads or writes the value stored at the address a pointer holds. This lets a function modify a caller’s variable by pointer.


#include <iostream>
void setTo99(int* p) {
    if (p) { *p = 99; }
}
int main() {
    int v = 5;
    setTo99(&v);
    std::cout << "v: " << v << "\n";  // v becomes 99
}

Output


v: 99

📝 Try it Yourself: Write swap(int* a, int* b) that swaps two integers via pointers, then test it in main.

4. Pointers and Arrays (Pointer Arithmetic)

The array name often “decays” to a pointer to its first element. Pointer arithmetic advances by element size (e.g., p + 1 points to the next int in memory).


#include <iostream>
int main() {
    int a[] = {10, 20, 30, 40};
    int* p = a; // same as &a[0]
    std::cout << "*p: "      << *p       << "\n";   // 10
    std::cout << "*(p+1): "  << *(p+1)   << "\n";   // 20
    std::cout << "*(p+2): "  << *(p+2)   << "\n";   // 30
    // Iterate using a pointer
    for (int* it = a; it != a + 4; ++it) {
        std::cout << *it << " ";
    }
    std::cout << "\n";
}

📝 Try it Yourself: Sum an array using a pointer loop (for (int* it = a; it != a + n; ++it)) and print the result.

5. Array Decay and sizeof Caveat

In its declaring scope, sizeof(a)/sizeof(a[0]) gives element count. After decay to a pointer (e.g., in function parameters), sizeof returns pointer size, not element count—so pass the length explicitly.


#include <iostream>
void printArr(const int* arr, int n) {
    // sizeof(arr) is sizeof(int*) here, not total array size
    for (int i = 0; i < n; ++i) std::cout << arr[i] << " ";
    std::cout << "\n";
}
int main() {
    int a[] = {1,2,3,4};
    int n = sizeof(a)/sizeof(a[0]);  // OK here
    printArr(a, n);
}

📝 Try it Yourself: Create a function that returns the max element of an array using pointer traversal; pass the length as a parameter.

6. const With Pointers (Three Useful Forms)

Use const to express intent and prevent bugs. The position of const matters:

  • const int* p: pointer to const int (you can change where p points, not the value via p)
  • int* const p: const pointer to int (address fixed; value modifiable)
  • const int* const p: const pointer to const int (neither changes)

int main() {
    int a = 1, b = 2;
    const int* p1 = &a;   // read-only via p1
    // *p1 = 7;           // ERROR: cannot modify value through p1
    p1 = &b;              // OK: can point to another int
    int* const p2 = &a;   // p2's address fixed
    *p2 = 7;              // OK: can modify a
    // p2 = &b;           // ERROR: cannot change the address
    const int* const p3 = &b; // both fixed
    // *p3 = 9;           // ERROR
    // p3 = &a;           // ERROR
}

📝 Try it Yourself: Convert a function parameter to const int* when it doesn’t modify the data. Try removing const to see how the compiler warns you when you call it with a const object.

7. Pointer to Pointer (int**)

A “pointer to pointer” stores the address of another pointer—useful when a function needs to update the caller’s pointer (e.g., allocate or reset it).


#include <iostream>
int main() {
    int x = 7;
    int* p = &x;
    int** pp = &p;
    std::cout << "**pp: " << **pp << "\n";  // 7
    **pp = 21; // modifies x through pp
    std::cout << "x: " << x << "\n";        // 21
}

Output


**pp: 7
x: 21

📝 Try it Yourself: Write a function resetToNull(int** p) that sets a caller’s pointer to nullptr, then confirm in main.

8. Dynamic Memory: new/delete and Smart Pointers

Use new/delete to allocate/free memory manually—but prefer smart pointers for safety. Always match new with delete and new[] with delete[].


#include <iostream>
#include <memory>  // std::unique_ptr, std::make_unique
int main() {
    // Raw new/delete (manual)
    int* p = new int(42);
    std::cout << "*p: " << *p << "\n";
    delete p;           // free
    p = nullptr;        // avoid dangling
    // Array form
    int* arr = new int[3]{1,2,3};
    std::cout << arr[1] << "\n";  // 2
    delete[] arr;
    arr = nullptr;
    // Safer: smart pointer (no manual delete)
    auto up = std::make_unique<int>(99);
    std::cout << "*up: " << *up << "\n";  // auto-frees
    return 0;
}

📝 Try it Yourself: Allocate a dynamic array with new[], fill it using a pointer loop, print the values, then free it with delete[]. Repeat with std::unique_ptr<int[]> to avoid manual deletion.

9. Common Pitfalls & Best Practices

  • Uninitialized pointers: Always initialize to a valid address or nullptr.
  • Dangling pointers: Don’t use pointers after freeing memory or after the pointee goes out of scope.
  • Double delete: Deleting the same pointer twice is undefined behavior; set it to nullptr after delete.
  • Pointer arithmetic bounds: Never step past the array bounds; out‑of‑range dereference is undefined.
  • new/delete pairing: Match new with delete and new[] with delete[].
  • Prefer smart pointers: Use std::unique_ptr/std::shared_ptr to manage lifetimes safely.

🔹 Frequently Asked Questions (FAQ)

Q1: What’s the difference between & and * with pointers?
& (address‑of) gives the memory address of a variable. * (dereference) accesses the value stored at an address held by a pointer.

Q2: Pointer vs reference—when should I use each?
References must be initialized and cannot be reseated; they often express required, non‑null access. Pointers can be reseated and null (nullptr), which is useful for optional or dynamic ownership scenarios.

Q3: What is nullptr and why not use NULL?
nullptr is a C++11 keyword with a dedicated type, avoiding overload ambiguity that can occur with NULL (typically 0). Prefer nullptr.

Q4: Why did my program crash with a segmentation fault?
Most often due to dereferencing an invalid pointer (uninitialized, dangling, or out‑of‑bounds arithmetic). Add checks, initialize to nullptr, and use smart pointers when possible.

Q5: Do smart pointers eliminate all pointer bugs?
Smart pointers manage lifetime automatically, preventing many leaks and double frees. You still must avoid cycles with shared_ptr (use weak_ptr for back‑references) and ensure correct ownership semantics.

Leave a Comment

About RadiantRiva

Your go-to resource for coding tutorials, developer guides, and programming tips.

Learn More

Quick Links

Follow Us

Newsletter

Get coding tips, tutorials, and updates straight to your inbox.