Difficulty: Intermediate | Prerequisites: C variables, pointers, address-of and dereference operators
Arrays are how C handles ordered collections of same-type data. Understanding arrays means understanding how they sit in memory, which leads directly to pointer arithmetic, one of the most powerful (and most error-prone) features of the language. This topic bridges the gap between basic pointer usage and dynamic memory allocation: once you see that an array name is just a pointer to its first element, dynamically allocated arrays become a natural next step. You need to be comfortable with pointers before starting here.
An array is an ordered, fixed-size collection of same-type variables stored in contiguous memory. The array name acts as a pointer to element zero. Pointer arithmetic lets you navigate arrays without index notation, and the compiler automatically scales pointer increments by the size of the data type.
Array
An ordered collection of variables, all of the same type, stored in contiguous memory locations. Think of it as a row of numbered lockers, each holding the same kind of item.
Allocated size
The total number of elements declared for the array (the number in the brackets at declaration).
Effective size
The number of elements you are actually using. You must track this yourself with a separate integer variable. In simple terms, you might have 100 lockers but only use 37 of them.
Pointer arithmetic
Adding or subtracting an integer to/from a pointer. The compiler scales the operation by the size of the pointed-to type. In simple terms, p + 1 does not add 1 byte; it moves forward by one whole element, regardless of how many bytes that element occupies.
Multidimensional array (2D array)
An array of arrays. Declared with two sets of brackets, e.g. int grid[4][3]. Stored in row-major order in memory. Think of it as a spreadsheet: rows and columns, but under the hood it is still one continuous strip of memory.
Row-major order
The memory layout where all elements of a single row are stored consecutively before the next row begins. This is how C stores 2D arrays.
Declaration:
int scores[10];
Allocates space for 10 int values in contiguous memory.
The size must be a constant expression (known at compile time in standard C).
Indexing:
scores[4] = 120;
Indices start at 0, so valid indices for scores are 0 through 9.
Initialisation at declaration:
int scores[] = {87, 99, 75, 88, 93, 56, 77, 84, 89, 79};
The compiler counts the elements and sets the size for you.
Allocated vs. effective size:
The allocated size is fixed at declaration.
If you only fill some of the slots, you need a separate variable (e.g. int numScores) to track how many are in use.
Arrays are always passed by reference:
The array name is equivalent to a pointer to its first element, so when you pass an array to a function, you pass an address, not a copy. Changes inside the function affect the original array.
Declaration:
int grid[4][3];
Conceptually: 4 rows, 3 columns.
In memory: still a single, flat block of 12 consecutive int values.
Indexing:
grid[0][1] = 5;
Memory layout (row-major):
All elements of row 0 are stored first, then all of row 1, and so on.
Elements in the same row are in consecutive memory locations.
Elements in the same column are not consecutive; they are separated by the width of one row.
Navigating an array with a pointer:
double values[10];
double *p;
p = &values[9];
while (p >= &values[0]) {
scanf("%lf", p);
p = p - 1;
}
This reads 10 doubles in reverse order, from index 9 down to index 0.
Automatic scaling:
Even though each double is 8 bytes apart in memory, p - 1 moves the pointer back by exactly one double (8 bytes), not 1 byte.
The compiler handles the scaling based on the pointer's type. This is why pointer types matter.
Array name as pointer equivalence:
values // identical to &values[0]
The array name, used on its own, decays to a pointer to element 0. This is how arrays are passed to functions.
Key difference between a pointer and an array:
double values[10]; allocates storage for 10 doubles.
double *p; allocates storage for one pointer (an address). No doubles are allocated.
However, you can point p at dynamically allocated memory to get the effect of a variable-sized array. That is covered under dynamic memory allocation.
Expression | Meaning |
|---|---|
| Element at index |
| Address of element at index |
| Same as |
| Same as |
| Same as |
| Number of elements in the array (only works in the scope where the array was declared) |
For a 2D array grid[R][C], the element grid[i][j] is at memory offset (i C + j) sizeof(element) from the start.
Arrays and pointer arithmetic are the backbone of string handling in C (a string is a char array terminated by '\0'). Image processing libraries step through pixel buffers using pointer arithmetic for performance. Embedded systems use arrays mapped to hardware registers, where understanding the exact memory layout is essential.
Students often think p + 1 adds 1 byte. It adds 1 element, which could be 1, 4, or 8 bytes depending on the pointer type.
Students sometimes expect the sizeof trick (sizeof(arr) / sizeof(arr[0])) to work inside a function that received the array as a parameter. It does not, because the array has decayed to a pointer, and sizeof a pointer gives you the pointer's size (typically 4 or 8 bytes), not the array's.
Students confuse allocated size with effective size. An array declared with 100 slots does not mean you are using 100 slots.
For 2D arrays, students assume elements in the same column are contiguous. They are not; only elements in the same row are.
⚠️ Know that the array name decays to a pointer to element 0. Be able to rewrite index notation as pointer arithmetic and vice versa.
⚠️ Understand pointer arithmetic scaling: incrementing a double * by 1 moves 8 bytes forward.
⚠️ Be able to trace through code that uses a pointer to iterate an array.
⚠️ Know the difference between double values[10]; (allocates 10 doubles) and double *p; (allocates one pointer, no doubles).
⚠️ For 2D arrays, know row-major layout and be able to calculate the flat-memory index for grid[i][j].
True or false: scores and &scores[0] are equivalent expressions.
Fill in the blank: Pointer arithmetic on an int * increments the address by ______ bytes per step (assuming 4-byte ints).
True or false: double *p; allocates memory for 10 doubles.
Fill in the blank: In a 2D array stored in row-major order, all elements of one ______ are contiguous in memory.
True or false: Passing an array to a function passes a copy of the entire array.
Q: What value does *(scores + 3) access, given int scores[] = {10, 20, 30, 40, 50};?
A: 40. scores + 3 is the address of the element at index 3, and dereferencing it gives 40.
Q: Why does sizeof(arr) inside a function that received arr as a parameter not give you the full array size?
A: Because the array decays to a pointer when passed to a function. sizeof then returns the size of the pointer (typically 4 or 8 bytes), not the size of the original array.
Q: In int grid[3][4], what is the flat-memory offset (in elements) of grid[2][1]?
A: 2 * 4 + 1 = 9.
Q: Declare a pointer to double and use it to read 5 doubles into an array called data using pointer arithmetic.
A: double *p = data; for (int i = 0; i < 5; i++) { scanf("%lf", p + i); }
Understanding that the array name is a pointer leads naturally to dynamic memory allocation with malloc, where you obtain a pointer to a freshly allocated block and treat it exactly like an array. Pointer arithmetic is also the basis for string manipulation functions (strlen, strcpy, etc.) and for iterating through data structures built with structs and pointers.
array, C array, array declaration, array initialisation, allocated size, effective size, multidimensional array, 2D array, row-major order, pointer arithmetic, pointer and array equivalence, array name as pointer, sizeof array, CS1, Computer Science I, UCF