Two Sides of the Same Coin: Understanding Arrays and Pointers in C

In C, arrays and pointers are intimately connected—so much so that they can often be used interchangeably. The name of an array acts like a constant pointer to its first element, and pointer arithmetic provides an alternative way to access array elements. Understanding this relationship is crucial for mastering C, as it affects everything from simple array traversal to complex data structures and function interfaces.

The Fundamental Relationship

The key insight is that when you declare an array, the array name represents the address of the first element. This means:

arr[i] == *(arr + i)

Let's see this in action:

#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
printf("Array name as pointer:\n");
printf("arr = %p\n", (void*)arr);
printf("&arr[0] = %p\n", (void*)&arr[0]);
printf("\nAccessing elements:\n");
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d\t", i, arr[i]);
printf("*(arr + %d) = %d\t", i, *(arr + i));
printf("Address: %p\n", (void*)(arr + i));
}
return 0;
}

Output (addresses will vary):

Array name as pointer:
arr = 0x7ffc12345670
&arr[0] = 0x7ffc12345670
Accessing elements:
arr[0] = 10    *(arr + 0) = 10    Address: 0x7ffc12345670
arr[1] = 20    *(arr + 1) = 20    Address: 0x7ffc12345674
arr[2] = 30    *(arr + 2) = 30    Address: 0x7ffc12345678
arr[3] = 40    *(arr + 3) = 40    Address: 0x7ffc1234567c
arr[4] = 50    *(arr + 4) = 50    Address: 0x7ffc12345680

Arrays Are Not Pointers (Important Distinction)

Despite their close relationship, arrays and pointers are not the same thing:

#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
printf("sizeof(arr) = %lu bytes\n", sizeof(arr));  // Size of entire array
printf("sizeof(ptr) = %lu bytes\n", sizeof(ptr));  // Size of pointer
printf("\narr[2] = %d\n", arr[2]);
printf("ptr[2] = %d\n", ptr[2]);
// Can modify pointer
ptr++;
printf("After ptr++, *ptr = %d (arr[1])\n", *ptr);
// Cannot modify array name
// arr++;  // ERROR: arr is not a modifiable lvalue
return 0;
}

Output:

sizeof(arr) = 20 bytes
sizeof(ptr) = 8 bytes
arr[2] = 3
ptr[2] = 3
After ptr++, *ptr = 2 (arr[1])

Pointer Arithmetic with Arrays

Pointer arithmetic is the foundation of array access:

#include <stdio.h>
int main() {
int arr[] = {2, 4, 6, 8, 10, 12, 14, 16};
int *ptr = arr;
int *end = arr + 8;  // Points one past the last element
printf("Array traversal with pointer arithmetic:\n");
while (ptr < end) {
printf("Current: %d, Next offset: %ld bytes\n", 
*ptr, (char*)(ptr + 1) - (char*)ptr);
ptr++;
}
// Reset pointer
ptr = arr;
printf("\nAccess patterns:\n");
printf("*(ptr + 3) = %d\n", *(ptr + 3));
printf("ptr[3] = %d\n", ptr[3]);
printf("3[ptr] = %d (weird but works!)\n", 3[ptr]);  // Equivalent to *(3 + ptr)
return 0;
}

Output:

Array traversal with pointer arithmetic:
Current: 2, Next offset: 4 bytes
Current: 4, Next offset: 4 bytes
Current: 6, Next offset: 4 bytes
Current: 8, Next offset: 4 bytes
Current: 10, Next offset: 4 bytes
Current: 12, Next offset: 4 bytes
Current: 14, Next offset: 4 bytes
Current: 16, Next offset: 4 bytes
Access patterns:
*(ptr + 3) = 8
ptr[3] = 8
3[ptr] = 8 (weird but works!)

Passing Arrays to Functions

When you pass an array to a function, you're actually passing a pointer:

