Pointers

Store the address of another object and use it to read, modify, and pass around data indirectly — the mechanism underneath references, arrays, and dynamic memory.

A pointer is an object that stores the address of another object or function. Pointers provide a way to indirectly access and manipulate data. They are an important part of C++ because they allow programs to work with objects through their addresses rather than only through their direct names.

Pointer Fundamentals

Consider an ordinary variable:

The variable has a name (value), a type (int), a stored value (42), and a memory location/address.

A pointer can store that address.

Conceptually, value refers to the actual object, and ptr refers to the address of value.

int value = 42;
int* ptr = &value;
value
+-------+
|  42   |
+-------+
   ^
   |
   | address
   |
+-------+
|  ptr  |
+-------+

Declaring a Pointer

The general syntax is type* pointer_name;

Three pointer declarations, one for each pointed-to type.

The pointer type indicates the type of object that the pointer is intended to point to. For example, int* ptr; means "ptr is a pointer to an int."

int* ptr;
double* dptr;
char* cptr;

Initializing a Pointer

A pointer should generally be initialized before it is used.

Here &value produces the address of value, and that address is stored in ptr.

int value = 42;
int* ptr = &value;

Address-of Operator &

The unary & operator obtains the address of an object.

ptr is initialized with the address of value.

Conceptually, value has an address and a value, and ptr's own value is that address. The actual address is determined by the implementation and execution environment.

int value = 42;
int* ptr = &value;
value: Address 0x1000, Value 42
ptr:   Value 0x1000

Dereference Operator *

The unary * operator can be used to access the object referred to by a pointer.

Accesses the object that ptr points to and prints it.

ptr holds the address of value *ptr accesses the object at that address, which is value

So *ptr means "access the object that ptr points to."

int value = 42;
int* ptr = &value;
std::cout << *ptr;

Reading Through a Pointer

Reads the pointed-to object's value through the pointer and copies it into result.

*ptr reads the object value points to, giving 100 The pointer does not contain 100 — it contains the address of the object containing 100.

int value = 100;
int* ptr = &value;
int result = *ptr;

Modifying Through a Pointer

Assigns through the pointer, which changes the pointed-to object itself.

ptr holds the address of value *ptr = 50 writes 50 into value

The pointer provides indirect access to the original object.

int value = 10;
int* ptr = &value;
*ptr = 50;
std::cout << value;

Pointer vs Pointee

It is important to distinguish the pointer from the object it points to.

value is the object; ptr is a separate object that stores value's address.

Five related expressions and what each one means.

This distinction is fundamental to understanding pointers.

int value = 42;
int* ptr = &value;
Expression | Meaning
value      | The actual int object
&value     | Address of value
ptr        | Pointer object storing that address
*ptr       | The object referred to by ptr
&ptr       | Address of the pointer object itself

Pointer Type

Each pointer's type matches the type of object it points to.

A pointer's type tells C++ how the pointed-to object should be interpreted when accessed and how pointer arithmetic works.

int value = 10;
double price = 12.5;
int* ip = &value;
double* dp = &price;

Pointer Size

The size of a pointer is not necessarily the same as the size of the object it points to.

The pointer types can have different pointed-to types while commonly having the same pointer representation size on a particular platform.

You can inspect their sizes with sizeof(ip), sizeof(dp), sizeof(cp) — the exact sizes are implementation- and platform-dependent.

Do not assume sizeof(pointer) == sizeof(pointed-to-object).

int* ip;
double* dp;
char* cp;

Null Pointer

A pointer that does not point to an object can be represented by a null pointer. Modern C++ uses nullptr.

ptr refers to no object.

A null pointer is different from an uninitialized pointer.

int* ptr = nullptr;

nullptr

nullptr is a special C++ null pointer literal.

Compares a pointer against nullptr directly.

A pointer converts to bool for a null check: true when non-null, false when null.

int* ptr = nullptr;
if (ptr == nullptr)
{
    std::cout << "Pointer is null";
}
if (ptr) { /* not null */ }
if (!ptr) { /* is null */ }

Why nullptr Is Preferred

Older C++ code commonly used int* ptr = 0; or int* ptr = NULL;

Modern C++ should generally use nullptr for a null pointer.

nullptr has a dedicated null pointer type and avoids several ambiguities that can occur with integer 0 or NULL.

int* ptr = nullptr;

Null Pointer Must Not Be Dereferenced

This is invalid: a null pointer does not refer to an object.

Dereferencing a null pointer results in undefined behavior. Always ensure that a pointer refers to a valid object before dereferencing it.

int* ptr = nullptr;
std::cout << *ptr;

Uninitialized Pointers

