Relationship Between Arrays and Pointers in C: Decay, Address Arithmetic and a Worked Memory Trace

See why an array is not a pointer, even when their displayed addresses match. Trace decay, pointer movement, sizeof, function parameters and bounds through one C array.

KnowledgeGate Team

Exam prep & CS education

Updated 8 Sep 20265 min read

An array expression, a pointer and &array can display the same starting address, yet they are not the same type and do not advance by the same amount. That is the source of many C output, type and sizeof traps. Let int a[5] = {12, 7, 25, 4, 18}; begin at decimal address 1000; assume each int occupies 4 bytes and each pointer occupies 8 bytes. The Coding & DSA category connects this memory model to related programming foundations.

An array object and a pointer object are different things

Start with int a[5] = {12, 7, 25, 4, 18}; int *p = a;. Here, a is one array object containing five contiguous int elements. It occupies 5 * 4 = 20 bytes under our assumptions. The separately declared object p is a pointer that stores the address of a[0]; it occupies 8 bytes in this illustration.

Expression

Type or conversion

Meaning

a

Converts to int * in most expressions

Points at a[0]

&a[0]

int *

Address of the first element

p

int *

A modifiable pointer object

&a

int (*)[5]

Pointer to the complete five-element array

All four can print 1000 at the start. Equal displayed addresses do not imply equal types, object sizes or pointer-arithmetic steps. This conversion of an array expression is called array-to-pointer conversion, commonly called decay. An array is not a pointer. For declarations, two-dimensional arrays and exam patterns, use Arrays and Pointers in C: Complete Guide with Worked Examples for GATE and Interviews. With a, one object and one base address are enough to calculate every step from decay to the one-past pointer.

Array decay happens in expressions, but not everywhere

The assignment p = a works because a decays to a pointer to its first element. Indexing uses the same rule: a[i] means *(a + i). Therefore, a[2] and *(a + 2) both read the value 25.

Two important contexts preserve the array. In the block where a is defined, sizeof a measures the complete array. The expression &a takes the address of that complete array, so its pointed-to type is int[5].

The pointer variable is modifiable, so p++ is valid when the resulting pointer stays within the array or reaches its one-past position. In contrast, a++ and a = p are invalid. The array name is not a modifiable pointer variable. Calling it a constant address hides this crucial type distinction.

Worked memory trace: values, addresses and pointer movement

The trace starts as follows:

i

0

1

2

3

4

a[i] initially

12

7

25

4

18

&a[i]

1000

1004

1008

1012

1016

p + i

1000

1004

1008

1012

1016

Each int step adds 4 bytes. Thus, a + 2 points at 1000 + 2 * 4 = 1008, and *(a + 2) reads 25. Similarly, p[3] is *(p + 3): the address is 1000 + 3 * 4 = 1012, where the value is 4.

Now execute *(p + 1) = 30. The target address is 1000 + 1 * 4 = 1004, so the shared array becomes {12, 30, 25, 4, 18}. Next, p = p + 4 makes p hold 1000 + 4 * 4 = 1016, and *p is 18. Moving p changes only the pointer variable. It does not move the array or change the base represented by a.

Memory strip of int a[5] starting at address 1000 in 4-byte cells, with a[1] changed to 30 and pointer p moving from 1000 to 1016.

Why a + 1 and &a + 1 land at different addresses

After decay, a + 1 advances by one int, so it points at 1000 + 4 = 1004. The expression &a + 1 advances by one complete int[5], so it points one past the 20-byte array at 1000 + 20 = 1020. A one-past pointer may be formed, but it must not be dereferenced.

The related size calculations in the defining block are:

  • sizeof a = 5 * 4 = 20

  • sizeof a[0] = 4

  • sizeof a / sizeof a[0] = 20 / 4 = 5

  • sizeof p = 8, only under this illustration's pointer-size assumption

Two number lines from address 1000: a+1 advances 4 bytes to 1004, while &a+1 advances 20 bytes to 1020 past the whole int[5] array.

Passing an array to a function passes a pointer view

Consider int sum(const int x[], size_t n). In a function parameter, const int x[] is adjusted to const int *x. The brackets neither copy five elements nor carry the length. Inside sum, sizeof x is 8 under our assumption, not 20, so the function needs a separate n.

Call sum(a, 5) after the mutation. The loop reads {12, 30, 25, 4, 18}, producing running totals 12, 12 + 30 = 42, 42 + 25 = 67, 67 + 4 = 71, and 71 + 18 = 89. Its valid traversal range is [x, x + n). The one-past pointer x + 5 may be formed for a loop condition, but not dereferenced.

Traps: declarations, bounds and writable storage

Looks similar

What it really means

Safe correction

int *q[3]

Array of three int * elements

Parse outward from q: brackets bind first

int (*r)[3]

One pointer to an array of three int elements

Use parentheses deliberately around *r

Bounds matter too. a + 5 forms the valid one-past pointer, while *(a + 5) has undefined behaviour. Pointer subtraction and ordering are meaningful only within the same array object, including its one-past position. Comparing unrelated objects this way is not portable.

Storage also changes what can be written. char s[] = "GATE"; creates a writable array copy, so s[0] = 'g' is valid. char *t = "GATE"; points at a string literal, and attempting t[0] = 'g' has undefined behaviour. Arrays and Strings in C: Array-to-Pointer Decay, sizeof Traps and 2D Address Arithmetic extends the same rules to string terminators, sizeof comparisons and row-major two-dimensional addresses. The five-element array a isolates the earlier step: how decay and pointed-to type determine each address increment.

How questions turn the relationship into output and type traps

Stable question patterns ask you to translate indexing into pointer arithmetic, predict sizeof in a caller and a function parameter, distinguish int *q[3] from int (*r)[3], or identify a one-past dereference and a string-literal write as undefined behaviour.

Use the locked trace for three rapid checks:

  1. *(a + 3) is 4 at address 1000 + 3 * 4 = 1012.

  2. &a + 1 represents address 1000 + 5 * 4 = 1020.

  3. After the mutation, sizeof a / sizeof a[0] remains 20 / 4 = 5. Values do not affect the array's type or extent.

Short version and the next practice step

Keep five truths together:

  • An array is storage for a fixed number of contiguous elements.

  • It decays to a pointer to its first element in most expressions.

  • sizeof a and &a preserve the complete array type.

  • Pointer arithmetic scales by the pointed-to type.

  • Bounds still matter even when an address can be computed.

Redraw the 1000-to-1020 memory strip from memory, recompute the three rapid checks, and write sum with a pointer plus an explicit length. The C Language Course: Concepts, MCQs & Coding provides structured concept and practice coverage.