Skip to main content

C++ Pointers and Arrays

Arrays and pointers in C++ have a special relationship that often confuses beginners. In this guide, we'll explore how arrays and pointers interact, why they sometimes seem interchangeable, and how to use them effectively together.

Introduction

In C++, arrays and pointers are closely related concepts. Understanding this relationship is crucial for effective memory management, efficient data processing, and developing a deeper understanding of how C++ works "under the hood."

By the end of this tutorial, you'll understand:

  • The relationship between arrays and pointers
  • How array names decay to pointers
  • Pointer arithmetic with arrays
  • Multi-dimensional arrays and pointers
  • Common pitfalls and best practices

Arrays and Pointers: The Connection

In C++, when you declare an array, you're allocating a contiguous block of memory to store elements of the same type. The array name itself represents the memory address of the first element of the array.

cpp
int numbers[5] = {10, 20, 30, 40, 50};

The name numbers actually represents the memory address of the first element (numbers[0]). This is why arrays and pointers have such a close relationship.

Array Name as a Pointer

Let's verify this relationship with a simple example:

cpp
#include <iostream>
using namespace std;

int main() {
int numbers[5] = {10, 20, 30, 40, 50};

cout << "Array name (numbers): " << numbers << endl;
cout << "Address of first element (&numbers[0]): " << &numbers[0] << endl;

// Using pointer to access array elements
int* ptr = numbers; // No need for & operator
cout << "First element using pointer: " << *ptr << endl;

return 0;
}

Output:

Array name (numbers): 0x7ffee9ec35b0
Address of first element (&numbers[0]): 0x7ffee9ec35b0
First element using pointer: 10

Notice that the array name numbers and the address of the first element &numbers[0] print the same memory address. This confirms that the array name itself acts as a pointer to the first element.

Array Decay

When an array is used in most expressions, it "decays" into a pointer to its first element. This is called "array decay" and is a fundamental concept in C++.

However, there are important differences between arrays and pointers:

  1. An array is a contiguous block of memory with a fixed size determined at compile time.
  2. A pointer is just a variable that stores a memory address.

Let's see an example that demonstrates array decay:

cpp
#include <iostream>
using namespace std;

int main() {
int numbers[5] = {10, 20, 30, 40, 50};
int* ptr = numbers; // Array decays to pointer

cout << "Size of array: " << sizeof(numbers) << " bytes" << endl;
cout << "Size of pointer: " << sizeof(ptr) << " bytes" << endl;

return 0;
}

Output:

Size of array: 20 bytes
Size of pointer: 8 bytes

On a typical 64-bit system, the output shows that sizeof(numbers) is 20 bytes (5 integers × 4 bytes per integer), while sizeof(ptr) is 8 bytes (the size of a pointer). This demonstrates that numbers and ptr are different types despite the array decay.

Accessing Array Elements with Pointers

You can use pointers to access array elements in two ways:

  1. Pointer arithmetic
  2. Array indexing notation

Pointer Arithmetic

When you perform arithmetic on a pointer, the pointer moves by the size of its type. For example, incrementing an int* moves the pointer 4 bytes forward (assuming an int is 4 bytes).

cpp
#include <iostream>
using namespace std;

int main() {
int numbers[5] = {10, 20, 30, 40, 50};
int* ptr = numbers;

for (int i = 0; i < 5; i++) {
cout << "Element at index " << i << ": " << *ptr << endl;
ptr++; // Move to the next integer
}

return 0;
}

Output:

Element at index 0: 10
Element at index 1: 20
Element at index 2: 30
Element at index 3: 40
Element at index 4: 50

Array Indexing Notation

You can also use the array indexing notation with pointers:

cpp
#include <iostream>
using namespace std;

int main() {
int numbers[5] = {10, 20, 30, 40, 50};
int* ptr = numbers;

for (int i = 0; i < 5; i++) {
cout << "Element at index " << i << ": " << ptr[i] << endl;
}

return 0;
}

Output:

Element at index 0: 10
Element at index 1: 20
Element at index 2: 30
Element at index 3: 40
Element at index 4: 50

It's important to understand that arr[i] is actually syntactic sugar for *(arr + i). This is why array indexing works with pointers too.

