Skip to content

Pointers & Free Store II

Video: Everything You Thought You Knew About Arrays is Wrong (in C Programming). This is Why.

Building on your understanding of pointers, let's explore how pointers work with arrays and the concept of pointer arithmetic.

Arrays and Pointers

In C++, arrays and pointers are closely related. When you declare an array, you're actually getting a pointer to the first element:

cpp
int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr;  // ptr points to the first element (1)

std::cout << *ptr << std::endl;      // 1
std::cout << arr[0] << std::endl;    // 1 (same thing!)

Key insight: arr is a pointer to the first element, and arr[i] is equivalent to *(arr + i).

Array Decay to Pointers

When you pass an array to a function or use it in certain expressions, it automatically "decays" to a pointer to its first element:

cpp
void printArray(int* arr, int size) {
    for (int i = 0; i < size; ++i) {
        std::cout << arr[i] << " ";
    }
}

int main() {
    int arr[5] = {1, 2, 3, 4, 5};

    // Array decays to pointer when passed to function
    printArray(arr, 5);  // arr decays to int*

    // Array decays to pointer in assignment
    int* ptr = arr;      // arr decays to int*

    // Array decays to pointer in arithmetic
    int* end = arr + 5;  // arr decays to int*
}

Why Does Array Decay Happen?

Array decay occurs because:

  1. Functions can't receive arrays by value - C++ would need to copy the entire array
  2. Arrays don't carry size information - the size is lost during decay
  3. Historical C compatibility - this behavior comes from C

Examples of Array Decay

cpp
int arr[5] = {1, 2, 3, 4, 5};

// 1. Function parameters
void func(int* ptr) { }  // arr decays to int*
func(arr);

// 2. Assignment
int* ptr = arr;  // arr decays to int*

// 3. Arithmetic expressions
int* end = arr + 5;  // arr decays to int*

// 4. Comparison
if (arr == nullptr) { }  // arr decays to int*

// 5. sizeof behavior changes
std::cout << sizeof(arr) << std::endl;    // 20 (5 * 4 bytes)
std::cout << sizeof(ptr) << std::endl;    // 8 (pointer size)

The sizeof Trap

A common mistake is using sizeof on a decayed array:

cpp
void printSize(int* arr) {
    // WRONG! This gives pointer size, not array size
    std::cout << "Size: " << sizeof(arr) << std::endl;  // Always 8 (or 4 on 32-bit)
}

int main() {
    int arr[5] = {1, 2, 3, 4, 5};

    std::cout << sizeof(arr) << std::endl;  // 20 (correct - array size)
    printSize(arr);                         // 8 (wrong - pointer size)
}

Solution: Always pass the size as a separate parameter.

Preventing Array Decay

You can prevent array decay in some cases:

cpp
// 1. Using references (C++ only)
void func(int (&arr)[5]) {
    // arr is still an array, not a pointer
    std::cout << "Size: " << sizeof(arr) << std::endl;  // 20
}

// 2. Using std::array (C++11)
#include <array>
std::array<int, 5> arr = {1, 2, 3, 4, 5};
// arr doesn't decay to pointer

// 3. Using templates
template<size_t N>
void func(int (&arr)[N]) {
    // N is the array size
    std::cout << "Size: " << N << std::endl;
}

Practical Implications

Understanding array decay is crucial because:

  1. Function design: You must pass size separately
  2. Memory safety: Decayed arrays lose size information
  3. Performance: No copying occurs during decay
  4. Debugging: sizeof behavior can be confusing

Pointer Arithmetic

Pointer arithmetic allows you to move through memory by adding or subtracting integers from pointers:

cpp
int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr;

std::cout << *ptr << std::endl;      // 10
std::cout << *(ptr + 1) << std::endl; // 20
std::cout << *(ptr + 2) << std::endl; // 30
std::cout << *(ptr - 1) << std::endl; // Undefined behavior!

Important rules:

  • Adding n to a pointer moves it n * sizeof(type) bytes forward
  • Subtracting n moves it backward
  • You can only access memory within the array bounds
  • Pointer arithmetic works with any pointer type

Array Access with Pointers

There are multiple ways to access array elements:

cpp
int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr;

// All of these are equivalent:
std::cout << arr[2] << std::endl;     // Array notation
std::cout << *(arr + 2) << std::endl; // Pointer arithmetic
std::cout << *(ptr + 2) << std::endl; // Pointer arithmetic with variable
std::cout << ptr[2] << std::endl;     // Pointer with array notation

Dynamic Arrays with Pointers

You can create dynamic arrays using pointers:

cpp
int size = 5;
int* dynamicArr = new int[size];

// Initialize with values
for (int i = 0; i < size; ++i) {
    dynamicArr[i] = i + 1;
}

// Access elements
for (int i = 0; i < size; ++i) {
    std::cout << *(dynamicArr + i) << " ";
}

deletedynamicArr;  // Don't forget to free memory!

Pointer Arithmetic Operations

Addition and Subtraction

cpp
int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr;

ptr = ptr + 2;  // Move 2 elements forward
std::cout << *ptr << std::endl;  // 30

ptr = ptr - 1;  // Move 1 element backward
std::cout << *ptr << std::endl;  // 20

Increment and Decrement

cpp
int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr;

std::cout << *ptr++ << std::endl;  // Print 10, then move to 20
std::cout << *ptr << std::endl;    // 20

std::cout << *++ptr << std::endl;  // Move to 30, then print 30
std::cout << *ptr << std::endl;    // 30

Pointer Differences

You can subtract two pointers to get the distance between them:

cpp
int arr[5] = {10, 20, 30, 40, 50};
int* start = arr;
int* end = arr + 5;

std::cout << "Distance: " << (end - start) << std::endl;  // 5
std::cout << "Elements between: " << (end - start) << std::endl;  // 5

Common Patterns

Iterating with Pointers

cpp
int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr;
int* end = arr + 5;

while (ptr < end) {
    std::cout << *ptr << " ";
    ++ptr;
}

Finding Array Length

cpp
int arr= {1, 2, 3, 4, 5};
int* end = arr + sizeof(arr) / sizeof(arr[0]);

std::cout << "Array size: " << (end - arr) << std::endl;  // 5

Safety Considerations

Bounds Checking

Always ensure you don't access memory outside the array:

cpp
int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr;

// Safe access
if (ptr + 2 < arr + 5) {
    std::cout << *(ptr + 2) << std::endl;  // Safe
}

// Dangerous access
std::cout << *(ptr + 10) << std::endl;  // Undefined behavior!

Null Pointer Checks

cpp
int* ptr = nullptr;

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

Your Tasks

Task 1: Array Reversal

Implement a function that reverses an array using pointer arithmetic. Use two pointers - one at the beginning and one at the end - and swap elements while moving them toward the center.

Task 2: Finding Maximum Element

Implement a function that finds the maximum element in an array using pointer arithmetic. Traverse the array using a pointer and keep track of the maximum value and its position.

Task 3: Array Copying

Implement a function that copies elements from one array to another using pointer arithmetic. Use two pointers to traverse both arrays simultaneously.

Key Takeaways

  • Arrays decay to pointers to their first element
  • Pointer arithmetic moves through memory by type size
  • arr[i] is equivalent to *(arr + i)
  • Always check bounds to avoid undefined behavior
  • Pointer arithmetic is powerful but requires careful memory management

Implement a function that finds the maximum element in an array using pointer arithmetic.

cpp
void findMax(int* arr, int size, int& maxValue, int& maxIndex) {
    // TODO: Implement function to find maximum element using pointer arithmetic
}