This is dangerous: if ptr is a local automatic variable and is not initialized, it contains an indeterminate value.

Using an uninitialized pointer as though it points to a valid object is unsafe.

Prefer int* ptr = nullptr; if there is not yet a valid object to point to.

int* ptr;
*ptr = 10; // Invalid

Pointer Assignment

Pointers can be assigned addresses of compatible objects.

ptr is retargeted from a to b.

initially ptr holds the address of a ptr = &b assigns the address of b into ptr ptr now refers to b

The pointer itself can change which object it refers to.

int a = 10;
int b = 20;
int* ptr = &a;
ptr = &b;

Changing the Pointer vs Changing the Object

These operations are different: *ptr = 100; changes the pointed-to object, while ptr = &other; changes the pointer itself.

One statement modifies the pointee; the other retargets the pointer.

*ptr = 50 writes through ptr into a, so a becomes 50 ptr = &b changes what ptr refers to, without touching a final state: a = 50, b = 20, ptr refers to b

int a = 10;
int b = 20;
int* ptr = &a;
*ptr = 50;     // changes a
ptr = &b;      // changes what ptr points to

Multiple Pointers Can Point to One Object

Both pointers refer to the same object.

Changing through one pointer affects the same object.

*p1 = 200 writes 200 into value *p2 reads value, which is now 200

int value = 100;
int* p1 = &value;
int* p2 = &value;
*p1 = 200;
std::cout << *p2;

Pointer Dereferencing and Assignment

Reads through the pointer, computes a new value, then writes it back through the same pointer.

*ptr reads value, giving 10 *ptr + 5 computes 15 *ptr = 15 writes 15 back into value final value: value == 15

int value = 10;
int* ptr = &value;
*ptr = *ptr + 5;

Pointers as Function Parameters

The function receives a pointer value by value, but that pointer can be used to access the caller's object.

number starts at 10 increment receives the address of number

++(*value) increments the object at that address after the call: number == 11

void increment(int* value)
{
    ++(*value);
}
int number = 10;
increment(&number);

Pointer-Based Modification

Guards the dereference with a null check before modifying the pointed-to object.

value starts at 10 setValue receives the address of value ptr is not null, so *ptr = 100 executes

The null check makes the function safe when a null pointer is a valid input.

#include <iostream>

void setValue(int* ptr)
{
    if (ptr != nullptr)
    {
        *ptr = 100;
    }
}

int main()
{
    int value = 10;
    setValue(&value);
    std::cout << value << '\n';
    return 0;
}

Pointer-Based Swap

Swaps the values of two caller objects through their addresses.

x starts at 10, y starts at 20 temp is set to *a, which is 10 *a = *b sets x to 20 *b = temp sets y to 10 afterward: x = 20, y = 10

void swapValues(int* a, int* b)
{
    if (a == nullptr || b == nullptr)
    {
        return;
    }
    int temp = *a;
    *a = *b;
    *b = temp;
}
int x = 10;
int y = 20;
swapValues(&x, &y);

Pointer to const

The pointer can read the object but cannot modify it through the pointer.

Reading through ptr is allowed; writing through it is not.

The important rule: const int* means "cannot modify the int through this pointer."

const int value = 42;
const int* ptr = &value;
std::cout << *ptr;   // OK
*ptr = 100;          // Error

Pointer to a Non-Const Object Through const View

A const int* can also point to a non-const int.

value itself is non-const, but ptr only offers a read-only view of it.

The original object can still be modified directly; modification through ptr is not allowed.

So const here restricts access through that pointer.

int value = 42;
const int* ptr = &value;
value = 100; // Valid
*ptr = 100;  // Error

void*

C++ provides void*, a pointer to no specific object type.

ptr can hold the address of an object of any type.

A void* can hold the address of an object, but it does not provide a specific pointed-to type for ordinary dereferencing — for example, *ptr is not valid because void has no object size or type for dereferencing. A typed pointer is normally preferable when the type is known.

int value = 42;
void* ptr = &value;

Pointer to Pointer

A pointer can itself have an address.

pptr stores the address of ptr, which itself stores the address of value.

Therefore *pptr gives ptr, while **pptr gives value. Pointer-to-pointer is a distinct concept and becomes particularly useful when working with dynamically managed objects and APIs that need to modify a pointer itself.

int value = 42;
int* ptr = &value;
int** pptr = &ptr;

Pointer to a Function

Declares a pointer to a function and calls through it.

operation is set to the address of add operation(2, 3) calls add(2, 3) indirectly

Function pointers are useful when a function needs to be selected or passed indirectly.

int add(int a, int b)
{
    return a + b;
}
int (*operation)(int, int) = &add;
int result = operation(2, 3);