Passing Arrays to Functions

When you pass an array to a function, what actually gets passed is a pointer to the first element of the array. This is another manifestation of array decay.

cpp
#include <iostream>
using namespace std;

// Function accepting an array
void printArray(int arr[], int size) {
cout << "Inside function, size of arr: " << sizeof(arr) << " bytes" << endl;

for (int i = 0; i < size; i++) {
cout << arr[i] << " ";
}
cout << endl;
}

// Function accepting a pointer
void printArrayWithPointer(int* ptr, int size) {
for (int i = 0; i < size; i++) {
cout << *(ptr + i) << " ";
}
cout << endl;
}

int main() {
int numbers[5] = {10, 20, 30, 40, 50};
cout << "In main, size of numbers: " << sizeof(numbers) << " bytes" << endl;

printArray(numbers, 5);
printArrayWithPointer(numbers, 5);

return 0;
}

Output:

In main, size of numbers: 20 bytes
Inside function, size of arr: 8 bytes
10 20 30 40 50
10 20 30 40 50

Notice that inside the function, sizeof(arr) returns 8 bytes (the size of a pointer) rather than the size of the entire array. This is because arr is actually a pointer, not an array.

Multi-dimensional Arrays and Pointers

Multi-dimensional arrays add another layer of complexity to the relationship between arrays and pointers.

2D Arrays

A 2D array can be visualized as an array of arrays:

cpp
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};

In memory, this 2D array is stored as a contiguous block of 12 integers. When working with pointers to 2D arrays, the type of pointer becomes important:

cpp
#include <iostream>
using namespace std;

int main() {
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};

// Pointer to the first 1D array
int (*rowPtr)[4] = matrix;

// Pointer to an integer (first element)
int* elemPtr = &matrix[0][0];

cout << "First element using rowPtr: " << (*rowPtr)[0] << endl;
cout << "First element using elemPtr: " << *elemPtr << endl;

// Access the second row, third column
cout << "Element at matrix[1][2]: " << *(*(matrix + 1) + 2) << endl;
cout << "Same element using indexing: " << matrix[1][2] << endl;

return 0;
}

Output:

First element using rowPtr: 1
First element using elemPtr: 1
Element at matrix[1][2]: 7
Same element using indexing: 7

The expression *(*(matrix + 1) + 2) may seem complex, but it breaks down as:

  1. matrix + 1 points to the second row
  2. *(matrix + 1) is the second row (an array of 4 integers)
  3. *(matrix + 1) + 2 points to the third element in the second row
  4. *(*(matrix + 1) + 2) is the value at that location

Dynamic Arrays with Pointers

One of the most practical applications of pointers with arrays is creating dynamic arrays whose size is determined at runtime.

cpp
#include <iostream>
using namespace std;

int main() {
int size;
cout << "Enter the size of the array: ";
cin >> size;

// Allocate memory for the dynamic array
int* dynamicArray = new int[size];

// Initialize the array
for (int i = 0; i < size; i++) {
dynamicArray[i] = i * 10;
}

// Access and print the array
cout << "Dynamic array elements: ";
for (int i = 0; i < size; i++) {
cout << dynamicArray[i] << " ";
}
cout << endl;

// Don't forget to deallocate the memory
delete[] dynamicArray;

return 0;
}

Sample Input:

5

Output:

Enter the size of the array: 5
Dynamic array elements: 0 10 20 30 40

This example shows how to:

  1. Allocate memory for an array of integers using new int[size]
  2. Use the pointer like an array with array indexing notation
  3. Free the memory with delete[] when done to prevent memory leaks

Common Pitfalls and Best Practices

1. Array Bounds Checking

C++ does not perform bounds checking on arrays. Accessing elements outside the array boundaries leads to undefined behavior.

cpp
int arr[5] = {1, 2, 3, 4, 5};
int value = arr[10]; // Undefined behavior! Out of bounds access

2. Memory Leaks with Dynamic Arrays

Always use delete[] (not delete) to free memory allocated with new[].

cpp
int* arr = new int[10];
// ... use the array
delete[] arr; // Correct way to free the memory

3. Dangling Pointers

