C Pointers and Arrays relationship with array indexing and pointer notation equivalence
|

C Pointers and Arrays: The Deep Connection Explained (2026)

C pointers and arrays are so closely related that many beginners think they are the same thing. They are not, but they are deeply connected. An array name can be used as a pointer. Pointer arithmetic can replace array indexing. And when you pass an array to a function, it silently converts to a pointer. Understanding this connection is essential for writing efficient C code.

In this lesson from our C Roadmap, you will learn exactly how arrays and pointers relate, where they differ, and how to use both effectively. This builds on C Arrays and C Pointers.

The Array-Pointer Relationship in C

In most expressions, an array name automatically converts to a pointer to its first element. This is called array decay:

#include <stdio.h>

int main(void) {
    int arr[] = {10, 20, 30, 40, 50};
    int *ptr = arr;  // arr decays to &arr[0]

    // These are equivalent:
    printf("%d\n", arr[0]);       // 10
    printf("%d\n", *arr);         // 10
    printf("%d\n", ptr[0]);       // 10
    printf("%d\n", *ptr);         // 10

    // Pointer arithmetic works on both:
    printf("%d\n", arr[2]);       // 30
    printf("%d\n", *(arr + 2));   // 30
    printf("%d\n", ptr[2]);       // 30
    printf("%d\n", *(ptr + 2));   // 30

    return 0;
}

The compiler translates arr[i] to *(arr + i) internally. Since addition is commutative, *(arr + i) equals *(i + arr), which means i[arr] is also valid (though never write this in real code).

Array Decay in C: When Arrays Become Pointers

Array decay happens automatically in most contexts, but there are three exceptions where an array does NOT decay:

#include <stdio.h>

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

    // Exception 1: sizeof
    printf("sizeof(arr) = %zu\n", sizeof(arr));     // 20 (5 * 4 bytes)
    int *ptr = arr;
    printf("sizeof(ptr) = %zu\n", sizeof(ptr));     // 8 (pointer size)

    // Exception 2: Address-of operator (&)
    printf("arr   = %p\n", (void *)arr);        // Address of first element
    printf("&arr  = %p\n", (void *)&arr);       // Address of entire array
    // Same value, but different TYPES:
    // arr decays to int*
    // &arr is int(*)[5] — pointer to array of 5 ints

    // Exception 3: String literal initializer
    char str[] = "Hello";  // Copies the string into str[]
    // No decay here — the literal initializes the array

    return 0;
}

The sizeof exception is critical. This is how the common array-length idiom works: sizeof(arr) / sizeof(arr[0]). Once the array decays to a pointer (like inside a function), this trick no longer works.

Key Differences Between C Arrays and Pointers

Despite their similarities, arrays and pointers are fundamentally different:

#include <stdio.h>

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

    // Difference 1: Assignment
    // arr = ptr;    // COMPILER ERROR: cannot reassign array
    ptr = arr;       // Fine: pointer can be reassigned

    // Difference 2: sizeof
    printf("sizeof(arr) = %zu\n", sizeof(arr));  // 20
    printf("sizeof(ptr) = %zu\n", sizeof(ptr));  // 8

    // Difference 3: Address
    printf("&arr == arr? %d\n", (void *)&arr == (void *)arr);  // 1 (same address)
    printf("&ptr == ptr? %d\n", (void *)&ptr == (void *)ptr);  // 0 (different!)

    // Difference 4: Memory
    // arr: the array IS the data (20 bytes of ints)
    // ptr: the pointer is a separate variable (8 bytes) pointing to data

    return 0;
}

Think of it this way: an array is the block of memory. A pointer refers to a block of memory. The array cannot be moved. The pointer can point anywhere.

Using Pointer Indexing on C Arrays

You can use the subscript operator [] on any pointer, not just arrays. And you can use pointer arithmetic on arrays:

#include <stdio.h>

