Koding Books

Professional, free coding tutorials

C++ Pointers

An article exploring C++ pointers.

I was recently working on designing and implementing a trading algorithm that tested my understanding of C++ pointers and references. When asked to explain this particular ‘algo’ to a junior software engineer, I was somewhat flummoxed.

If you can’t explain it simply, you don’t understand it well enough

Albert Einstein

Thus began my quest to improve my understanding of this subject, the goal being to enlighten myself and my junior colleague, who was, at this point, wholly confused. Luckily, I managed to lift his confusion and my understanding.

Pointers

Pointers are variables that store memory addresses. They allow you to manipulate data indirectly by accessing the memory location where the data is stored. In C++, pointers are denoted by the * symbol.

To declare a pointer, specify the data type followed by an asterisk and the name of the pointer variable. For example, to declare a pointer to an integer variable, you would write:

int x = 5;
int *ptr = &x;

To assign a value to a pointer, you use the address-of operator (&) to get the memory address of a variable and assign it to the pointer. For example:

int x = 5;
int *ptr = &x;

This sets the value of ptr to the memory address of x. You can then use the pointer to access the value of x indirectly:

This sets the value of x to 10.

Dynamic memory allocation

To dynamically allocate memory in C++, you can use the new operator to allocate memory on the heap. The new operator returns a pointer to the allocated memory. Here’s an example:

int *arr = new int[10];

This allocates memory for an integer on the heap and assigns the memory address to ptr. You can then use the pointer to access the value of the integer:

*ptr = 5;

This sets the value of the integer to 5.

When you’re done using the dynamically allocated memory, you should free it using the delete operator

delete ptr;

This frees the memory allocated for the integer.

Pointers as function arguments

To pass a pointer as an argument to a function in C++, you declare the function parameter as a pointer. Here’s an example:

void foo(int *ptr) {
    *ptr = 5;
}

int main() {
    int x = 0;
    foo(&x);
    // x is now 5
    return 0;
}

In this example, the foo function takes a pointer to an integer as a parameter. Inside the function, the value of the integer is set to 5 using the pointer. In main, we declare an integer x and pass its memory address to foo using the address of operator (&). After the function call, the value of x is 5.

You can also pass pointers to dynamically allocated memory to functions:

void bar(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        arr[i] = i;
    }
}

int main() {
    int *arr = new int[10];
    bar(arr, 10);
    // arr now contains {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    delete[] arr;
    return 0;
}

In this example, the bar function takes a pointer to an integer array and the size of the array as parameters. Inside the function, the array elements are set to their index using the pointer. In main, we allocate an array of 10 integers on the heap using the new operator and pass its memory address to the bar. After the function call, the elements of the array are set to {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}. Finally, we free the memory allocated for the array using the delete[] operator.

Returning pointers

Let’s take a look at this code:

int* foo() {
    int *ptr = new int;
    *ptr = 5;
    return ptr;
}

int main() {
    int *ptr = foo();
    // ptr points to dynamically allocated memory containing the value 5
    delete ptr;
    return 0;
}

In this example, the foo function returns a pointer to an integer. Inside the function, we allocate memory for an integer on the heap using the new operator, set its value to 5 using the pointer, and return the pointer. In main, we call foo and assign the returned pointer to ptr. After the function call, ptr points to dynamically allocated memory containing the value 5. Finally, we free the memory allocated for the integer using the delete operator.

You can also return pointers to arrays or structures:

int* bar() {
    int *arr = new int[10];
    for (int i = 0; i < 10; i++) {
        arr[i] = i;
    }
    return arr;
}

