Introduction

A C array is a fixed number of objects of one type stored contiguously in memory. Arrays and pointers are closely tied: in most expressions an array name turns into a pointer to its first element, and the subscript a[i] is defined through pointer arithmetic. They are not the same thing, though, and this note sets out exactly the places where they differ: sizeof, &a versus a, and multidimensional arrays.

This note covers the C language side. Address formulas for arbitrary index ranges, column-major order and special matrices are in Arrays: Address Calculation and Special Matrices.

Conventions: C11, LP64 (4-byte int, 8-byte pointers). Byte offsets in examples are for gcc on x86-64 Linux; addresses such as 2000 are illustrative.

Prerequisites

  • Pointers: &, *, pointer arithmetic and its scaling.

Core concepts

Declaring and initialising arrays

  • int a[5]; declares 5 ints, a[0] to a[4]. The size must be a positive integer constant expression, except for variable length arrays (VLAs, int v[n]; at block scope), which C11 makes an optional feature.
  • int a[5] = {1, 2, 3}; initialises the first three; the remaining elements are set to 0. int z[100] = {0}; zeroes the whole array.
  • int b[] = {5, 6, 7, 8}; takes its size, 4, from the initialiser.
  • An automatic array with no initialiser has indeterminate elements; a static-duration array is zero-filled.
  • Arrays cannot be assigned or compared as wholes: a = b; does not compile, and a == b compares addresses.
  • Indexing outside 0..n-1 is undefined behaviour; C does no bounds checking.

The element count of an array (not a pointer) is sizeof a / sizeof a[0].

Array-to-pointer conversion (decay)

Rule (C11 6.3.2.1): an expression of type "array of TT" is converted to "pointer to TT" pointing at element 0, except when it is

  1. the operand of sizeof,
  2. the operand of unary &, or
  3. a string literal used to initialise an array, as in char s[] = "hi";.

(The C11 text also lists _Alignof, but _Alignof accepts only a type name, so that item has no practical effect.)

Consequences:

  • a[i] is defined as *(a + i): a decays to &a[0], then pointer arithmetic moves ii elements.
  • Passing a to a function passes &a[0]; the parameter is a pointer, and sizeof inside the function gives 8 (Functions and Parameter Passing).
  • sizeof a is the size of the whole array: 20 for int a[5].

a versus &a

Both have the same address value, but different types.

ExpressionTypePoints to+ 1 advances by
a (decayed)int *element 04 bytes (one int)
&a[0]int *element 04 bytes
&aint (*)[5]the whole array20 bytes (one array of 5 int)
*&aint [5](is the array)sizeof *&a is 20

If a starts at 2000, then a + 1 is 2004 and &a + 1 is 2020.

Array of pointers versus pointer to array

Read the declaration from the name, giving [] priority over * unless parentheses intervene (Function Pointers and Reading Complex Declarations):

  • int *ap[3]; is an array of 3 pointers to int: sizeof ap is 24.
  • int (*pa)[3]; is a pointer to an array of 3 int: sizeof pa is 8.

Two-dimensional arrays

int m[3][4]; is an array of 3 elements, each of which is an array of 4 int. C stores it in row-major order: row 0, then row 1, then row 2.

ExpressionTypeMeaning
m (decayed)int (*)[4]pointer to row 0
m + iint (*)[4]pointer to row ii; advances 4×4=164 \times 4 = 16 bytes per row
*(m + i), m[i]int [4], decays to int *row ii, as a pointer to its first element
*(m + i) + jint *address of m[i][j]
*(*(m + i) + j), m[i][j]intthe element

Element m[i][j] lies at byte offset (i⋅C+j)⋅w(i \cdot C + j) \cdot w from the start, where CC is the number of columns and ww the element size. With C=4C = 4, w=4w = 4 and base 2000, m[2][1] is at 2000+(2⋅4+1)⋅4=20362000 + (2 \cdot 4 + 1) \cdot 4 = 2036.

Row-major layout of int m[3][4] with pointer steps
Figure 1. The array int m[3][4] as a grid and as the same 12 ints in row-major order in memory, with m + 1 stepping one row (16 bytes), *m + 1 stepping one int (4 bytes) and m[2][1] at byte offset 36.

