Pointers in C / C++

Pointer is the most notorious but most powerful syntax/keyword in C/C++. Although the concept of pointer is simple that pointer simply points to something. As pointer can points actually everything and it could work outside of code scopes, it causes complexities. In addition to this, with combining dynamic memory allocation (heap allocation), it often causes serious problem in memory leakages, dangling pointer, buffer overflow and securities (e.g. stack smashing).

Table of contents

Basic Pointer

Here’s C / C++ basic raw-pointer. In this example, we simply points the stack memories. If you follow comments and compare them to the outputs, you can easily follow how the pointer can be used in stack memory.

#include <iostream>

void CStylePointer() {
    std::cout << "\n\n==== BASIC C-style RAW POINTER - Declaration ====\n";
    
    int int1 = 13;
    int int2 = 5;

    int* int1Ptr = &int1;
    int* int2Ptr = &int2;

    // Even if another pointer points to the same value, it points to the same address of int1,
    // but the pointer location itself is different.
    int* int3Ptr = &int1; 

    std::cout << "int1Ptr address: " << &int1Ptr << "\n";
    std::cout << "int2Ptr address: " << &int2Ptr << "\n";
    std::cout << "int3Ptr address: " << &int3Ptr << "\n";

    std::cout << "\n\n==== BASIC C-style RAW POINTER - Swap Values ====\n";
    
    std::cout << "Before swap:\n";
    std::cout << "int1Ptr points to " << int1Ptr << " with value: " << *int1Ptr << "\n";
    std::cout << "int2Ptr points to " << int2Ptr << " with value: " << *int2Ptr << "\n";

    // Swapping values
    int tempInt = *int1Ptr;  
    *int1Ptr = *int2Ptr;  
    *int2Ptr = tempInt;  

    std::cout << "After swap:\n";
    std::cout << "int1Ptr points to " << int1Ptr << " with value: " << *int1Ptr << "\n";
    std::cout << "int2Ptr points to " << int2Ptr << " with value: " << *int2Ptr << "\n";

    std::cout << "int3Ptr still points to int1, so its value also changed: " << *int3Ptr << "\n";


    std::cout << "\n\n==== BASIC C-style RAW POINTER - 1D Array and Pointer Arithmetic ====\n";

    int array1D[5] = {0, 1, 2, 3, 4};
    int* arr1DPtr = array1D;
    int* arr1DPtrAlias = &array1D[0];

    std::cout << "arr1DPtr : " << arr1DPtr << '\n';
    std::cout << "arr1DPtrAlias : " << arr1DPtrAlias << '\n';

    // Pointer Arithmetic
    std::cout << "Pointer arithmetic:\n";
    std::cout << "arr1DPtr is at address: " << arr1DPtr << " with value: " << *arr1DPtr << '\n';
    arr1DPtr++;  
    std::cout << "After ++arr1DPtr, new address: " << arr1DPtr << " with value: " << *arr1DPtr << '\n';

    std::cout << "Value at arr1DPtr + 2: " << *(arr1DPtr + 2) << " (moves forward by 2 integers)\n";

    // Iterate through array using pointer arithmetic
    std::cout << "Iterating array using pointer arithmetic: ";
    for (int offset = 0; offset < 5; ++offset) {
        std::cout << *(array1D + offset) << " ";
    }
    std::cout << "\n";


    std::cout << "\n\n==== BASIC C-style RAW POINTER - 2D Array ====\n";

    int array2D[2][5] = {
        {0, 1, 2, 3, 4},
        {5, 6, 7, 8, 9}
    };

    std::cout << "**array2D points to: " << **array2D << " (first element: 0)\n";
    std::cout << "**(array2D + 1) points to: " << **(array2D + 1) << " (first element of second row: 5)\n";
    std::cout << "*((*array2D) + 5) points to: " << *((*array2D) + 5) << " (linear memory access to second row first element: 5)\n";
    std::cout << "*(*(array2D + 1) + 2) points to: " << *(*(array2D + 1) + 2) << " (element at row 1, col 2: 7)\n";

    // Pointer Aliases for Rows
    int* arr2DPtr0 = array2D[0];
    int* arr2DPtr0Alias = *array2D;
    int* arr2DPtr1 = array2D[1];

    std::cout << "array2D base address: " << array2D << "\n";
    std::cout << "arr2DPtr0 (first row): " << arr2DPtr0 << "\n";
    std::cout << "arr2DPtr0Alias (alias for first row): " << arr2DPtr0Alias << "\n";
    std::cout << "arr2DPtr1 (second row): " << arr2DPtr1 << "\n";

    std::cout << "array2D[1] (second row): " << array2D[1] << "\n";
    std::cout << "(array2D[0] + 5) (next row start): " << (array2D[0] + 5) << "\n";
    std::cout << "(array2D + 1) (pointer to second row): " << (array2D + 1) << "\n";
}