Pointer Arithmetic

Pointers to elements of an array support pointer arithmetic.

Advancing ptr moves it to the next array element.

The movement is based on the pointed-to type, not simply one raw byte. Detailed pointer arithmetic and its relationship with arrays belong to the dedicated pointer-arithmetic topic.

int values[] = {10, 20, 30, 40};
int* ptr = values;
++ptr;

Pointers and Object Lifetime

A pointer does not automatically keep an object alive merely because it stores the object's address.

value's block ends before ptr is used further.

value is created inside the inner block ptr is set to the address of value the block ends, and value ceases to exist

*ptr would now attempt to access an object that no longer exists The pointer has become dangling.

int* ptr;
{
    int value = 42;
    ptr = &value;
}

Dangling Pointer

A dangling pointer is a pointer whose stored value no longer refers to a valid object. Common causes include pointing to a local object that has gone out of scope, pointing to an object whose lifetime has ended, using memory after it has been released, and keeping a pointer after its referenced storage is no longer valid.

Once the inner block ends, ptr no longer refers to a live object.

The pointer may still contain an address-like value, but the object is gone.

int* ptr;
{
    int value = 42;
    ptr = &value;
}
// ptr is now dangling

Pointer Does Not Mean Ownership

ptr does not own value.

The lifetime of value is controlled by the scope and lifetime rules governing value. Ownership is a separate concept from simply referring to an object.

int value = 42;
int* ptr = &value;

Pointer and Reference

A feature-by-feature comparison of pointers and references.

Both ptr and ref ultimately modify value.

*ptr = 20 sets value to 20 ref = 30 sets value to 30, since ref is an alias for value value ends at 30

Both ultimately modify value, but their semantics are different.

Feature                                    | Pointer                    | Reference
Can be null                                | Yes                        | No valid null reference
Must initialize                            | Not always, but should be | Yes
Can change what it refers to               | Yes                        | No reseating
Uses * for access                          | Yes                        | No explicit dereference
Uses & to obtain address                   | Yes                        | Yes
Pointer arithmetic                         | Yes, where valid           | No
Can represent absence                      | Yes, with nullptr          | Not directly
Stores an address as its language-level model | Pointer stores a pointer value | Reference is an alias
int value = 10;
int* ptr = &value;
int& ref = value;
*ptr = 20;
ref = 30;

Best Practices

1. Initialize pointers — prefer this over leaving a pointer uninitialized.

2. Check nullable pointers before using them.

3. Dereference only valid pointers — a non-null pointer is not automatically a valid pointer; it must also point to a live, appropriately typed object.

4. Keep ownership clear — a pointer should not be assumed to own an object merely because it points to it.

5. Prefer references when null is not meaningful — if an operation requires a valid object and does not need reseating, a reference can often express that requirement more directly.

6. Use meaningful pointer names when the purpose is known.

7. Never return a pointer to a local (automatic-duration) variable from a function — like a dangling reference, it refers to storage that no longer exists once the function returns; only return a pointer to an object with a suitable, longer lifetime.

int* ptr = nullptr;
if (ptr != nullptr)
{
    /* safe to use if the pointer also meets lifetime/validity requirements */
}
int* sensorValue; // clearer than: int* p;

Complete Example

A complete program combining initialization, dereferencing, pointer-based modification through a function, and a null check.

value starts at 50, and ptr points to it the first two lines print 50 and 50 updateValue adds 10 through the pointer, making value 60 the third line prints 60 ptr is set to nullptr the null check succeeds, printing the final message The program's output.

#include <iostream>

void updateValue(int* ptr)
{
    if (ptr != nullptr)
    {
        *ptr += 10;
    }
}

int main()
{
    int value = 50;
    int* ptr = &value;
    std::cout << "Before: " << value << '\n';
    std::cout << "Through pointer: " << *ptr << '\n';
    updateValue(ptr);
    std::cout << "After: " << value << '\n';
    ptr = nullptr;
    if (ptr == nullptr)
    {
        std::cout << "Pointer is null\n";
    }
    return 0;
}
Before: 50
Through pointer: 50
After: 60
Pointer is null

Pointer Mental Model

Think of a pointer as a separate object that can contain a reference to another object's location:

The pointer object holds an address that leads to a separate object elsewhere.

For int value = 42; int* ptr = &value; the relationships are: value is the actual int object, &value is the address of that object, ptr stores that pointer value, *ptr accesses the object. The fundamental operations are therefore &object to obtain an address, pointer to store an address, and *pointer to access the referred object.

Pointer
                  +---------+
                  | address |
                  +---------+
                       |
                       |
                       v
                  +---------+
                  | object  |
                  |  value  |
                  +---------+