Passing a 2D array to a function. The parameter must give every dimension except the first: int sum(int r, int a[][4]) (equivalently int (*a)[4]). int a[][] does not compile, because the compiler needs the row length to compute a[i][j]. With a VLA parameter, int sum(int r, int c, int a[r][c]) also works.

Arrays of strings

  • const char *names[] = {"ab", "cde", "f"}; is an array of 3 pointers to string literals: sizeof names is 24, and the strings live elsewhere (read-only).
  • char grid[3][4] = {"ab", "cde", "f"}; is a 3×43 \times 4 character array holding copies: sizeof grid is 12, and each row is padded with '\0'. The rows are modifiable.

Worked examples

All outputs below are for C11 with gcc on x86-64 Linux (LP64).

Example 1 (sizes and partial initialisation).

C
int a[5] = {1, 2, 3};
printf("%zu %zu %zu\n", sizeof a, sizeof a[0], sizeof a / sizeof a[0]);   /* 20 4 5 */
printf("%d %d %d\n", a[3], a[4], *(a + 2));                                /* 0 0 3 */

Example 2 (a + 1 versus &a + 1). For the same a, (char *)(a + 1) - (char *)a is 4 and (char *)(&a + 1) - (char *)a is 20. (void *)a == (void *)&a is 1: same address, different types.

Example 3 (2D access, every notation). With m[i][j] = 10 * i + j for int m[3][4]:

  • m[2][1], *(*(m + 2) + 1) are 21; *(m[1] + 3) is 13; (*(m + 1))[2] is 12.
  • Byte offsets: m + 1 is 16, *m + 1 is 4, &m[2][1] is 36.
  • sizeof m, sizeof m[0], sizeof m[0][0] are 48, 16, 4.
  • int (*rp)[4] = m; rp++; makes (*rp)[0] equal to m[1][0], which is 10.
  • Inner-array bounds. With a declared int m[3][4], forming int *flat = &m[0][0] does not permit flat[2*4+1] in strictly conforming C11: that access crosses the first inner array's boundary. Use m[2][1], a correctly typed pointer-to-array, or an allocation that is genuinely one flat int array. Do not claim universal support for the out-of-row idiom.

Example 4 (passing a 2D array). sum(2, m) with int m[2][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}} and int sum(int r, int a[][4]) returns 36.

Important properties and observations

  • a[i], *(a + i), *(i + a) and i[a] are the same expression.
  • An array decays to a pointer except under sizeof, under &, and as a string literal initialising an array.
  • a and &a have the same address but step by an element versus by the whole array.
  • An array parameter is a pointer; only the first dimension of a multidimensional parameter may be omitted.
  • Row-major layout: offset of m[i][j] is (iC+j)w(iC + j)w.
  • Missing initialisers are zero; no initialiser at all leaves automatic arrays indeterminate.
  • Arrays are not assignable; copy with a loop or memcpy.

GATE examination perspective

  • Pointer expressions on 2D arrays: translate each * and + using the type table; m + 1 and *m + 1 differ by a factor of the row length.
  • Address questions: "an int array a[10][20] starts at 1000; find the address of a[5][7]" is 1000+(5⋅20+7)⋅4=14281000 + (5 \cdot 20 + 7) \cdot 4 = 1428 with 4-byte int. Check whether the question uses 0-based indices (C always does).
  • sizeof traps: inside a function the array parameter is a pointer.
  • Array of pointers versus pointer to array in declaration questions.

Summary

Sources

  1. ISO/IEC 9899:2011 (C11), committee draft N1570, (consulted 2026-11-09): Section 6.3.2.1 (array-to-pointer conversion, paragraph 3), 6.5.2.1 (array subscripting, including the row-major example in paragraph 4), 6.5.6 (pointer arithmetic), 6.7.6.2 (array declarators, VLAs), 6.7.9 (initialization).
  2. B. W. Kernighan and D. M. Ritchie, The C Programming Language, 2nd ed., Prentice Hall, 1988: Sections 5.3 (pointers and arrays), 5.7 (multi-dimensional arrays), 5.9 (pointers versus multi-dimensional arrays).

Next steps