Arrays vs Pointers in C – The Truth Behind the Confusion (Complete Guide 2026)

Arrays vs. Pointers in C

1. Introduction: why this topic breaks so many beginners

Here is something most C tutorials won’t admit: they teach arrays and pointers in a way that creates confusion on purpose — not intentionally, but because they oversimplify.

They say things like “an array name is just a pointer” and move on. You nod, write some code, and then six weeks later, you hit a bug you cannot explain. You try sizeof on an array passed to a function and get the wrong number. You try to increment an array name and get a compiler error. You return a local array from a function, and your program crashes.

All of these problems trace back to one root cause: you were never told the full truth about how arrays and pointers actually relate in C. This guide fixes that — completely. No shortcuts. No oversimplifications. Just the real, deep, accurate picture, with code examples and memory-level explanations that will make everything click. By the end of this guide, you will know:

  • What arrays and pointers actually are at the memory level.
  • Where they behave identically and where they are fundamentally different.
  • What “array decay” is and when it happens.
  • Why does sizeof give different results for arrays and pointers?
  • How pointer arithmetic works and why it is powerful.
  • The exact rules for passing arrays to functions.
  • Every common mistake and exactly how to avoid it.

Prerequisites

This guide assumes you already understand what arrays are and how pointers work in C. If you read a refresher.

Let’s get into it.

2. The relationship — where the confusion starts

Let’s start with why this confusion exists in the first place. In C, when you write this:

int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr;

Both arr and ptr can now be used to access elements the same way:

printf("%d\n", arr[2]);   // 30
printf("%d\n", ptr[2]);   // 30
printf("%d\n", *(arr+2)); // 30
printf("%d\n", *(ptr+2)); // 30

All four lines print the same value. This makes them look interchangeable — and that’s exactly where the illusion begins. They are not interchangeable. They just act similarly in this specific context. The moment you go deeper, the differences become sharp and consequential.

3. The decay rule — the most important concept

To understand everything in this guide, you must first understand pointer decay. When you use an array name in most expressions, C automatically converts it into a pointer to its first element. This is called decay — the array “decays” into a pointer.

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

// arr decays to &arr[0] in this context
int *ptr = arr;   // same as: int *ptr = &arr[0];

This decay is automatic and implicit. You do not write it. C does it for you. But here is the critical part: decay is not the same as being a pointer. Decay is a conversion that happens in certain contexts. The array itself is still an array. It has not become a pointer. When you write int *ptr = arr, you are creating a new pointer variable ptr that holds the address of the first element — the array itself has not changed.

Decay happens in most contexts, but not in these three

  1. When the array is the operand of sizeof.
  2. When the array is the operand of the unary “&” operator.
  3. When the array is a string literal used to initialize a char array.

These three exceptions are where the real differences between arrays and pointers become visible.

4. Where arrays and pointers are similar

Before diving into differences, let’s be honest about where they genuinely behave the same way.

Element access is identical

int arr[4] = {1, 2, 3, 4};
int *ptr = arr;

arr[2];       // 3
ptr[2];       // 3
*(arr + 2);   // 3
*(ptr + 2);   // 3

This works because arr[i] is literally defined in C as *(arr + i). The bracket notation is just shorthand for pointer arithmetic. So once decay happens, accessing elements through an array name and through a pointer is identical — the compiler generates the same machine code.

They can be used interchangeably in many function calls

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

int arr[4] = {1, 2, 3, 4};
int *ptr = arr;

print(arr, 4);   // Works
print(ptr, 4);   // Works — identical behavior

Pointer arithmetic works on both

printf("%d\n", *(arr + 1));   // 2
printf("%d\n", *(ptr + 1));   // 2

That is where the similarity ends.

5. Where arrays and pointers are completely different

Now for the substance. Here are the genuine, fundamental differences — the ones that matter.

Difference 1 — what they actually are in memory

An array is a named region of memory. When you declare int arr[5], the compiler allocates 20 bytes (5 x 4) of memory and gives that region the name arr. The name is not stored anywhere — it is a compile-time label.

A pointer is a variable that holds an address. When you declare int *arr, the compiler allocates 8 bytes (on a 64-bit system) to hold an address. ptr itself lives in memory. arr is a name that does not take up extra storage — it is the storage.

int arr[5];    // 20 bytes of storage, named arr
int *ptr;      // 8 bytes of storage, holds an address

This single difference is the root of every other difference in this guide.

Difference 2 — sizeof reveals the truth

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

printf("%zu\n", sizeof(arr));   // 20 — total size of the array
printf("%zu\n", sizeof(ptr));   // 8  — size of one pointer (64-bit)

sizeof(arr) gives you the total number of bytes in the array — 5 elements x 4 bytes each = 20. This works because the compiler knows at compile time that arr is an array of 5 integers.

sizeof(ptr) gives you the size of the pointer variable itself — 8 bytes on a 64-bit system, regardless of what it points to. The pointer has no knowledge of how many elements are at the address it points to.

This is the most practical and testable difference between arrays and pointers.

