C Programming: Low-Level Mastery
Arrays

Arrays & Pointers

Understand the deep relationship between arrays and pointers in C. Learn pointer arithmetic, array decay, how arrays are passed to functions, and the equivalence between array and pointer notation.

Arrays ARE Pointers (Almost)

In most contexts, an array name evaluates to a pointer to its first element. This is called "array decay." Understanding this relationship is crucial for mastering C - it explains function parameters, pointer arithmetic, and many array operations. However, arrays and pointers aren't identical - sizeof and & behave differently.

C
#include <stdio.h>

int main(void) {
    int arr[5] = {10, 20, 30, 40, 50};
    
    /* Array name is address of first element */
    printf("%p\n", arr);      // Address of arr[0]
    printf("%p\n", &arr[0]);  // Same address
    
    /* These are equivalent */
    printf("%d\n", arr[0]);   // 10
    printf("%d\n", *arr);     // 10 (dereference pointer)
    
    /* Pointer to array */
    int *ptr = arr;  // ptr points to first element
    
    /* Access elements through pointer */
    printf("%d\n", *ptr);       // 10 (arr[0])
    printf("%d\n", *(ptr + 1)); // 20 (arr[1])
    printf("%d\n", *(ptr + 2)); // 30 (arr[2])
    
    /* Array notation vs pointer notation */
    printf("%d\n", arr[2]);     // 30
    printf("%d\n", *(arr + 2)); // 30 (equivalent!)
    printf("%d\n", ptr[2]);     // 30 (works on pointers too!)
    printf("%d\n", 2[arr]);     // 30 (weird but valid!)
    
    return 0;
}

/* Key insight: arr[i] is syntactic sugar for *(arr + i) */

Pointer Arithmetic

Adding an integer to a pointer moves it by that many elements (not bytes!). The compiler automatically scales by the element size. This makes traversing arrays natural and efficient. Pointer arithmetic is the foundation of array indexing in C.

C
/* Pointer arithmetic */
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr;  // Points to arr[0]

/* Incrementing pointer */
printf("%d\n", *ptr);  // 10
ptr++;                  // Move to next element
printf("%d\n", *ptr);  // 20
ptr++;
printf("%d\n", *ptr);  // 30

/* Adding to pointer */
ptr = arr;             // Reset to start
printf("%d\n", *(ptr + 0));  // 10
printf("%d\n", *(ptr + 1));  // 20
printf("%d\n", *(ptr + 2));  // 30
printf("%d\n", *(ptr + 4));  // 50

/* Pointer arithmetic scales by element size */
int *p = arr;
printf("p: %p\n", (void*)p);        // e.g., 0x1000
printf("p+1: %p\n", (void*)(p+1));  // 0x1004 (4 bytes higher)
printf("p+2: %p\n", (void*)(p+2));  // 0x1008 (8 bytes higher)

/* Works with any type */
double darr[3] = {1.1, 2.2, 3.3};
double *dp = darr;
printf("dp: %p\n", (void*)dp);       // e.g., 0x2000
printf("dp+1: %p\n", (void*)(dp+1)); // 0x2008 (8 bytes for double)

/* Subtracting pointers gives element count */
int *start = arr;
int *end = arr + 5;
printf("Distance: %ld elements\n", end - start);  // 5

/* Comparing pointers */
int *p1 = arr;
int *p2 = arr + 3;
if (p2 &gt; p1) {
    printf("p2 is ahead of p1\n");
}

Array-Pointer Equivalence

Array subscript notation arr[i] and pointer notation *(arr+i) are completely equivalent. The compiler converts arr[i] to *(arr+i) internally. This means you can use array notation on pointers and pointer notation on arrays interchangeably.

C
/* These four expressions are IDENTICAL */
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr;

/* All print 30 (third element) */
printf("%d\n", arr[2]);      // Array notation on array
printf("%d\n", *(arr + 2));  // Pointer notation on array
printf("%d\n", ptr[2]);      // Array notation on pointer!
printf("%d\n", *(ptr + 2));  // Pointer notation on pointer