Quick Reference

A quick-reference table summarizing common pointer operations from this topic.

Given int value = 5; int* ptr = &value;, what is the difference between what ptr holds and what *ptr accesses?

ptr holds the address of value (its own stored value is that address). *ptr dereferences ptr, giving access to the actual object at that address, which is value itself — reading or writing *ptr reads or writes value.

Why is int* ptr = nullptr; generally preferable to leaving int* ptr; uninitialized, and why is a null pointer still different from a valid pointer?

An uninitialized local pointer holds an indeterminate value and using it is unsafe (it may appear to point anywhere). Setting it to nullptr gives it a well-defined 'points to no object' state that can be checked with ptr == nullptr. It is still not a valid pointer to an object — dereferencing a null pointer is undefined behavior just as dereferencing a stray uninitialized one is; a pointer must be null-checked and refer to a live object before it is dereferenced.

Given const int value = 42; const int* ptr = &value;, which of these are valid: std::cout << *ptr; and *ptr = 100;? Explain the rule.

std::cout << *ptr; is valid — reading through the pointer is allowed. *ptr = 100; is invalid — const int* means the int cannot be modified through this pointer, regardless of whether the pointed-to object itself is const.

For int* ptr = &value; int** pptr = &ptr;, what do *pptr and **pptr each give you?

*pptr dereferences pptr once, giving ptr itself (the address it stores, i.e. the address of value). **pptr dereferences twice, giving the object value points to, i.e. value itself.

What happens in this code, and what should you call the resulting state of ptr after the block ends: int* ptr; { int value = 42; ptr = &value; } ?

value is a local automatic object that is destroyed when the inner block ends. ptr still holds the address value used to occupy, but that storage no longer holds a valid object — ptr is now a dangling pointer. Dereferencing it after the block is undefined behavior.

Trace through this code. What are the final values of a, b, and *ptr, and why doesn't ptr = &b; affect a?

*ptr = 50; writes through ptr while it still points to a, so a becomes 50. ptr = &b; then retargets the pointer itself to point to b instead — it does not write through the pointer, so a is untouched by this line. Final: a is 50, b is 20, and *ptr (now referring to b) is 20. Output: 50 20 20.

Write a function named tripleValue that takes an int* parameter and, if the pointer is not null, multiplies the pointed-to object by 3. Call it on an int variable holding 7 and print the result.

tripleValue receives the address of number. Since ptr is not null, *ptr *= 3; multiplies the object at that address by 3, changing number from 7 to 21. The program prints 21.

This code swaps two pointers (not the values they point to). What does it print, and what would need to change to instead swap the values of x and y themselves?

Swapping px and py only changes which object each pointer refers to; x and y themselves are never modified. Before the swap px pointed to x (1) and py pointed to y (2); after swapping the pointers, px points to y and py points to x, so *px is 2 and *py is 1. Output: 2 1. To actually swap the values of x and y, you would need to swap through the pointers instead, e.g. int t = *px; *px = *py; *py = t;, which writes into x and y directly.

Write a function named makeNull that takes an int** parameter (a pointer to a pointer) and sets the pointed-to pointer to nullptr. Demonstrate it on an int* that initially points to a local int, printing whether the pointer is null before and after the call.

makeNull receives the address of ptr itself (an int**), so *pptr refers to ptr. Assigning *pptr = nullptr; changes ptr in the caller to nullptr. Before the call, ptr is not null (it points to value), so the first line prints 'not null'; after the call, ptr has been set to nullptr, so the second line prints 'null'. This is a case where pointer-to-pointer is needed because the function must modify the caller's pointer itself, not just the pointed-to object.

Operation          | Example               | Meaning
Declare pointer    | int* p;               | Pointer to int
Initialize         | int* p = &x;          | Store address of x
Address-of         | &x                    | Address of x
Dereference        | *p                    | Access pointed-to object
Modify             | *p = 10;              | Modify pointed-to object
Null pointer       | int* p = nullptr;     | Points to no object
Null check         | p == nullptr          | Test for null
Pointer assignment | p = &x;               | Make p point to x
Pointer to pointer | int** pp = &p;        | Pointer to a pointer
Const pointee      | const int* p          | Cannot modify object through p
Function pointer   | int (*p)(int,int)     | Pointer to compatible function
Pointer size       | sizeof(p)             | Size of pointer object
int a = 10;
int b = 20;
int* ptr = &a;
*ptr = 50;
ptr = &b;
std::cout << a << ' ' << b << ' ' << *ptr;
int x = 1;
int y = 2;
int* px = &x;
int* py = &y;
int* temp = px;
px = py;
py = temp;
std::cout << *px << ' ' << *py;