int main(void) {
    int arr[] = {100, 200, 300, 400, 500};
    int *mid = arr + 2;  // Points to arr[2]

    // Pointer indexing (relative to mid)
    printf("mid[-2] = %d\n", mid[-2]);  // 100 (arr[0])
    printf("mid[-1] = %d\n", mid[-1]);  // 200 (arr[1])
    printf("mid[0]  = %d\n", mid[0]);   // 300 (arr[2])
    printf("mid[1]  = %d\n", mid[1]);   // 400 (arr[3])
    printf("mid[2]  = %d\n", mid[2]);   // 500 (arr[4])

    return 0;
}

Negative indices are perfectly valid as long as the resulting address is within the array bounds. This is useful when a pointer is positioned in the middle of an array.

C Arrays in Functions Are Always Pointers

This is the most important practical consequence of array decay:

#include <stdio.h>

// arr is actually a pointer, not an array
void process(int arr[], int size) {
    printf("sizeof(arr) inside function: %zu\n", sizeof(arr));  // 8, not 20!

    // You can even reassign it (impossible with a real array):
    int other[] = {99, 98, 97};
    arr = other;  // This works because arr is a pointer
    printf("arr[0] = %d\n", arr[0]);  // 99
}

int main(void) {
    int data[] = {1, 2, 3, 4, 5};
    printf("sizeof(data) in main: %zu\n", sizeof(data));  // 20
    process(data, 5);
    return 0;
}

The function signature int arr[] is identical to int *arr. The compiler treats them the same. This is why you always need to pass the size separately, as we emphasized in our C Arrays lesson.

Pointer to an Entire C Array

A pointer to an array is different from a pointer to the first element. The syntax uses parentheses:

#include <stdio.h>

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

    int *p = arr;           // Pointer to first element (int *)
    int (*pa)[5] = &arr;    // Pointer to entire array (int (*)[5])

    // Both start at the same address:
    printf("p  = %p\n", (void *)p);
    printf("pa = %p\n", (void *)pa);

    // But arithmetic differs:
    printf("p + 1  = %p\n", (void *)(p + 1));   // Moves 4 bytes
    printf("pa + 1 = %p\n", (void *)(pa + 1));  // Moves 20 bytes (entire array)

    // Accessing elements through array pointer:
    printf("(*pa)[2] = %d\n", (*pa)[2]);  // 30

    return 0;
}

This distinction becomes important with Multidimensional Arrays. When you pass a 2D array to a function, the parameter type is a pointer to a row (which is itself an array).

Array of Pointers in C

An array of pointers stores multiple addresses. The most common use is an array of strings:

#include <stdio.h>

int main(void) {
    // Array of string pointers
    const char *colors[] = {"Red", "Green", "Blue", "Yellow"};
    int n = sizeof(colors) / sizeof(colors[0]);

    for (int i = 0; i < n; i++) {
        printf("colors[%d] = %s (at %p)\n", i, colors[i], (void *)colors[i]);
    }

    // Array of int pointers
    int a = 10, b = 20, c = 30;
    int *ptrs[] = {&a, &b, &c};

    for (int i = 0; i < 3; i++) {
        printf("*ptrs[%d] = %d\n", i, *ptrs[i]);
    }

    return 0;
}

This is different from a pointer to an array. Compare:

int *arr_of_ptrs[5];    // Array of 5 pointers to int
int (*ptr_to_arr)[5];   // Pointer to an array of 5 ints

The parentheses make all the difference. Without them, the [] binds first.

2D Arrays and Pointers in C

Understanding how pointers work with 2D arrays requires knowing that a 2D array is an array of arrays:

#include <stdio.h>

void print_matrix(int rows, int cols, int mat[rows][cols]) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%3d ", mat[i][j]);
        }
        printf("\n");
    }
}