/* Even weirder but valid */
printf("%d\n", 2[arr]);      // Same as arr[2]!
printf("%d\n", 2[ptr]);      // Same as ptr[2]!

/* Why 2[arr] works:
   arr[2] becomes *(arr + 2)
   2[arr] becomes *(2 + arr) - addition is commutative!
   So 2[arr] == arr[2]
*/

/* Iterating with array notation */
for (int i = 0; i < 5; i++) {
    printf("%d ", arr[i]);
}
printf("\n");

/* Iterating with pointer notation */
for (int i = 0; i < 5; i++) {
    printf("%d ", *(arr + i));
}
printf("\n");

/* Iterating with pointer incrementation */
int *p = arr;
for (int i = 0; i < 5; i++) {
    printf("%d ", *p);
    p++;
}
printf("\n");

/* Iterating with pointer comparison */
int *start = arr;
int *end = arr + 5;
for (int *p = start; p < end; p++) {
    printf("%d ", *p);
}
printf("\n");

Arrays as Function Parameters

When you pass an array to a function, it "decays" to a pointer to the first element. The function receives a pointer, not a copy of the array. This is why functions can modify array contents and why sizeof doesn't work on array parameters - they're actually pointers.

C
/* These three function declarations are IDENTICAL */
void process1(int arr[]);
void process2(int arr[10]);  // Size ignored!
void process3(int *arr);     // Exactly the same

/* Function receiving array */
void modify_array(int arr[], int size) {
    /* arr is really a pointer here */
    printf("sizeof(arr): %zu\n", sizeof(arr));  // Size of pointer (8)
    
    /* Can modify original array */
    for (int i = 0; i < size; i++) {
        arr[i] *= 2;
    }
}

/* Calling function */
int main(void) {
    int numbers[5] = {1, 2, 3, 4, 5};
    
    printf("Before: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");
    
    /* Pass array (really passes pointer) */
    modify_array(numbers, 5);
    
    printf("After: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", numbers[i]);  // Modified!
    }
    printf("\n");
    
    return 0;
}

/* Why size parameter is needed */
void wrong(int arr[]) {
    /* Cannot determine array size here! */
    int size = sizeof(arr) / sizeof(arr[0]);  // WRONG
    // sizeof(arr) is sizeof(int*), not array size
}

void correct(int arr[], int size) {
    /* Must pass size separately */
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
}

/* Const array parameter (read-only) */
void print_array(const int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
        // arr[i] = 0;  // ERROR: Can't modify const
    }
}

Differences Between Arrays and Pointers

Despite their close relationship, arrays and pointers aren't identical. Understanding the differences prevents subtle bugs and helps you reason about C code correctly.

C
/* Difference 1: sizeof */
int arr[10];
int *ptr = arr;

printf("sizeof(arr): %zu\n", sizeof(arr));  // 40 (10 * 4 bytes)
printf("sizeof(ptr): %zu\n", sizeof(ptr));  // 8 (pointer size)

/* Difference 2: Address-of operator */
int arr[5];
printf("arr: %p\n", (void*)arr);      // Address of first element
printf("&arr: %p\n", (void*)&arr);    // Address of entire array
// Same address, but different types!

/* Difference 3: Cannot assign to array */
int arr1[5];
int arr2[5];
// arr1 = arr2;  // ERROR: Can't assign to array

int *ptr1 = arr1;
int *ptr2 = arr2;
ptr1 = ptr2;  // OK: Can assign pointers

/* Difference 4: Array is not a variable */
int arr[5];
// arr++;  // ERROR: Can't modify array "pointer"

int *ptr = arr;
ptr++;  // OK: Can modify pointer

/* Difference 5: Declaration creates storage */
int arr[10];  // Allocates 40 bytes
int *ptr;     // Allocates only pointer (8 bytes)
              // Doesn't allocate array!

/* Difference 6: Array name is constant pointer */
int arr[5] = {1, 2, 3, 4, 5};
// Think of arr as: int *const arr = &storage[0];
// It's a constant pointer, can't be changed