int main() {
    int *arr = bar();
    // arr points to dynamically allocated memory containing {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    delete[] arr;
    return 0;

In this example, the bar function returns a pointer to an integer array. Inside the function, we allocate memory for an array of 10 integers on the heap using the new operator, set its elements to their index using the pointer, and return the pointer. In main, we call bar and assign the returned pointer to arr. After the function call, arr points to dynamically allocated memory containing {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}. Finally, we free the memory allocated for the array using the delete[] operator.

Null Pointers

A null pointer is a pointer that does not point to any memory address. It is represented by the literal value nullptr (or NULL in older versions of C++).

A null pointer often indicates that a pointer does not currently point to a valid object or memory location. For example, if you dynamically allocate memory using the new operator and the allocation fails, the new operator returns a null pointer to indicate that no memory was allocated.

Here’s an example of using a null pointer:

int *ptr = nullptr;
if (ptr == nullptr) {
    // ptr is a null pointer
}

In this example, we declare a pointer ptr and initialize it to a null pointer using the nullptr literal. We then check if ptr is equal to nullptr using the equality operator (==). Since ptr is a null pointer, the condition is true and the code inside the if statement is executed.

It’s important to note that dereferencing a null pointer (i.e., accessing the memory location it points to) results in undefined behaviour, which can cause your program to crash or behave unpredictably. Therefore, you should always check if a pointer is null before dereferencing it.

Dangling Pointer

In C++, a dangling pointer is a pointer that points to a memory location that has been deallocated or otherwise freed. Dereferencing a dangling pointer can result in undefined behaviour, which can cause your program to crash or behave unpredictably.

Here’s an example of a dangling pointer:

int *foo() {
    int x = 5;
    int *ptr = &x;
    return ptr;
}

int main() {
    int *ptr = foo();
    // ptr points to a memory location that has been deallocated
    *ptr = 10; // undefined behavior
    return 0;
}

In this example, the foo function declares an integer x and a pointer ptr that points to x. The function then returns ptr. In main, we call foo and assign the returned pointer to ptr. However, x is a local variable inside foo, meaning its memory is deallocated when the function returns. Therefore, ptr becomes a dangling pointer that points to a memory location that has been deallocated. We get undefined behaviour when we try to dereference ptr and set its value to 10.

To avoid dangling pointers, you should always ensure that a pointer points to a valid memory location before dereferencing it. You should also avoid returning pointers to local variables or freeing memory still used by a pointer.

Memory Leaks

A memory leak occurs when the memory that has been dynamically allocated on the heap is not freed when it is no longer needed. This can happen when a program loses track of a pointer to the allocated memory or terminates without freeing the memory.

Memory leaks can cause your program to consume more and more memory over time, eventually leading to performance issues or crashes. They can also cause your program to run out of memory and fail to allocate more memory when needed.

Here’s an example of a memory leak:

int main() {
    while (true) {
        int *ptr = new int;
        // do something with ptr
    }
    return 0;
}

In this example, we declare a pointer ptr and allocate memory for an integer on the heap using the new operator inside an infinite loop. We then do something with ptr, but we never free the memory allocated for the integer using the delete operator. Therefore, each iteration of the loop allocates more memory on the heap, eventually causing the program to run out of memory.

To avoid memory leaks, you should always free dynamically allocated memory when it is no longer needed using the delete operator. You should also avoid losing track of pointers to allocated memory and free all allocated memory before your program terminates.

Smart Pointers

Smart pointers are a type of pointer in C++ that provides automatic memory management for dynamically allocated memory. They are designed to help prevent memory leaks and dangling pointers by automatically freeing memory when it is no longer needed.

There are two types of smart pointers in C++: unique_ptr and shared_ptr.

A unique_ptr is a smart pointer that owns the memory it points to. It ensures that the memory is automatically freed when the unique_ptr goes out of scope or is reset. A unique_ptr cannot be copied, but it can be moved.

Here’s an example of using a unique_ptr:

#include <memory>

int main() {
    std::unique_ptr<int> ptr(new int);
    *ptr = 5;
    // ptr automatically frees the memory when it goes out of scope
    return 0;
}

In this example, we declare a unique_ptr ptr that points to dynamically allocated memory for an integer. We set the value of the integer to 5 using the pointer. When ptr goes out of scope at the end of main, it automatically frees the memory allocated for the integer.

A shared_ptr is a smart pointer that allows multiple pointers to share ownership of the same memory. It keeps track of the number of pointers that point to the memory and automatically frees the memory when the last pointer goes out of scope or is reset.

Here’s an example of using a shared_ptr:

#include <memory>

int main() {
    std::shared_ptr<int> ptr1(new int);
    std::shared_ptr<int> ptr2 = ptr1;
    // ptr1 and ptr2 share ownership of the memory
    *ptr1 = 5;
    *ptr2 = 10;
    // memory is automatically freed when both ptr1 and ptr2 go out of scope
    return 0;

In this example, we declare two shared_ptrs ptr1 and ptr2 that both point to dynamically allocated memory for an integer. Since they share ownership of the memory, they both point to the same memory location. We set the value of the integer to 5 using ptr1 and 10 using ptr2. When both ptr1 and ptr2 go out of scope at the end of main, they automatically free the memory allocated for the integer.

Unique & Shared Pointers

The main difference between unique_ptr and shared_ptr in C++ is how they manage ownership of dynamically allocated memory.

A unique_ptr is a smart pointer that owns the memory it points to. It ensures that the memory is automatically freed when the unique_ptr goes out of scope or is reset. A unique_ptr cannot be copied, but it can be moved. This means that there can only be one unique_ptr that owns a particular piece of memory at any given time.

A shared_ptr is a smart pointer that allows multiple pointers to share ownership of the same memory. It keeps track of the number of pointers that point to the memory and automatically frees the memory when the last pointer goes out of scope or is reset. This means that there can be multiple shared_ptrs that own the same piece of memory at the same time.

Here’s an example that demonstrates the difference between unique_ptr and shared_ptr:

#include <memory>

int main() {
    std::unique_ptr<int> ptr1(new int);
    std::unique_ptr<int> ptr2 = ptr1; // error: unique_ptr cannot be copied
    std::shared_ptr<int> ptr3(new int);
    std::shared_ptr<int> ptr4 = ptr3; // ptr3 and ptr4 share ownership of the memory
    return 0;
}

In this example, we declare two unique_ptrs ptr1 and ptr2 that both point to dynamically allocated memory for an integer. Since unique_ptr cannot be copied, we get a compilation error when we try to copy ptr1 to ptr2. We also declare two shared_ptrs ptr3 and ptr4 that both point to dynamically allocated memory for an integer. Since shared_ptr allows multiple pointers to share ownership of the same memory, ptr3 and ptr4 both point to the same memory location.

In general, you should use unique_ptr when you want to ensure that a piece of memory is owned by only one pointer at a time, and shared_ptr when you want to allow multiple pointers to share ownership of the same memory.

Weak Pointers

In C++, a weak pointer is a smart pointer that holds a non-owning (“weak”) reference to an object managed by a shared_ptr. Unlike a shared_ptr, a weak pointer does not increment the reference count of the object it points to, so it does not prevent the object from being deleted.

Weak pointers are often used to break circular references between objects managed by shared_ptrs. Circular references can occur when two or more objects hold shared_ptrs to each other, preventing them from being deleted even when they are no longer needed.

Here’s an example of using a weak pointer:

#include <memory>

class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
};

class B {
public:
    std::weak_ptr<A> a_ptr;
};

int main() {
    std::shared_ptr<A> a_ptr(new A);
    std::shared_ptr<B> b_ptr(new B);
    a_ptr->b_ptr = b_ptr;
    b_ptr->a_ptr = a_ptr;
    // a_ptr and b_ptr hold shared_ptrs to each other, creating a circular reference
    return 0;
}

In this example, we declare two classes A and B that hold shared_ptrs to each other. In main, we create shared_ptrs a_ptr and b_ptr for objects of type A and B, respectively. We then set a_ptr->b_ptr to b_ptr and b_ptr->a_ptr to a_ptr, creating a circular reference between the two objects. Since each object holds a shared_ptr to the other, they cannot be deleted even when they are no longer needed.

To break the circular reference, we can use a weak_ptr instead of a shared_ptr in B:

#include <memory>

class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
};

