Articles

C++ Pointers Explained: Types, Examples & Best Practices

When learning C++, one of the most powerful yet often misunderstood concepts is pointers. Pointers provide direct access to memory, enabling more efficient and flexible programs. However, misusing them can lead to errors that are hard to debug.

In this guide, we’ll explore what C++ pointers are, their types, how to use them, and the best practices to follow for using them efficiently.

Let’s start with a brief overview of what pointers are in C++.

  • Learn how to use C pointers and proper memory management to make your programs more efficient.
    • Beginner Friendly.
      1 hour
  • References and pointers are some of the most powerful features in C++; they allow programmers to directly manipulate memory.
    • Beginner Friendly.
      1 hour

What are C++ pointers?

Pointers in C++ are a special type of variable that holds or stores the memory address of another variable instead of storing a direct value. This means that rather than holding actual data like an int or char, a pointer points to the location in memory where that data is located.

Every variable in a program is stored at a unique memory address, and pointers give us a way to directly work with those addresses. This makes them extremely powerful for tasks like dynamic memory allocation, efficient array traversal, and giving functions the ability to modify data stored elsewhere. Conceptually, this is similar to pass-by-reference, but it is done explicitly using memory addresses.

The size of a C++ pointer depends on the system architecture. On a 32-bit system, a pointer typically occupies 4 bytes, while on a 64-bit system, it usually occupies 8 bytes.

Now that we know what pointers are, let’s learn how to declare and initialize pointers in C++.

Declaring and initializing pointers in C++

Before using pointers in C++, we must both declare and initialize them correctly. A pointer that is declared but not initialized may point to an undefined memory location, often called garbage memory, which indicates leftover or unpredictable values stored in that location. Accessing such memory leads to unpredictable behavior and program crashes.

Declaring pointers in C++

The syntax for declaring a pointer is:

data_type* pointer_name; 

Or,

data_type *pointer_name; 

In the syntax, data_type defines the type of value the pointer will point to (e.g., int, char, double). On the other hand, the asterisk (*) has two related meanings in C++:

  • When used in a declaration (int* ptr;), it informs the compiler that this variable is a pointer.
  • When used in an expression (*ptr), it dereferences the pointer—i.e., accesses the value stored at the memory address the pointer holds.

For example:

int* ptr; // Pointer to an int
char* cptr; // Pointer to a char

At this point, the pointers are declared but not yet initialized, so dereferencing them now would lead to undefined behavior.

Initializing pointers in C++

There are two safe ways to initialize a pointer:

1. Assigning an existing variable’s address

The most common way to initialize a pointer is by storing the address of an already declared variable. This directly links the pointer to that variable’s memory location, allowing us to access or modify its value through the pointer using dereferencing:

int num = 20;
int* ptr = # // Pointer initialized with address of num
cout << *ptr << endl; // Dereferencing ptr to access the value of num
*ptr = 30; // Dereferencing ptr to modify the value of num
cout << num << endl; // Output: 30

In this example, ptr points to the memory location of num, denoted using &num. The & operator (known as the reference operator) is used to retrieve the memory address of the num variable. The *ptr syntax dereferences the pointer, allowing us to read or write the value stored at that address.

2. Using nullptr (Modern C++)

Sometimes we need to declare a pointer in advance, even if we don’t yet have a variable for it to point to—for example, when a pointer will later be assigned dynamically allocated memory or a value returned by a function. In such cases, we should initialize the pointer with nullptr. This prevents it from pointing to random memory and makes it safer to check before using it:

int* ptr = nullptr; // Safe initialization, does not point anywhere

If we attempt to dereference a null pointer, the program will typically crash or throw a runtime error instead of silently reading or writing garbage memory. This makes bugs easier to detect. By initializing a pointer with nullptr, we ensure it doesn’t accidentally access random memory, and we can safely check whether it points to a valid location before using it:

if (ptr != nullptr) {
*ptr = 10; // Safe only if ptr points to valid memory
}

Next, let’s check out the different types of pointers in C++.

Types of pointers in C++

C++ provides different types of pointers depending on how they are used and what they point to. Let’s discuss some of the more important ones.

Null pointer

A null pointer doesn’t point to any memory location. In modern C++, it is represented using the keyword nullptr. Using nullptr helps ensure that a pointer does not accidentally reference garbage memory.