int *ptr = arr;
// ptr is a regular pointer, can be changed
ptr = NULL;    // OK
ptr = arr + 2; // OK

/* When array doesn't decay to pointer */

// 1. sizeof operator
int arr[10];
sizeof(arr);  // Size of array, not pointer

// 2. Address-of operator
&arr;  // Pointer to array, not pointer to first element

// 3. String initialization
char str[] = "Hello";  // Array, not pointer!
// vs
char *str2 = "Hello";  // Pointer to string literal

Multidimensional Arrays and Pointers

Multidimensional arrays add complexity to the array-pointer relationship. A 2D array decays to a pointer to its first row, not a pointer to int. Understanding this is crucial for passing 2D arrays to functions.

C
/* 2D array */
int matrix[3][4] = {
    {1,  2,  3,  4},
    {5,  6,  7,  8},
    {9, 10, 11, 12}
};

/* matrix decays to pointer to array of 4 ints */
int (*ptr)[4] = matrix;  // Pointer to array of 4 ints

/* Access elements */
printf("%d\n", matrix[1][2]);  // 7
printf("%d\n", ptr[1][2]);     // 7 (same)
printf("%d\n", *(*(matrix + 1) + 2));  // 7 (pointer notation)

/* How 2D indexing works */
// matrix[i][j]
// Step 1: matrix[i] gets pointer to row i
// Step 2: [j] indexes into that row
// Equivalent to: *((*(matrix + i)) + j)

/* Passing 2D array to function */
void process_2d(int arr[][4], int rows) {
    // Must specify column size!
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

/* Alternative: Pass as pointer to array */
void process_2d_ptr(int (*arr)[4], int rows) {
    // Same as above
}

/* Or with VLAs (C99+) */
void process_2d_vla(int rows, int cols, int arr[rows][cols]) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

/* Array of pointers (different from 2D array!) */
int row1[] = {1, 2, 3};
int row2[] = {4, 5, 6};
int row3[] = {7, 8, 9};

int *rows[] = {row1, row2, row3};  // Array of pointers

/* Access */
printf("%d\n", rows[1][2]);  // 6
// rows[1] gets pointer to row2
// [2] indexes into row2

Common Pitfalls

The array-pointer relationship causes several common errors. Understanding these helps you avoid crashes and undefined behavior.

C
/* Pitfall 1: sizeof on function parameter */
void wrong_size(int arr[]) {
    int size = sizeof(arr) / sizeof(arr[0]);  // WRONG!
    // sizeof(arr) is sizeof(int*), not array size
}

/* Pitfall 2: Returning pointer to local array */
int* bad_function(void) {
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    return arr;  // WRONG: arr destroyed after return
}

/* Pitfall 3: Confusing array of pointers and pointer to array */
int *arr1[10];    // Array of 10 pointers to int
int (*arr2)[10];  // Pointer to array of 10 ints

/* Pitfall 4: Pointer arithmetic overflow */
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr + 10;  // Out of bounds!
// Undefined behavior

/* Pitfall 5: Modifying string literals */
char *str = "Hello";
// str[0] = 'h';  // WRONG: String literals are const

/* Correct: Use array */
char str[] = "Hello";
str[0] = 'h';  // OK

/* Pitfall 6: Forgetting to pass size */
void process(int arr[]) {
    /* How many elements? Can't tell! */
}

/* Correct: Always pass size */
void process_correct(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        process_element(arr[i]);
    }
}

Summary & What's Next

Key Takeaways:

  • ✅ Array name decays to pointer to first element
  • ✅ arr[i] is syntactic sugar for *(arr + i)
  • ✅ Pointer arithmetic scales by element size
  • ✅ Arrays passed to functions become pointers
  • ✅ sizeof works differently on arrays vs pointers
  • ✅ Cannot assign to arrays, but can assign pointers
  • ✅ Always pass array size to functions
  • ✅ 2D arrays decay to pointer to row

What's Next?

Now let's dive deep into pointers - the most powerful feature of C!