int main()
{
    CStylePointer();
    return 0;
}

Dynamic Allocation with Pointers

When it comes to the dynamic allocation (or heap allocation), the pointer needs developer’s management. Unlike modern managed languages using garbage collections such as Python, JAVA and so on, developers needs to indicate when the memory is deleted explicitly.

In C-style, we use malloc (which refers to “memory allocate”) for allocating heap memory, and use free (which refers to “free memory”) for deallocating the existing memory. Those functions are in stdlib.h.

Note - malloc, calloc, realloc and alloca
malloc is generally used for memory allocation, while calloc clear the memory first then allocate. realloc often uses for resize allocation memory. and finally alloca is stack-heap memory allocation depends on the platforms (OS (that supports which compilers) / architecture).

In C++ style, we can use C-style allocation, however, we generally use built-in memory allocation keywords: new and delete.

In either style, C or C++, A developer must use pointer to access memory and manage the memory with pointer.

Of course the best option is writing code only using stack memory (which is controlled by the code block), however, unfortunately, we generally needs large memory space in most programming.

RAII (Resource Acquisition Is Initialization) pattern might be the best pattern that using heap memory effectively by mimicing the stack behavior (automatic deallocation of memory when the code block is ended and even unexpectedly ended).

In C++, std::memory like libraries supports RAII pattern such as std::unique_ptr, std::shared_ptr and std::weak_ptr. In most case, using std::unique_ptr is effectively used for without concerns, however, there is no free lunch, most big tech companies using C++ have their own std::memory-like library for their own purposes: some of them are more debuggable or much more safer or much more powerful.

C-style Dynamic Allocation

#include <iostream>
#include <cstdlib> // <stdlib.h>

void CStyleDynamicAllocation() {
    std::cout << "\n\n==== C-style Dynamic Allocation ====\n";

    // Allocate memory for an integer
    int* intPtr = (int*)malloc(sizeof(int));
    if (intPtr == nullptr)
    {
        std::cerr << "Memory allocation failed!\n";
        return;
    }
    *intPtr = 123;
    std::cout << "Allocated integer value: " << *intPtr << "\n";

    // Free the allocated memory
    free(intPtr);
    std::cout << "Freed integer memory.\n";

    // Allocate memory for an array of integers
    int* arrayPtr = (int*)malloc(5 * sizeof(int));
    if (arrayPtr == nullptr)
    {
        std::cerr << "Memory allocation failed for array!\n";
        return;
    }
    for (int i = 0; i < 5; ++i)
    {
        arrayPtr[i] = i * 10;
    }
    std::cout << "Allocated array values: ";
    for (int i = 0; i < 5; ++i)
    {
        std::cout << arrayPtr[i] << " ";
    }
    std::cout << "\n";

    // Free the allocated array memory
    free(arrayPtr);
    std::cout << "Freed array memory.\n";
}

int main() {
    CStyleDynamicAllocation();
    return 0;
}

C++ - RAII Dynamic Allocation

The following UniquePtr is mocking of std::unique_ptr in standard library, that make a pointer delete its occupation of memory automatically when the code block ends.

#include <iostream>
#include <functional> // for C++ style function pointers (callbacks)

template <typename T, typename Deleter = std::function<void(T*)>>
class UniquePtr
{
public:
    // Constructor (default deleter uses `delete`)
    // for default value of deleter, set a normal function that deletes pointer : delete p;
    explicit UniquePtr(T* ptr = nullptr, Deleter deleter = [](T* p) { delete p; })
        : m_Ptr(ptr)
        , m_Deleter(deleter)
        {}

    // Destructor
    ~UniquePtr()
    {
        if (m_Ptr)
        {
            m_Deleter(m_Ptr);
            std::cout << "[DEBUG] Deleted object at " << m_Ptr << std::endl;
        }
    }

    // Delete Copy Constructor & Copy Assignment
    UniquePtr(const UniquePtr&) = delete;
    UniquePtr& operator=(const UniquePtr&) = delete;

