Placement Prep

Pointers and Arrays in C: How They Differ and Where They Don't

Arrays and pointers in C are not the same type, but they decay into each other in most expressions. Here is what that means in placement code.

By FACE Prep Team 7 min read
c-programming pointers arrays pointer-arithmetic placement-prep technical-interview data-structures

Arrays and pointers in C look identical in most code, but they are different types, and the differences appear precisely where placement interviewers like to ask questions.

The standard rule is that an array name decays to a pointer to its first element in almost every expression. That single rule is responsible for most C interview confusion, and for the apparent inconsistency between code that compiles fine and code that segfaults. The five sections below cover the rule, its exceptions, and the patterns that follow from it.

For a wider catalogue of pointer questions and other C concepts that appear in placement screens, see 31 most-asked C programming interview questions.

The Array-to-Pointer Decay Rule

Declare int arr[5] and you have allocated 20 bytes of contiguous storage. The identifier arr is an array of 5 ints, which is a distinct type from int *. In almost every expression, however, the compiler silently converts arr into &arr[0], a pointer to its first element. This conversion is called array-to-pointer decay, and it is specified in the cppreference array declaration page.

There are exactly three places where decay does NOT happen:

  1. As the operand of sizeof. sizeof(arr) returns 20, the array size in bytes, not the pointer size.
  2. As the operand of the address-of operator &. &arr has type int (*)[5], a pointer to the entire array, not int **.
  3. As the initialiser of a character array from a string literal. char s[] = "hello" copies 6 bytes into s; the string literal does not decay first.

Everywhere else, an array name behaves like a pointer to its first element. The decay rule is also why C deliberately keeps the language small: rather than introduce separate array-passing syntax (as some sibling languages do, an instinct shared with function overloading in C and other features the standard intentionally omits), the standard reuses the existing pointer machinery.

#include <stdio.h>

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

    printf("sizeof(arr) = %zu\n", sizeof(arr));  /* 20 — array */
    printf("sizeof(&arr[0]) = %zu\n", sizeof(&arr[0])); /* 8 — pointer */
    printf("arr == &arr[0] ? %d\n", arr == &arr[0]);    /* 1 — equal */
    return 0;
}

The expressions arr, &arr[0], and the pointer that decay produces all evaluate to the same memory address. They are not, however, the same type, and sizeof is the operator that exposes the difference.

Pointer Arithmetic and Element Indexing

The C standard, section 6.5.2.1 in N1570, defines the subscript operator E1[E2] as exactly equivalent to *((E1) + (E2)). So arr[i] is shorthand for *(arr + i). Two consequences follow.

First, addition is commutative, so i[arr] is also legal C. It compiles, it runs, and it produces the same value as arr[i]. No production code uses this, but it is a favourite trick question in second-round interviews.

Second, pointer arithmetic scales by element size, not by bytes. The expression ptr + 1 advances the address by sizeof(*ptr) bytes. For int *ptr on a 64-bit system, that is 4 bytes; for double *ptr, that is 8 bytes. The scaling is automatic, which is why *(ptr + 2) correctly reads the third element regardless of the element type.

#include <stdio.h>

int main(void) {
    int arr[] = {5, 10, 15, 20, 25};
    int *ptr = arr;          /* decay: ptr = &arr[0] */

    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d, *(ptr+%d) = %d\n",
               i, arr[i], i, *(ptr + i));
    }
    return 0;
}

The two columns of output are identical for every index. This is the core of the array-pointer relationship: same address, same scaling, same result.

The four pointer-arithmetic operations defined for arrays:

OperationEffect
ptr++Advance to the next element
ptr--Step back to the previous element
ptr + nAddress of the element n positions ahead
ptr - nAddress of the element n positions back
p2 - p1Number of elements between two pointers into the same array

Subtracting one pointer from another (when both point into the same array) returns the element distance, not the byte distance. The result type is ptrdiff_t, defined in <stddef.h>.

Passing Arrays to Functions

Inside a function parameter list, int arr[] and int *arr are the same declaration. The compiler treats both as a pointer parameter; the array brackets are a documentation convention. This is a direct consequence of the decay rule applied at the call site.

#include <stdio.h>

void scale_array(int *arr, int n, int factor) {
    for (int i = 0; i < n; i++) {
        arr[i] *= factor;
    }
}

int main(void) {
    int data[] = {1, 2, 3, 4, 5};
    int n = sizeof(data) / sizeof(data[0]);  /* = 5, computed in main */

    scale_array(data, n, 2);

    for (int i = 0; i < n; i++) {
        printf("%d ", data[i]);              /* prints 2 4 6 8 10 */
    }
    return 0;
}

Three details worth noting:

  • The mutation inside scale_array is visible in main because the function received a pointer into the same memory. C has no built-in pass-by-value for arrays.
  • sizeof(data) / sizeof(data[0]) works inside main because data is still an array there. Move that expression inside scale_array and sizeof(arr) returns 8 (pointer size on a 64-bit system), which makes the count wrong. Always compute the length where the array is still in scope.
  • The standard pattern is to pass the length as a separate parameter, as n here. Every Linux kernel function that takes an array does this; every interview answer should too.

The sizeof trap is one of the most common conceptual errors flagged in placement screens. For a longer treatment of the parsing rules involved, see must-solve conceptual C questions.

Dynamic Arrays with malloc and free

When the array size is not known at compile time, allocate on the heap with malloc from <stdlib.h>. The function returns a void * to a block of the requested byte count, or NULL if the allocation fails. The standard reference is the cppreference malloc page.

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