#include <stdio.h>
// These three declarations are equivalent
void printArray1(int arr[], int size) {
printf("sizeof(arr) in function: %lu bytes\n", sizeof(arr));  // Pointer size!
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
void printArray2(int *arr, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", *(arr + i));
}
printf("\n");
}
void printArray3(int arr[5], int size) {  // Size in brackets is ignored
for (int i = 0; i < size; i++) {
printf("%d ", *(arr + i));
}
printf("\n");
}
void modifyArray(int *arr, int size) {
for (int i = 0; i < size; i++) {
arr[i] *= 2;  // Modifies original array
}
}
int main() {
int numbers[] = {1, 2, 3, 4, 5};
int size = sizeof(numbers) / sizeof(numbers[0]);
printf("Original array: ");
printArray1(numbers, size);
modifyArray(numbers, size);
printf("After modification: ");
printArray2(numbers, size);
printf("\nArray in main, sizeof = %lu bytes\n", sizeof(numbers));
return 0;
}

Output:

Original array: 1 2 3 4 5 
After modification: 2 4 6 8 10 
Array in main, sizeof = 20 bytes
sizeof(arr) in function: 8 bytes

Multidimensional Arrays and Pointers

Multidimensional arrays add another level of pointer relationships:

#include <stdio.h>
int main() {
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
printf("matrix = %p (address of first row)\n", (void*)matrix);
printf("matrix[0] = %p (address of first element)\n", (void*)matrix[0]);
printf("&matrix[0][0] = %p\n", (void*)&matrix[0][0]);
printf("\nRow pointers:\n");
for (int i = 0; i < 3; i++) {
printf("matrix[%d] = %p\n", i, (void*)matrix[i]);
}
printf("\nElement access:\n");
printf("matrix[1][2] = %d\n", matrix[1][2]);
printf("*(*(matrix + 1) + 2) = %d\n", *(*(matrix + 1) + 2));
printf("*(matrix[1] + 2) = %d\n", *(matrix[1] + 2));
// Pointer to array (row pointer)
int (*rowPtr)[4] = matrix;  // Pointer to array of 4 ints
printf("\nRow pointer arithmetic:\n");
printf("rowPtr points to row %d\n", *rowPtr[0]);
rowPtr++;  // Move to next row
printf("After rowPtr++, points to row starting with %d\n", (*rowPtr)[0]);
return 0;
}

Output:

matrix = 0x7ffc12345670
matrix[0] = 0x7ffc12345670
&matrix[0][0] = 0x7ffc12345670
Row pointers:
matrix[0] = 0x7ffc12345670
matrix[1] = 0x7ffc12345680
matrix[2] = 0x7ffc12345690
Element access:
matrix[1][2] = 7
*(*(matrix + 1) + 2) = 7
*(matrix[1] + 2) = 7
Row pointer arithmetic:
rowPtr points to row 1
After rowPtr++, points to row starting with 5

Pointer to Pointer vs 2D Array

There's an important distinction between 2D arrays and arrays of pointers:

#include <stdio.h>
#include <stdlib.h>
int main() {
// Method 1: True 2D array (contiguous memory)
int arr2D[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// Method 2: Array of pointers (non-contiguous)
int *arrOfPtrs[3];
for (int i = 0; i < 3; i++) {
arrOfPtrs[i] = malloc(4 * sizeof(int));
for (int j = 0; j < 4; j++) {
arrOfPtrs[i][j] = i * 4 + j + 1;
}
}
printf("True 2D array memory layout:\n");
printf("arr2D = %p\n", (void*)arr2D);
printf("arr2D[0] = %p\n", (void*)arr2D[0]);
printf("arr2D[1] = %p\n", (void*)arr2D[1]);
printf("arr2D[2] = %p\n", (void*)arr2D[2]);
printf("Distance between rows: %ld bytes\n", 
(char*)arr2D[1] - (char*)arr2D[0]);
printf("\nArray of pointers memory layout:\n");
printf("arrOfPtrs = %p\n", (void*)arrOfPtrs);
printf("arrOfPtrs[0] = %p\n", (void*)arrOfPtrs[0]);
printf("arrOfPtrs[1] = %p\n", (void*)arrOfPtrs[1]);
printf("arrOfPtrs[2] = %p\n", (void*)arrOfPtrs[2]);
// Cleanup
for (int i = 0; i < 3; i++) {
free(arrOfPtrs[i]);
}
return 0;
}

Practical Examples

Example 1: Array Reversal with Pointers

#include <stdio.h>
void reverseArray(int *arr, int size) {
int *start = arr;
int *end = arr + size - 1;
while (start < end) {
// Swap using pointers
int temp = *start;
*start = *end;
*end = temp;
start++;
end--;
}
}
void printArray(int *arr, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8};
int size = sizeof(arr) / sizeof(arr[0]);
printf("Original: ");
printArray(arr, size);
reverseArray(arr, size);
printf("Reversed: ");
printArray(arr, size);
return 0;
}