For example, this code block shows how nullptr can prevent undefined behavior by allowing us to check whether a pointer is pointing to a valid memory location before using it:

int* ptr = nullptr;
if (ptr != nullptr) {
*ptr = 10; // Safe: This block runs only if ptr points to valid memory
} else {
std::cout << "Pointer is null, cannot assign value.\n";
}

Void pointer

A void pointer (also called a generic pointer) can hold the memory address of any data type, but it cannot be dereferenced directly without typecasting.

When the type is known at compile time and no runtime checks are needed, static casting is used:

void* ptr;
int x = 10;
ptr = &x; // Void pointer holding the address of x
cout << *((int*)ptr); // Output: 10 (Cast needed to access value)

On the other hand, dynamic casting is used when converting pointers or references in a class hierarchy with runtime type checking to ensure the cast is safe.

In this example, a void pointer is first cast to a base class pointer using static casting, and then dynamic casting safely downcasts it to the derived class before use:

void* ptr = new Derived();
Base* basePtr = static_cast<Base*>(ptr); // First convert void* → Base* (Static casting)
// Use dynamic casting for safe downcasting at runtime
if (Derived* dPtr = dynamic_cast<Derived*>(basePtr)) {
dPtr->greet(); // Output: Hello from Derived
}

Dangling pointer

A dangling pointer is one that points to memory that has already been freed or deleted. Accessing such memory is unsafe and may crash the program:

int* ptr = new int(5);
delete ptr; // Memory freed
// ptr is now dangling (Invalid memory reference)

Wild pointer

A wild pointer is an uninitialized pointer that points to a random memory location. Dereferencing such a pointer is dangerous because it can cause undefined behavior, crashes, or data corruption:

int* ptr; // Wild pointer (Not initialized)
*ptr = 10; // Dangerous

Constant pointer

A constant pointer is a pointer that, once initialized, cannot point to a different address, but the value stored at that address can still be modified:

int x = 5, y = 10;
int* const ptr = &x;
*ptr = 20; // Allowed
// ptr = &y; // Not allowed

With different types covered, we can now explore how pointers and arrays work together in C++.

Pointers and arrays

In C++, pointers are closely related to arrays. In fact, the name of an array itself acts like a pointer to its first element. When we assign an array to a pointer, the pointer refers to the beginning of the array. From there, we can use pointer arithmetic to access different elements, just as we would with indexing.

Using pointers with arrays is useful because it allows more efficient traversal and manipulation of array elements, especially in functions, without copying the entire array. Pointer arithmetic can also simplify operations like iterating through dynamically allocated arrays or working with segments of an array.

Array name as a pointer

When we write:

int arr[3] = {10, 20, 30};
int* ptr = arr;

Here:

  • arr gives the address of the first element (&arr[0])
  • ptr now points to the address of the first element.

Now, ptr is equivalent to arr[0], and (ptr + i) is equivalent to arr[i].

Accessing array elements using pointers

This example shows how we can access array elements using pointers:

#include <iostream>
using namespace std;
int main() {
double numbers[] = {2.5, 4.5, 6.5, 8.5};
double* ptr = numbers;
for (int i = 0; i < 4; i++) {
cout << "numbers[" << i << "] = " << *(ptr + i) << endl;
}
return 0;
}

The output will be:

numbers[0] = 2.5
numbers[1] = 4.5
numbers[2] = 6.5
numbers[3] = 8.5

In this example, instead of array indexing (arr[i]), we used pointer arithmetic (*(ptr + i)).

Modifying array elements using pointers

Since pointers give direct access to memory, we can also change array values using them. This can be useful when we want to efficiently modify elements in an array without using the array index notation, especially in scenarios like passing arrays to functions, iterating through large datasets, or implementing dynamic data structures where direct memory manipulation improves performance:

#include <iostream>
using namespace std;
int main() {
int arr[3] = {1, 2, 3};
int* ptr = arr;
*ptr = 10; // Changes arr[0] to 10
*(ptr + 1) = 20; // Changes arr[1] to 20
for (int i = 0; i < 3; i++) {
cout << arr[i] << " ";
}
return 0;
}

The output will be:

10 20 3

Just like arrays, pointers also play a crucial role when working with functions.

Pointers and functions

One of the most powerful applications of pointers in C++ is their use with functions. Normally, when we pass variables to a function, they are passed by value, meaning the function receives a copy of the variable. Changes inside the function don’t affect the original variable.