int main(void) {
    int n = 5;
    int *arr = malloc(n * sizeof(*arr));   /* 5 ints on the heap */
    if (arr == NULL) {
        return 1;                          /* allocation failed */
    }

    for (int i = 0; i < n; i++) {
        arr[i] = (i + 1) * (i + 1);        /* 1, 4, 9, 16, 25 */
    }

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

    free(arr);                             /* return memory to the heap */
    arr = NULL;                            /* avoid dangling reference */
    return 0;
}

Four discipline rules from this snippet:

  • Use sizeof(*arr) rather than sizeof(int) — when the type of arr changes, the allocation stays correct.
  • Always check the NULL return. Out-of-memory is rare on a laptop and common in embedded targets and long-running servers.
  • Every malloc needs a matching free on the same pointer. Calling free twice on the same pointer is undefined behavior.
  • Set the pointer to NULL after free so a later accidental dereference produces a predictable segfault rather than reading freed memory.

malloc does not initialise the memory; the bytes hold whatever the heap last left there. Use calloc(n, sizeof(*arr)) when the array must start zeroed.

Pointer-to-Array vs. Array-of-Pointers

The two declarations below look almost identical and behave completely differently:

int (*p)[5];   /* p is a pointer to an array of 5 ints */
int *p[5];    /* p is an array of 5 pointers to int   */

The parentheses around *p bind the dereference to p before the subscript binds, making p a pointer first and the array part its target. Without the parentheses, [5] binds tighter than *, making p an array of 5 elements where each element is a pointer.

Concrete sizes on a 64-bit system:

DeclarationWhat it isSize
int arr[5]An array of 5 ints20 bytes
int (*p)[5]A pointer to one array of 5 ints8 bytes
int *p[5]An array of 5 pointers to int40 bytes
int **pA pointer to a pointer to int8 bytes

Pointer-to-array shows up most often when iterating over a 2-D array. int grid[3][4] decays to int (*)[4], not int **, because the inner dimension is part of the type. Functions that accept a 2-D array therefore declare the parameter as int grid[][4] or int (*grid)[4]. Array-of-pointers, by contrast, is the standard form for argv in int main(int argc, char *argv[]) and for jagged arrays where each row has a different length.

The parsing rule (right-left, parentheses first) is the same one that decides whether int (*fp)(int) is a function pointer or a function returning a pointer. For a deeper run through these declarations, see deep-thinking conceptual C programming questions. The cppreference pointer page has the formal grammar.

Why This Still Matters

C-style pointer arithmetic is not a museum piece. Inference engines like llama.cpp, ggml, and the CUDA-side of PyTorch all sit on top of explicit pointer arithmetic over flat tensor buffers. KV-cache slots, quantised weight blocks, and attention-mask offsets are all base_pointer + element_index * stride calculations under the hood. A student who can read *(arr + i) and reason about sizeof traps already speaks the dialect that LLM systems code is written in.

That dialect is what TinkerLLM lets you exercise without writing a kernel. The ₹299 launch tier ships a hands-on playground for building an inference-time pipeline (tokeniser, embedding lookup, top-k sampler), where the array-versus-pointer distinction from the section above stops being academic and starts being the reason your output is right or off-by-one. If you found the sizeof(arr) trap or the parentheses rule clarifying, the same pattern (small mechanical rules, big behavioural consequences) repeats across every layer of an LLM stack.

Primary sources

Frequently asked questions

Are arrays and pointers the same in C?

No. They are different types with different memory layouts. An array of 5 ints occupies 20 bytes of contiguous storage; a pointer to int occupies 8 bytes on a 64-bit system. They are interchangeable in most expressions because the C standard converts an array name into a pointer to its first element automatically.

What does sizeof(arr) return inside a function versus inside main?

Inside main, `sizeof(arr)` for `int arr[5]` returns 20 (5 elements times 4 bytes per int). Inside a function that received the array as `int arr[]` or `int *arr`, `sizeof(arr)` returns 8 on a 64-bit system because the parameter is a pointer, not an array. Always pass the length as a separate argument.

Why does arr[i] work the same as *(arr + i)?

The C standard defines the subscript operator `E1[E2]` as identical to `*((E1) + (E2))`. The expression `arr[i]` is just shorthand for `*(arr + i)`. Because addition is commutative, `i[arr]` produces the same value, which is a legal but rarely used C idiom.

What is the difference between int (*p)[5] and int *p[5]?

`int (*p)[5]` is a single pointer to an array of 5 ints; the parentheses bind the dereference to `p` first. `int *p[5]` is an array of 5 pointers to int. The first declaration uses 8 bytes on a 64-bit system; the second uses 40 bytes.

When should I use malloc instead of a fixed-size array?

Use `malloc` when the array size is not known until runtime, when the array is too large to fit on the stack (typically more than a few megabytes), or when the array must outlive the function that created it. Always pair every `malloc` with a matching `free` on the same pointer.

Can a pointer be incremented past the end of an array safely?

A pointer may legally point to one position past the last element of an array (the standard one-past-the-end position). Dereferencing that pointer is undefined behavior. Incrementing further is also undefined behavior, even without dereferencing. Loops should compare against the one-past-the-end pointer, not dereference it.

Build AI projects

A self-paced playground for building with LLMs.

TinkerLLM is FACE Prep's sister property. A guided environment for shipping real LLM applications, the kind of project that earns a paragraph on your resume, not a line.

Try TinkerLLM (₹299 launch)
Free AI Roadmap PDF