Skip to content
Laviyx Learn

Try

C++ course menu

C++ Pointers Explained: Addresses, Dereferencing, nullptr and Pointer Arithmetic

Learn C++ pointers step by step: memory addresses with &, declaring pointers, dereferencing with *, nullptr, pointers and arrays, pointer arithmetic, const and pointers, pointers to structs with ->, and common pointer bugs.

Intermediate10 min readUpdated
On this page

Every variable lives somewhere in memory, and every location in memory has an address, a number that says where it is. A pointer is a variable that stores an address. Pointers let you refer to data indirectly, which is how C++ builds dynamic data structures, works with arrays, and talks to hardware and C libraries.

Addresses: the & operator#

Putting & before a variable gives its address:

C++
int age = 21;
std::cout << &age << '\n';   // prints something like 0x7ffd5c3e9a4c

The exact number is different on every run and every machine, so you rarely care about the value itself, only about what it points to.

Declaring and using a pointer#

A pointer's type says what it points to: int* is "pointer to int". Putting * before a pointer dereferences it, giving the value at that address:

C++
#include <iostream>

int main() {
    int age = 21;
    int* ptr = &age;      // ptr holds the address of age

    std::cout << *ptr << '\n';   // read through the pointer

    *ptr = 30;                   // write through the pointer
    std::cout << age << '\n';

    std::cout << std::boolalpha << (ptr == &age) << '\n';
}
Output
21
30
true

The two meanings of each symbol confuse everyone at first:

SymbolIn a declarationIn an expression
*int* p declares a pointer*p gets the value pointed to
&int& r declares a reference&x gets the address of x

nullptr: pointing at nothing#

A pointer can hold the special value nullptr, meaning "points to nothing". Dereferencing a null pointer is undefined behaviour (usually a crash), so check before using a pointer that might be null:

C++
#include <iostream>

void print_value(const int* p) {
    if (p == nullptr) {
        std::cout << "(no value)\n";
        return;
    }
    std::cout << *p << '\n';
}

int main() {
    int x = 42;
    print_value(&x);
    print_value(nullptr);
}
Output
42
(no value)

Always initialize pointers, either to a real address or to nullptr. An uninitialized pointer holds a random address, and using it causes some of the worst bugs in C++. (Old code uses NULL or 0; use nullptr in modern C++.)

Changing what a pointer points to#

Unlike a reference, a pointer can be re-pointed at another variable:

C++
#include <iostream>

int main() {
    int a = 1;
    int b = 2;
    int* p = &a;
    std::cout << *p << ' ';

    p = &b;               // now points at b
    *p = 20;
    std::cout << *p << ' ' << a << ' ' << b << '\n';
}
Output
1 20 1 20

Pointers to structs: the -> operator#

To reach a member through a pointer, write p->member, which is shorthand for (*p).member:

C++
#include <iostream>
#include <string>

struct Account {
    std::string owner;
    double balance = 0;
};

void deposit(Account* acc, double amount) {
    if (acc) {
        acc->balance += amount;
    }
}

int main() {
    Account a{"Asha", 1000};
    deposit(&a, 250);
    std::cout << a.owner << ": " << a.balance << '\n';
}
Output
Asha: 1250

In real code you would write this function with a reference (Account& acc). A pointer parameter is appropriate when passing "no account" (nullptr) is a valid choice.

Pointers and arrays#

An array's name converts to a pointer to its first element. Adding to a pointer moves it by whole elements, not bytes:

C++
#include <iostream>

int main() {
    int nums[] = {10, 20, 30, 40};
    int* p = nums;            // same as &nums[0]

    std::cout << *p << ' ' << *(p + 2) << ' ' << p[3] << '\n';

    for (int* it = nums; it != nums + 4; ++it) {
        std::cout << *it << ' ';
    }
    std::cout << '\n';
}
Output
10 30 40
10 20 30 40

p[i] is exactly *(p + i). This pointer arithmetic is how arrays work underneath, and it is the idea behind iterators in the STL. It is also dangerous: nothing stops p + 10 from pointing past the end of the array.

const and pointers#

const can apply to the pointed-to value, to the pointer itself, or to both. Read the declaration from right to left:

C++
int value = 5;
int other = 6;

const int* p1 = &value;        // pointer to const int: *p1 cannot change
// *p1 = 7;                    // error
p1 = &other;                   // fine: the pointer itself can move

int* const p2 = &value;        // const pointer to int: cannot move
*p2 = 7;                       // fine: the value can change
// p2 = &other;                // error

const int* const p3 = &value;  // neither can change

const int* is by far the most common: a function that takes const T* promises not to modify the data.

When do you need raw pointers?#

In modern C++, you need raw pointers much less than in older code:

  • For "refers to an object that must exist", use a reference.
  • For collections, use std::vector, std::array and std::string instead of pointers to arrays.
  • For objects created with dynamic memory, use smart pointers (Smart Pointers).

Raw pointers remain useful as non-owning, optional references (something that may be nullptr), when working with C libraries, and inside data structures you build yourself. You will see how dynamic memory works in Dynamic Memory.

Common mistakes#

Practice#

Practice exercise

Write void min_max(const int* data, std::size_t n, int* min_out, int* max_out) that finds the smallest and largest of n values and writes them through the two output pointers. Test it with {7, 2, 9, 4}.

Show solution
C++
#include <iostream>

void min_max(const int* data, std::size_t n, int* min_out, int* max_out) {
    if (n == 0 || !min_out || !max_out) return;
    *min_out = data[0];
    *max_out = data[0];
    for (std::size_t i = 1; i < n; ++i) {
        if (data[i] < *min_out) *min_out = data[i];
        if (data[i] > *max_out) *max_out = data[i];
    }
}

int main() {
    int values[] = {7, 2, 9, 4};
    int lo = 0;
    int hi = 0;
    min_max(values, 4, &lo, &hi);
    std::cout << lo << ' ' << hi << '\n';
}
Output
2 9

This C-style signature is common in older libraries. In modern C++ you would take a const std::vector<int>& and return a std::pair.

Key takeaway#

Finished this lesson?