Difference 3 — you cannot reassign an array name

A pointer is a variable. You can point it wherever you want:

int a = 10, b = 20;
int *ptr = &a;
ptr = &b;     // Perfectly legal — ptr now points to b
ptr++;        // Legal — ptr moves forward

An array name is fixed. It always refers to the same block of memory:

int arr[5] = {1, 2, 3, 4, 5};
int other[5] = {6, 7, 8, 9, 10};

arr = other;   // COMPILER ERROR — cannot assign to an array
arr++;         // COMPILER ERROR — cannot increment an array name

The array name is not an lvalue you can modify. It is a permanent label for a fixed memory region.

Difference 4 — the address-of operator behaves differently

This one surprises even experienced developers.

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

Now compare:

printf("%p\n", arr);    // Address of first element — e.g., 0x1000
printf("%p\n", &arr);   // Address of the whole array — also 0x1000
printf("%p\n", &arr[0]);// Address of first element — 0x1000

arr and &arr print the same numeric address — but they are different types.

  • arr decays to int * — a pointer to one integer.
  • &arr is int (*)[5] — a pointer to the entire array of 5 integers.

This difference becomes visible when you do arithmetic:

printf("%p\n", arr + 1);    // 0x1004 — moves 4 bytes (one int forward)
printf("%p\n", &arr + 1);   // 0x1014 — moves 20 bytes (one whole array forward)

arr + 1 moves past one element. &arr + 1 moves past the entire array. Same starting address, completely different step size — because the types are different. For a pointer variable, &ptr is the address of the pointer itself — a completely different location:

printf("%p\n", ptr);    // 0x1000 — what ptr points to
printf("%p\n", &ptr);   // 0x7ff... — where ptr itself lives in memory

6. The sizeof trap in functions

This is the most common and most damaging real-world bug that comes from misunderstanding arrays vs pointers.

#include <stdio.h>

void badCount(int arr[]) {
    // arr is a POINTER here, not an array
    int count = sizeof(*arr) / sizeof(arr[0]);
    printf("Count inside function: %d\n", count);  // WRONG — prints 1, not 5
}

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

    int count = sizeof(arr) / sizeof(arr[0]);
    printf("Count in main: %d\n", count);  // Correct — prints 5

    badCount(arr);  // arr decays to a pointer here

    return 0;
}
Output
Count in main: 5
Count inside function: 1

When arr is passed to badcount, it decays into a pointer. Inside the function, sizeof(*arr) returns 4 (pointer size), and sizeof(arr[0]) return 4. So 4/4 = 1 — completely wrong.

The fix — always pass the length explicitly

void correctCount(int arr[], int length) {
    // Use the length parameter, never sizeof
    for (int i = 0; i < length; i++)
        printf("%d ", arr[i]);
}

correctCount(arr, 5);  // Pass 5 explicitly

This is not a workaround. This is the correct and intended design. C does not carry size information with arrays — you are responsible for tracking it.

7. Assignability — a deeper look

Let’s look at exactly what can and cannot be assigned:

int arr[5] = {1, 2, 3, 4, 5};
int brr[5] = {6, 7, 8, 9, 10};
int *ptr = arr;
int *qtr = brr;

// Pointer — fully reassignable
ptr = qtr;      // Legal — ptr now points to brr's first element
ptr = arr;      // Legal — back to arr
ptr++;          // Legal — points to arr[1]
ptr += 3;       // Legal — points to arr[4]

// Array — nothing is reassignable
arr = brr;      // ILLEGAL — compiler error
arr++;          // ILLEGAL — compiler error
arr = ptr;      // ILLEGAL — compiler error

However, you can copy array contents element by element, or use memcpy:

#include <string.h>

memcpy(arr, brr, sizeof(arr));  // Copies brr's contents into arr — Legal

You are copying the data, not reassigning the array name. The distinction matters.

8. Arrays and pointers in function parameters

This section reveals something surprising about C’s type system. When you write a function parameter as int arr[], C silently treats it as int *arr. They are literally identical to the compiler:

void func1(int arr[])   { }  // arr is int*
void func2(int arr[10]) { }  // arr is STILL int* — the 10 is ignored!
void func3(int *arr)    { }  // arr is int*

All three declarations are identical. The [10] in func2 is completely ignored by the compiler. This is why you cannot use sizeof to count elements inside a function — there is no array, only a pointer.

This also explains why the function can accept arrays of any length:

int small[3] = {1, 2, 3};
int large[100];

func1(small);   // Fine
func1(large);   // Also fine — just a pointer either way

9. Pointer arithmetic vs array indexing

As established, arr[i] and *(arr + i) are identical. But it is worth understanding exactly what pointer arithmetic does. When you add an integer to a pointer, C does not add raw bytes. It adds multiples of the element size:

int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr;

// ptr currently holds address 0x1000 (hypothetical)
ptr + 1;   // 0x1004  — moved 4 bytes (1 × sizeof(int))
ptr + 2;   // 0x1008  — moved 8 bytes (2 × sizeof(int))
ptr + 4;   // 0x1010  — moved 16 bytes (4 × sizeof(int))