After deleting an array, the pointer becomes a "dangling pointer." It's good practice to set it to nullptr after deletion.

cpp
int* arr = new int[10];
// ... use the array
delete[] arr;
arr = nullptr; // Good practice to avoid dangling pointer issues

4. Array Decay in Function Parameters

When passing arrays to functions, always pass the size along with the array, since the size information is lost during array decay.

cpp
void processArray(int arr[], int size) {
// Use the size parameter, not sizeof(arr)
for (int i = 0; i < size; i++) {
// process arr[i]
}
}

Real-world Example: Image Processing

Here's a simplified example of how pointers and arrays might be used in image processing:

cpp
#include <iostream>
using namespace std;

// Function to apply a simple blur filter to an image
void applyBlur(unsigned char* image, int width, int height) {
// Create a copy of the original image
unsigned char* temp = new unsigned char[width * height];
for (int i = 0; i < width * height; i++) {
temp[i] = image[i];
}

// Apply blur filter (simple averaging of neighboring pixels)
for (int y = 1; y < height - 1; y++) {
for (int x = 1; x < width - 1; x++) {
int index = y * width + x;

// Average of 9 pixels (3x3 kernel)
image[index] = (
temp[(y-1) * width + (x-1)] + temp[(y-1) * width + x] + temp[(y-1) * width + (x+1)] +
temp[y * width + (x-1)] + temp[y * width + x] + temp[y * width + (x+1)] +
temp[(y+1) * width + (x-1)] + temp[(y+1) * width + x] + temp[(y+1) * width + (x+1)]
) / 9;
}
}

delete[] temp;
}

int main() {
// Create a small 5x5 grayscale "image" (0-255 values)
const int width = 5;
const int height = 5;
unsigned char* image = new unsigned char[width * height];

// Initialize with a pattern
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
image[y * width + x] = (x + y) * 25;
}
}

// Print original image
cout << "Original image:" << endl;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
cout << static_cast<int>(image[y * width + x]) << "\t";
}
cout << endl;
}

// Apply blur
applyBlur(image, width, height);

// Print blurred image
cout << "\nBlurred image:" << endl;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
cout << static_cast<int>(image[y * width + x]) << "\t";
}
cout << endl;
}

delete[] image;
return 0;
}

Output:

Original image:
0 25 50 75 100
25 50 75 100 125
50 75 100 125 150
75 100 125 150 175
100 125 150 175 200

Blurred image:
0 25 50 75 100
25 56 75 94 125
50 75 100 125 150
75 106 125 144 175
100 125 150 175 200

In this example:

  1. The 2D image is stored as a 1D array using row-major order
  2. We use pointer arithmetic to access pixel values
  3. We allocate and deallocate memory for both the image and a temporary copy
  4. The blur filter demonstrates how to process a grid of data with pointers

Summary

The relationship between arrays and pointers in C++ is fundamental but can be confusing for beginners. Here's what we've learned:

  1. Array Decay: Array names decay to pointers to their first elements in most contexts.
  2. Pointer Arithmetic: Allows us to navigate arrays efficiently by understanding how pointer values change when incremented or decremented.
  3. Array Indexing: arr[i] is equivalent to *(arr + i).
  4. Function Parameters: Arrays are always passed by pointer, and size information is lost.
  5. Multi-dimensional Arrays: Require careful consideration of pointer types and dereferencing.
  6. Dynamic Memory: Pointers enable runtime-sized arrays with new[] and delete[].

Understanding these concepts will greatly improve your ability to work with arrays efficiently in C++ and help you avoid common memory-related bugs.

Practice Exercises

  1. Write a function that reverses an array in place using pointers.
  2. Create a function that concatenates two arrays into a new, dynamically allocated array.
  3. Implement a basic matrix multiplication algorithm using pointers and 2D arrays.
  4. Write a program that uses dynamic memory allocation to create a jagged array (an array of arrays where each sub-array has a different length).
  5. Create a simple image rotation function that rotates a 2D image array by 90 degrees.

Additional Resources

Remember that modern C++ often favors using std::array or std::vector over raw arrays and pointers for safety and convenience, but understanding the underlying array-pointer relationship remains essential for mastering C++.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)