Pointers solve this limitation by enabling pass-by-reference. Unlike C++’s built-in pass-by-reference (&), pointers allow more flexibility—for example, passing nullptr, handling arrays dynamically, or modifying multiple variables through a single pointer. They also make it clear that a function may change the original data, improving both clarity and efficiency.

Passing pointers to functions

This example shows how to pass a pointer to a function:

#include <iostream>
using namespace std;
void updateValue(int* p) {
*p = *p + 10; // Modifies the original variable
}
int main() {
int num = 5;
updateValue(&num); // Pass address of num
cout << "Updated value: " << num << endl;
return 0;
}

The output will be:

Updated value: 15

Here, updateValue() receives the address of num, and by dereferencing the pointer (*p), it changes the original variable.

Pointers to functions

C++ also allows pointers to point to functions themselves. This lets us store the address of a function in a pointer and call it indirectly, which is useful for callback mechanisms or passing functions as arguments:

#include <iostream>
using namespace std;
void greet() {
cout << "Hello from a function!" << endl;
}
int main() {
void (*funcPtr)() = &greet; // Pointer to function
funcPtr(); // Call function via pointer
return 0;
}

In this example, (*funcPtr) declares funcPtr as a pointer to a function. The parentheses are necessary as without them, C++ would interpret it as a function returning void instead of a pointer. The () after (*funcPtr) indicates that the function takes no parameters.

As a whole, the void (*funcPtr)() = &greet; line assigns the address of the greet function to the (*funcPtr) pointer. The & is optional, as the function name alone also represents its address.

The output will be:

Hello from a function!

Now that we’ve seen how powerful pointers can be in functions, it’s equally important to understand their advantages and disadvantages before using them extensively.

Pointer arithmetic in C++

One of the unique features of pointers in C++ is that we can perform a number of arithmetic operations on them. This allows us to move through memory locations efficiently, especially when working with arrays. However, pointer arithmetic is limited and only supports a few well-defined operations:

1. Increment (++)

The increment operator (++) moves the pointer to the next memory location based on its data type:

#include <iostream>
using namespace std;
int main() {
int arr[] = {10, 20, 30};
int* ptr = arr; // Points to arr[0]
ptr++; // Now points to arr[1]
cout << *ptr;
return 0;
}

Here, ptr++ increases the pointer by the size of the data type (int) (i.e., 4 bytes), not just by one byte.

The output will be:

20

2. Decrement (–)

The decrement operator (--) moves the pointer to the previous memory location:

#include <iostream>
using namespace std;
int main() {
int arr[] = {10, 20, 30};
int* ptr = &arr[2]; // Points to arr[2]
ptr--; // Now points to arr[1]
cout << *ptr;
return 0;
}

The output will be:

20

3. Addition (+)

The addition operator (+) can be used with pointers to move them forward in memory. When we add an integer n to a pointer, the pointer advances by n elements, not raw bytes. The actual number of bytes skipped depends on the size of the type the pointer refers to:

#include <iostream>
using namespace std;
int main() {
int arr[] = {10, 20, 30, 40};
int* ptr = arr;
cout << *(ptr + 2);
return 0;
}

The output will be:

30

If we increment a pointer beyond the valid range of the array (or object) it points to, we invoke undefined behavior. This can lead to memory corruption, program crashes, or unpredictable results. Always ensure your pointer arithmetic stays within the allocated bounds.

4. Subtraction (-)

The subtraction operator (-) can be used with pointers to move them backward in memory. When we subtract an integer n from a pointer, the pointer moves back by n elements:

#include <iostream>
using namespace std;
int main() {
int arr[] = {10, 20, 30, 40};
int* ptr = &arr[3]; // Points to arr[3]
cout << *(ptr - 2); // Moves 2 elements back → arr[1]
return 0;
}

The output will be:

20

Just like the addition operator, subtracting too far can move the pointer outside the valid range of the array (or object) it belongs to, invoking undefined behavior.

5. Pointer difference

We can subtract one pointer from another (as long as they both point to the same array). The result is the number of elements between them, not the number of bytes:

#include <iostream>
using namespace std;
int main() {
int arr[] = {10, 20, 30, 40};
int* p1 = &arr[0];
int* p2 = &arr[3];
cout << (p2 - p1);
return 0;
}

