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 feedback@compilenrun.com. I’d greatly appreciate your feedback! :)