// Alternative: pointer to array parameter
void print_matrix2(int rows, int cols, int (*mat)[cols]) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%3d ", mat[i][j]);
        }
        printf("\n");
    }
}

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

    // grid decays to int (*)[4] — pointer to array of 4 ints
    print_matrix(3, 4, grid);
    printf("---\n");
    print_matrix2(3, 4, grid);

    // Manual pointer access:
    // grid[i][j] = *(*(grid + i) + j)
    printf("grid[1][2] = %d\n", *(*(grid + 1) + 2));  // 7

    return 0;
}

For a deeper dive into multidimensional arrays, see our Multidimensional Arrays lesson.

String Arrays: char * vs char[] in C

The difference between char * and char[] is one of the most misunderstood topics:

#include <stdio.h>
#include <string.h>

int main(void) {
    char arr[] = "Hello";   // Array: copies "Hello" onto the stack
    char *ptr = "Hello";    // Pointer: points to read-only string literal

    // arr is modifiable:
    arr[0] = 'J';           // Fine: arr is a mutable copy
    printf("%s\n", arr);    // "Jello"

    // ptr points to read-only memory:
    // ptr[0] = 'J';        // CRASH: string literals are read-only

    // arr cannot be reassigned:
    // arr = "World";       // COMPILER ERROR

    // ptr can be reassigned:
    ptr = "World";          // Fine: point to different literal
    printf("%s\n", ptr);   // "World"

    printf("sizeof(arr) = %zu\n", sizeof(arr));  // 6 (5 chars + null)
    printf("sizeof(ptr) = %zu\n", sizeof(ptr));  // 8 (pointer size)

    return 0;
}

Use char[] when you need to modify the string. Use const char * for string literals and read-only strings. As we covered in C Strings, understanding this difference prevents segfaults.

Dynamic Arrays with C Pointers

Since arrays have fixed size at compile time, you need pointers and malloc() for runtime-sized arrays:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n;
    printf("How many numbers? ");
    scanf("%d", &n);

    // Allocate array dynamically
    int *arr = malloc(n * sizeof(int));
    if (arr == NULL) {
        fprintf(stderr, "Allocation failed\n");
        return 1;
    }

    // Use it like a normal array
    for (int i = 0; i < n; i++) {
        arr[i] = i * i;
    }

    // Print
    for (int i = 0; i < n; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }

    // Resize with realloc
    int new_n = n * 2;
    int *temp = realloc(arr, new_n * sizeof(int));
    if (temp == NULL) {
        free(arr);
        return 1;
    }
    arr = temp;

    // Fill new elements
    for (int i = n; i < new_n; i++) {
        arr[i] = i * 10;
    }

    printf("Resized array has %d elements\n", new_n);

    free(arr);
    arr = NULL;

    return 0;
}

Dynamic arrays are the foundation of most real-world C programs. We will cover malloc, calloc, realloc, and free in depth in a future Dynamic Memory lesson.

Practice Exercises

Exercise 1: Write a function that takes a const char * string and returns a pointer to the last character (before the null terminator). Use pointer arithmetic only, no indexing.

Exercise 2: Create an array of 5 int * pointers, each pointing to a different dynamically allocated integer. Print all values through the pointer array, then free everything.

Exercise 3: Write a function that takes a 2D array as a pointer-to-array parameter and computes the sum of all elements.

Solution to Exercise 1
const char *last_char(const char *s) {
    if (*s == '\0') return NULL;  // Empty string
    while (*(s + 1) != '\0') {
        s++;
    }
    return s;
}

Summary

C pointers and arrays share a deep connection through array decay. An array name converts to a pointer to its first element in most contexts, and arr[i] is equivalent to *(arr + i). However, arrays and pointers differ in sizeof, assignability, and the address-of operator. Array of pointers (int *arr[]) is different from pointer to array (int (*p)[N]). In the next lesson, we will dive into pointers to pointers. Follow our C Roadmap to continue mastering C.

Similar Posts

Leave a Reply

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