Here is the output:

3

The output indicates that there are 3 elements between the pointers p1 and p2.

Next, let’s discuss how we can use pointers with arrays in C++.

Advantages and disadvantages of C++ pointers

There are several advantages of using pointers in C++:

  • Efficient memory management: Pointers allow us to allocate and deallocate memory dynamically, which is essential for managing resources effectively.
  • Faster execution: By giving direct access to memory addresses, pointers reduce overhead and make programs more efficient.
  • Support for complex data structures: Pointers are the foundation for implementing linked lists, trees, graphs, and other advanced data structures.
  • Pass by reference: Pointers make it possible to modify variables directly inside functions without returning values.

However, there are some disadvantages as well:

  • Risk of memory leaks: If dynamically allocated memory is not properly freed, it can lead to wasted memory.
  • Dangling and wild pointers: Mismanagement of pointers (e.g., using freed or uninitialized pointers) can cause crashes or undefined behavior.
  • Increased complexity: Pointer syntax and logic can be difficult for beginners to understand and debug.
  • Security issues: Improper use of pointers can expose vulnerabilities like buffer overflows, leading to potential exploits.

Finally, let’s go through some best practices that we should follow when using pointers in C++.

Best practices for using C++ pointers

Applying these best practices will ensure effective usage of C++ pointers:

  • Always initialize pointers: Set pointers to a valid address or nullptr to avoid wild pointers.
  • Limit pointer arithmetic: Keep arithmetic minimal and always stay within array bounds to avoid undefined behavior.
  • Validate pointers before use: Check for nullptr before dereferencing to ensure safe access.
  • Use references when possible: For function parameters, prefer references over raw pointers if ownership transfer is not required.

Conclusion

In this guide, we explored what C++ pointers are, their different types, how to declare and initialize them, and how pointer arithmetic works. We also looked at the relationship between pointers and arrays, their role in functions, and weighed their advantages and disadvantages. Finally, we discussed some best practices to avoid common pitfalls and make the most of pointers in modern C++ programming.

Pointers in C++ are an essential concept that unlocks low-level memory management, efficient data handling, and powerful features like dynamic memory allocation. While they can be tricky to master, following best practices will ensure our code is safe and efficient. By understanding C++ pointers, we can write cleaner and more reliable programs.

If you want to learn more about pointers in C++, check out the Learn C++: References and Pointers course on Codecademy.

Frequently asked questions

1. Why are C++ pointers useful?

C++ pointers are useful because they provide direct access to memory, which makes programs more efficient. They allow us to manipulate variables, work with arrays more effectively, and enable dynamic memory allocation.

2. What’s a dangling pointer in C++?

A dangling pointer in C++ is a pointer that references memory after the object it pointed to has been deleted or gone out of scope. Using it may access garbage memory, causing undefined behavior, crashes, or data corruption. To avoid this, set pointers to nullptr after freeing or losing ownership of memory.

3. What’s the size of a pointer in C++?

The size of a C++ pointer depends on the system architecture. On a 32-bit system, a pointer typically occupies 4 bytes, while on a 64-bit system, it usually occupies 8 bytes.

4. Is pointer a data type in C++?

In C++, a pointer is not a primitive data type like int or float, but rather a derived type. It contains the memory address of another variable. Each pointer is strongly typed, meaning an int* can only point to integers, a char* to characters, and so on.

5. What is a null pointer in C++?

A null pointer in C++ is a special type of pointer that does not point to any valid memory location. In modern C++, we use the keyword nullptr to represent this. Null pointers are often used to specify that a pointer is not currently assigned, and checking for nullptr before dereferencing helps prevent runtime errors.

Codecademy Team

'The Codecademy Team, composed of experienced educators and tech experts, is dedicated to making tech skills accessible to all. We empower learners worldwide with expert-reviewed content that develops and enhances the technical skills needed to advance and succeed in their careers.'

Meet the full team

Learn more on Codecademy

  • Learn how to use C pointers and proper memory management to make your programs more efficient.
    • Beginner Friendly.
      1 hour
  • References and pointers are some of the most powerful features in C++; they allow programmers to directly manipulate memory.
    • Beginner Friendly.
      1 hour
  • Learn about the C programming language in this beginner-friendly skill path.
    • Includes 6 Courses
    • With Certificate
    • Beginner Friendly.
      11 hours