Output:

Original: 1 2 3 4 5 6 7 8 
Reversed: 8 7 6 5 4 3 2 1

Example 2: Finding Array Boundaries

#include <stdio.h>
int isWithinArray(int *arr, int size, int *ptr) {
int *start = arr;
int *end = arr + size;  // One past the last element
return (ptr >= start && ptr < end);
}
int main() {
int numbers[] = {10, 20, 30, 40, 50};
int size = sizeof(numbers) / sizeof(numbers[0]);
int *valid = &numbers[2];
int *invalid = &numbers[10];  // Out of bounds
printf("Array starts at: %p\n", (void*)numbers);
printf("Array ends at:   %p\n", (void*)(numbers + size));
printf("Valid pointer:   %p\n", (void*)valid);
printf("Invalid pointer: %p\n", (void*)invalid);
printf("\nValid pointer within array: %s\n", 
isWithinArray(numbers, size, valid) ? "Yes" : "No");
printf("Invalid pointer within array: %s\n", 
isWithinArray(numbers, size, invalid) ? "Yes" : "No");
return 0;
}

Example 3: Matrix Multiplication with Pointers

#include <stdio.h>
#include <stdlib.h>
void matrixMultiply(int *A, int *B, int *C, 
int m, int n, int p) {
for (int i = 0; i < m; i++) {
for (int j = 0; j < p; j++) {
int sum = 0;
for (int k = 0; k < n; k++) {
// A[i][k] * B[k][j]
sum += *(A + i * n + k) * *(B + k * p + j);
}
*(C + i * p + j) = sum;
}
}
}
void printMatrix(int *matrix, int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%4d ", *(matrix + i * cols + j));
}
printf("\n");
}
}
int main() {
int A[2][3] = {{1, 2, 3}, {4, 5, 6}};
int B[3][2] = {{7, 8}, {9, 10}, {11, 12}};
int C[2][2];
printf("Matrix A (2x3):\n");
printMatrix(&A[0][0], 2, 3);
printf("\nMatrix B (3x2):\n");
printMatrix(&B[0][0], 3, 2);
matrixMultiply(&A[0][0], &B[0][0], &C[0][0], 2, 3, 2);
printf("\nResult C = A * B (2x2):\n");
printMatrix(&C[0][0], 2, 2);
return 0;
}

Output:

Matrix A (2x3):
1    2    3 
4    5    6 
Matrix B (3x2):
7    8 
9   10 
11   12 
Result C = A * B (2x2):
58   64 
139  154

Example 4: String Array with Pointers