class B {
public:
    std::weak_ptr<A> a_ptr;
};

int main() {
    std::shared_ptr<A> a_ptr(new A);
    std::shared_ptr<B> b_ptr(new B);
    a_ptr->b_ptr = b_ptr;
    b_ptr->a_ptr = a_ptr;
    // a_ptr and b_ptr hold shared_ptrs and weak_ptrs to each other, breaking the circular reference
    return 0;
}

In this example, we declare b_ptr->a_ptr as a weak_ptr instead of a shared_ptr. Since a weak_ptr does not increment the reference count of the object it points to, it does not prevent the object from being deleted. This breaks the circular reference between A and B, allowing them to be deleted when they are no longer needed.

RAW Pointers

A weak pointer is a smart pointer that holds a non-owning (“weak”) reference to an object that is managed by a shared_ptr, while a raw pointer is a simple pointer that does not provide any automatic memory management.

A weak pointer is designed to help prevent circular references between objects that are managed by shared_ptrs. It does not increment the reference count of the object it points to, which means that it does not prevent the object from being deleted. This makes it useful for breaking circular references between objects that hold shared_ptrs to each other.

A raw pointer, on the other hand, does not provide any automatic memory management. It simply holds a memory address that points to an object. This means that it does not protect against memory leaks or dangling pointers, and it can be dangerous to use if not used carefully.