This arithmetic automatically scales by the type size. It works the same way whether you use the array name (after decay) or a pointer variable.

Subtracting pointers

You can subtract two pointers that point into the same array to find the number of elements between them:

int arr[5] = {10, 20, 30, 40, 50};
int *start = &arr[1];
int *end   = &arr[4];

ptrdiff_t diff = end - start;   // 3 — three elements apart

The result is of type ptrdiff_t — a signed integer type designed for pointer differences.

10. String literals — the most dangerous edge case

Strings in C expose the arrays-vs-pounters distinction in a way that causes real crashes in real programs.

// Case 1 — Array of chars — modifiable
char str1[] = "Hello";
str1[0] = 'h';   // Legal — str1 is an array on the stack
printf("%s\n", str1);  // hello

// Case 2 — Pointer to string literal — READ ONLY
char *str2 = "Hello";
str2[0] = 'h';   // UNDEFINED BEHAVIOR — likely crash

In Case 1, “Hello” is copied into a stack-allocated char array. You own that memory and can modify it.

In Case 2, str2 points to a string literal stored in read-only memory (the .rodata section of your program). Writing to it is undefined behavior — on most modern systems, it causes a segmentation fault.

The fix for string literals

If you need a modifiable string, always use char array[]. If you only need to read it, use const char *.

const char *str3 = "Hello";   // Correct — const signals read-only intent

The const does not make the program correct — the string was always read-only. But it tells the compiler to warn you if you accidentally try to write to it.

11. The complete comparison table

FeatureArrayPointer
What it isNamed memory regionVariable holding an address
StorageData lives directly in arrayPointer variable + data elsewhere
sizeofTotal array size (e.g., 20 bytes)Always pointer size (4 or 8 bytes)
ReassignableNo — fixed labelYes — can point anywhere
IncrementableNo — arr++ is illegalYes — ptr++ is legal
&name typePointer to whole array int (*)[N]Pointer to the pointer variable
In function paramsDecays to pointer — size lostStays a pointer
Initializationint arr[] = {1, 2, 3}int *ptr = arr
LifetimeTied to scope (stack or static)Pointer outlives data if misused
NULL possibleNoYes — ptr = NULL
String modificationchar s[] = "hi" — modifiablechar *s = "hi" — read-only
Memory trackingCompiler knows the sizeProgrammer must track size

12. When to use an array vs a pointer

Use a static array when

  • The size is fixed and known at compile time.
  • You want the compiler to track the size for you (at least in the declaring scope).
  • You want simple, automatic memory management (stack allocated, auto-freed).
  • You are storing a small, fixed collection of elements.
int scores[10];           // Fixed 10 scores
char name[50];            // Fixed buffer
int board[8][8];          // Chess board

Use a pointer when

  • The size is determined at runtime (dynamic allocation).
  • You need to pass large data to functions without copying.
  • You need to iterate through memory efficiently.
  • You are building data structures (linked lists, trees).
  • You need to return heap-allocated data from a function.
int *data = malloc(n * sizeof(int));   // Runtime size
char *result = processString(input);   // Return from function

The practical rule

Use an array for storage. Use a pointer for access and navigation. When you need both flexibility and efficiency, allocate with malloc and navigate with a pointer.

13. Conclusion

Let’s bring it all together.

Arrays and pointers in C are related but not the same. The relationship exists because of one rule — decay — where an array name converts to a pointer to its first element in most expressions. This makes them look interchangeable in everyday code. But they have distinct identities, distinct memory behavior, and distinct rules.

The three things to always remember

(i) Decay is a conversion, not an identity

An array is not a pointer. It becomes a pointer temporarily in certain contexts.

(ii) sizeof is the litmus test

sizeof(array) gives you the full size. sizeof(pointer) gives you 8 bytes. If you are ever unsure what you are working with, sizeof will tell you the truth.

(iii) Once an array enters a function, it is a pointer

Always pass the length separately. Never use sizeof to count elements inside a function that received an array as a parameter.

Master these three rules, and the entire arrays-vs-pointers confusion disappears. The rest is just syntax. Now here is something interesting. Everything you just learned — decay, pointer arithmetic, the difference between char [] and char *, read-only memory, modifiable buffers — is not just theory. It plays out in the most concrete and practical way possible when you work with strings in C.

Strings are where arrays and pointers stop being an academic discussion and start being a source of real bugs in real programs. A char array and a char * look identical on the surface. But one lets you modify the data, the other crashes your program if you try. One carries its size, the other does not. One lives on the stack, the other points into read-only memory.

Sound familiar? That is because strings in C are built entirely on the concepts you just mastered.

In the next blog, we are diving deep into strings in C — how they are stored, why they behave so differently depending on how you declare them, how string functions like strcpy, strlen and strcmp actually work under the hood, and the mistakes that even experienced C developers make.

If arrays vs pointers felt like the theory, strings are the exam.

Leave a Comment

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

Scroll to Top