#include <stdio.h>
#include <string.h>
void sortStrings(char *strings[], int count) {
for (int i = 0; i < count - 1; i++) {
for (int j = i + 1; j < count; j++) {
if (strcmp(strings[i], strings[j]) > 0) {
// Swap pointers, not strings
char *temp = strings[i];
strings[i] = strings[j];
strings[j] = temp;
}
}
}
}
void printStrings(char *strings[], int count) {
for (int i = 0; i < count; i++) {
printf("  %s\n", strings[i]);
}
}
int main() {
char *fruits[] = {
"Banana",
"Apple",
"Orange",
"Mango",
"Grape"
};
int count = sizeof(fruits) / sizeof(fruits[0]);
printf("Original order:\n");
printStrings(fruits, count);
sortStrings(fruits, count);
printf("\nSorted order:\n");
printStrings(fruits, count);
// Memory layout
printf("\nPointer array memory:\n");
for (int i = 0; i < count; i++) {
printf("fruits[%d] = %p -> \"%s\"\n", 
i, (void*)fruits[i], fruits[i]);
}
return 0;
}

Output:

Original order:
Banana
Apple
Orange
Mango
Grape
Sorted order:
Apple
Banana
Grape
Mango
Orange
Pointer array memory:
fruits[0] = 0x400624 -> "Apple"
fruits[1] = 0x40062b -> "Banana"
fruits[2] = 0x400646 -> "Grape"
fruits[3] = 0x400637 -> "Mango"
fruits[4] = 0x400632 -> "Orange"

Example 5: Dynamic 2D Array with Pointers

#include <stdio.h>
#include <stdlib.h>
int** createMatrix(int rows, int cols) {
int **matrix = malloc(rows * sizeof(int*));
for (int i = 0; i < rows; i++) {
matrix[i] = malloc(cols * sizeof(int));
}
return matrix;
}
void freeMatrix(int **matrix, int rows) {
for (int i = 0; i < rows; i++) {
free(matrix[i]);
}
free(matrix);
}
void fillMatrix(int **matrix, int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
matrix[i][j] = i * cols + j + 1;
}
}
}
void printMatrixPtr(int **matrix, int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%4d ", matrix[i][j]);
}
printf("\n");
}
}
int main() {
int rows = 3, cols = 4;
int **matrix = createMatrix(rows, cols);
fillMatrix(matrix, rows, cols);
printf("Dynamic 2D array:\n");
printMatrixPtr(matrix, rows, cols);
printf("\nMemory layout:\n");
for (int i = 0; i < rows; i++) {
printf("matrix[%d] = %p -> row data at %p\n", 
i, (void*)&matrix[i], (void*)matrix[i]);
}
freeMatrix(matrix, rows);
return 0;
}

Common Array-Pointer Idioms

1. Iterating with Pointers

#include <stdio.h>
int sumArray(int *arr, int size) {
int sum = 0;
int *end = arr + size;
for (int *p = arr; p < end; p++) {
sum += *p;
}
return sum;
}
int main() {
int numbers[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int size = sizeof(numbers) / sizeof(numbers[0]);
printf("Sum: %d\n", sumArray(numbers, size));
return 0;
}

2. Finding Array Length

#include <stdio.h>
// Works only for actual arrays, not pointers
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0]))
void processArray(int arr[], int size) {
// In function, arr is a pointer, so can't use macro
for (int i = 0; i < size; i++) {
// process arr[i]
}
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
printf("Array size: %lu\n", ARRAY_SIZE(arr));  // Works
int *ptr = arr;
printf("Pointer 'size': %lu\n", ARRAY_SIZE(ptr));  // Wrong! Gives 1 or 2
return 0;
}

3. Pointer Difference for Index

#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50, 60, 70};
int *ptr = &arr[3];  // Points to 40
// Find index using pointer arithmetic
int index = ptr - arr;
printf("ptr points to arr[%d] = %d\n", index, *ptr);
// Traverse from pointer
printf("From pointer to end: ");
for (int *p = ptr; p < arr + 7; p++) {
printf("%d ", *p);
}
printf("\n");
return 0;
}

Common Pitfalls

1. Confusing Array of Pointers with 2D Array