    // Move Constructor
    UniquePtr(UniquePtr&& other) noexcept
        : m_Ptr(other.m_Ptr), m_Deleter(std::move(other.m_Deleter))
    {
        other.m_Ptr = nullptr;
    }

    // Move Assignment
    UniquePtr& operator=(UniquePtr&& other) noexcept
    {
        if (this != &other)
        {
            Reset(); // Delete current resource
            m_Ptr = other.m_Ptr;
            m_Deleter = std::move(other.m_Deleter);
            other.m_Ptr = nullptr;
        }
        return *this;
    }

    // Overloaded Operators
    T& operator*() const { return *m_Ptr; }
    T* operator->() const { return m_Ptr; }

    // Getter
    T* Get() const { return m_Ptr; }

    // Reset function
    void Reset(T* newPtr = nullptr)
    {
        if (m_Ptr)
        {
            m_Deleter(m_Ptr);
            std::cout << "[DEBUG] Reset deleted object at " << m_Ptr << std::endl;
        }
        m_Ptr = newPtr;
    }

    // Release function (returns the raw pointer and removes ownership)
    T* Release()
    {
        T* temp = m_Ptr;
        m_Ptr = nullptr;
        return temp;
    }

private:
    T* m_Ptr;
    Deleter m_Deleter;
};

// for Arrays
template <typename T, typename Deleter>
class UniquePtr<T[], Deleter>
{
public:
    // Constructor
    explicit UniquePtr(T* ptr = nullptr, Deleter deleter = [](T* p) { delete[] p; })
        : m_Ptr(ptr)
        , m_Deleter(deleter)
    {}

    // Destructor
    ~UniquePtr()
    {
        if (m_Ptr)
        {
            m_Deleter(m_Ptr);
            std::cout << "[DEBUG] Deleted array at " << m_Ptr << std::endl;
        }
    }

    // Delete Copy Constructor & Copy Assignment
    UniquePtr(const UniquePtr&) = delete;
    UniquePtr& operator=(const UniquePtr&) = delete;

    // Move Constructor
    UniquePtr(UniquePtr&& other) noexcept
        : m_Ptr(other.m_Ptr), m_Deleter(std::move(other.m_Deleter))
    {
        other.m_Ptr = nullptr;
    }

    // Move Assignment
    UniquePtr& operator=(UniquePtr&& other) noexcept
    {
        if (this != &other)
        {
            Reset();
            m_Ptr = other.m_Ptr;
            m_Deleter = std::move(other.m_Deleter);
            other.m_Ptr = nullptr;
        }
        return *this;
    }

    // Overloaded Operator (array-style access)
    T& operator[](size_t index) const { return m_Ptr[index]; }

    // Getter
    T* Get() const { return m_Ptr; }

    // Reset function
    void Reset(T* newPtr = nullptr)
    {
        if (m_Ptr)
        {
            m_Deleter(m_Ptr);
            std::cout << "[DEBUG] Reset deleted array at " << m_Ptr << std::endl;
        }
        m_Ptr = newPtr;
    }

    // Release function
    T* Release()
    {
        T* temp = m_Ptr;
        m_Ptr = nullptr;
        return temp;
    }

private:
    T* m_Ptr;
    Deleter m_Deleter;
};

int main()
{
    // Managing a single object with default deleter
    UniquePtr<int> singlePtr(new int(42));
    std::cout << "Single Value: " << *singlePtr << std::endl;

    // Managing a single object with a custom deleter
    UniquePtr<int> customDelPtr(
        new int(99),
        [](int* p)
        {
            std::cout << "[DEBUG] Custom deleter called for " << *p << std::endl;
            delete p;
        }
    );

    // Managing a dynamic array with default deleter
    UniquePtr<int[]> arrayPtr(new int[5]{1, 2, 3, 4, 5});
    std::cout << "Array Values: ";
    for (int i = 0; i < 5; ++i)
    {
        std::cout << arrayPtr[i] << " ";
    }
    std::cout << std::endl;

    // Resetting a pointer
    singlePtr.Reset(new int(77));
    std::cout << "After Reset: " << *singlePtr << std::endl;

    // Transferring ownership
    UniquePtr<int[]> movedArray = std::move(arrayPtr);
    if (!arrayPtr.Get())
    {
        std::cout << "Ownership transferred successfully!" << std::endl;
    }

    return 0;
}