Here’s an example that demonstrates the difference between a weak pointer and a raw pointer:

#include <memory>

class A {
public:
    int x;
};

int main() {
    std::shared_ptr<A> a_ptr(new A);
    std::weak_ptr<A> weak_a_ptr = a_ptr;
    A *raw_a_ptr = a_ptr.get();
    a_ptr.reset();
    // weak_a_ptr is still valid, but raw_a_ptr is now a dangling pointer
    return 0;
}

In this example, we declare a shared_ptr a_ptr that points to an object of type A. We then declare a weak_ptr weak_a_ptr and a raw pointer raw_a_ptr that both point to the same object. We then reset a_ptr, which deletes the object and frees its memory. Since weak_a_ptr is a weak pointer, it is still valid even though the object has been deleted. However, raw_a_ptr is now a dangling pointer that points to memory that has been freed, which can cause undefined behavior if it is dereferenced.

In general, you should use a weak pointer when you want to hold a non-owning reference to an object that is managed by a shared_ptr, and a raw pointer when you need a simple pointer to an object but do not need any automatic memory management. However, you should always be careful when using raw pointers to avoid memory leaks and dangling pointers.

The last byte…

In conclusion, pointers are a powerful feature of C++ that allows you to manipulate memory directly and create complex data structures. However, they can also be a source of bugs and errors if not used carefully. It’s important to understand the different types of pointers in C++, including raw pointers, smart pointers, and weak pointers, and how they can be used to manage memory and prevent memory leaks and dangling pointers.

When using pointers, it’s important to follow best practices such as initialising pointers to null, checking for null pointers before dereferencing them, and avoiding dangling references. You should also be careful when using pointers to avoid memory leaks and undefined behaviour.

Overall, pointers are essential to C++ programming, and mastering their use can help you create efficient and robust programs. By understanding the different types of pointers and following best practices, you can use pointers effectively and avoid common pitfalls

Ali Kayani

https://www.linkedin.com/in/ali-kayani-silvercoder007/

Post navigation

Queues

Leave a Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

Building High-Performance Market Data Processors in C++

Combining Mean Reversion And Momentum In The FOREX Market.

Neural Networks, an introduction and implementation in C++

Implementing the “first mode” with Scipy