#include <stdio.h>
#include <stdlib.h>
int main() {
// This is a 2D array (contiguous)
int arr2D[3][3];
// This is an array of pointers (non-contiguous)
int *ptrArray[3];
for (int i = 0; i < 3; i++) {
ptrArray[i] = malloc(3 * sizeof(int));
}
printf("Size of arr2D: %lu bytes\n", sizeof(arr2D));
printf("Size of ptrArray: %lu bytes\n", sizeof(ptrArray));
// They are accessed differently in functions
// void func(int arr[][3])      // For arr2D
// void func(int **arr)          // For ptrArray (not the same!)
// Cleanup
for (int i = 0; i < 3; i++) {
free(ptrArray[i]);
}
return 0;
}

2. Assuming Pointer Arithmetic Works on void*

#include <stdio.h>
int main() {
int arr[] = {1, 2, 3, 4, 5};
void *vp = arr;
// vp++;  // ERROR in standard C (GCC extension allows it)
// Must cast first
int *ip = (int*)vp;
ip++;
printf("Second element: %d\n", *ip);
return 0;
}

3. Forgetting That Array Parameters Are Pointers

#include <stdio.h>
void badFunction(int arr[]) {
// This doesn't work as expected
int size = sizeof(arr) / sizeof(arr[0]);  // arr is a pointer!
printf("In function, size = %d (wrong!)\n", size);
}
void goodFunction(int arr[], int size) {
printf("In function, with passed size = %d\n", size);
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
badFunction(arr);
goodFunction(arr, size);
return 0;
}

Performance Considerations

Pointer arithmetic can be more efficient than array indexing:

#include <stdio.h>
#include <time.h>
#define SIZE 10000000
int main() {
static int arr[SIZE];
clock_t start, end;
// Initialize
for (int i = 0; i < SIZE; i++) {
arr[i] = i;
}
// Array indexing
start = clock();
int sum1 = 0;
for (int i = 0; i < SIZE; i++) {
sum1 += arr[i];
}
end = clock();
double time1 = (double)(end - start) / CLOCKS_PER_SEC;
// Pointer arithmetic
start = clock();
int sum2 = 0;
int *ptr = arr;
int *endPtr = arr + SIZE;
while (ptr < endPtr) {
sum2 += *ptr++;
}
end = clock();
double time2 = (double)(end - start) / CLOCKS_PER_SEC;
printf("Array indexing: %.3f seconds\n", time1);
printf("Pointer arithmetic: %.3f seconds\n", time2);
return 0;
}

Summary Table

OperationArray SyntaxPointer Syntax
Access element iarr[i]*(ptr + i)
Address of element i&arr[i]ptr + i
First elementarr[0]*ptr
Move to nextN/Aptr++
Sizesizeof(arr)sizeof(ptr)
Can be modified?NoYes
Pass to functionfunc(arr)func(ptr)

Best Practices

  1. Use array syntax for static arrays - More readable
  2. Use pointer arithmetic for traversal - Often more efficient
  3. Pass size explicitly - Arrays decay to pointers in functions
  4. Be consistent - Mixing styles can confuse
  5. Use const when appropriate - Protect data that shouldn't change
  6. Check boundaries - Prevent buffer overflows
  7. Understand the differences - Arrays and pointers are not the same

Conclusion

The relationship between arrays and pointers is fundamental to C programming. While they can often be used interchangeably, understanding their differences is crucial:

  • Arrays are fixed-size blocks of memory; the array name is a constant address
  • Pointers are variables that store addresses; they can be modified
  • Array indexing arr[i] is syntactic sugar for pointer arithmetic *(arr + i)
  • Passing arrays to functions passes a pointer, losing size information
  • Multidimensional arrays have more complex pointer relationships

Mastering the array-pointer relationship enables you to:

  • Write more efficient code
  • Implement complex data structures
  • Interface with system APIs
  • Understand C's memory model
  • Debug pointer-related issues

This duality is what makes C both powerful and challenging. Embrace it, and you'll unlock the full potential of the language.

Leave a Reply

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


Macro Nepal Helper