Arrays sit at the center of much low level work in C, and they raise real questions about how memory, pointers, and indexing behave. The language gives you direct access to memory, so details such as layout, element size, and how expressions are evaluated have a direct effect on what the machine actually does. Today, we will look at how C stores arrays in memory, why array names act like pointers in many expressions, how indexing works in terms of pointer arithmetic, and what happens when you pass arrays into functions.
Array Layout In C Memory
In C, an array is a real object with a size, a storage location, and a precise order for its elements in memory. When you write an array definition, the compiler reserves one block of memory large enough to hold every element, with no gaps between them. That layout influences how indexing works, how sizeof behaves, and what you can do safely with pointers that refer to those elements.
Fixed Size Array Objects
When you introduce an array like this in a block scope:
The compiler allocates storage for temps in the stack frame for log_temps. All four int elements sit side by side in that frame. On a machine where sizeof(int) is 4, the array occupies 16 bytes as one contiguous region.
The first element of temps has the lowest address within that region, the second element follows right after it, and so on until the last element. Indexing temps[i] tells the compiler to start at the address of the first element, add i times the size of int, and access the result as an int. That rule does not rely on any extra metadata stored with the array and only depends on the base address and the element size that belong to the type.
Arrays can also live in static storage. When you write a global definition like this:
Here, the array object becomes part of the static data segment for the executable or library. The same contiguity rule holds, so the eight elements occupy one uninterrupted block of memory that the runtime keeps for the entire life of the process.
Arrays created through dynamic allocation still follow the same idea. Code that allocates a block through malloc and then treats it as an array of a given type gets a region of memory where elements are again laid out back to back:
This call to malloc returns a pointer to the first byte of a region that can hold n int values, and the indexing operations step through that region according to the size of int. The language treats this region as a sequence of n int elements laid out back to back, so indexing with samples[i] follows the same rules as with a named array.
C treats the array length as part of the type. A definition with four elements and a definition with eight elements do not share the same array type, even when they hold the same element type:
Both arrays contain int values, yet sizeof a and sizeof b produce different totals. A pointer to int[4] has type int (*)[4], while a pointer to int[8] has type int (*)[8]. That difference matters whenever you take the address of the entire array object or pass a pointer to an array into a function that expects a specific bound.
C99 introduced variable length arrays, where the bound can come from a runtime value:
In this case the compiler arranges storage for buffer with space for count elements at run time instead of at compile time. The object is still one array with contiguous elements, and the bound stays fixed for the lifetime of the block that holds buffer. Some compilers treat variable length arrays as an extension setting that can be toggled, so portable code still tends to rely on either fixed length arrays or dynamic allocation through functions like malloc and free.
Multidimensional Arrays In Row Order
C expresses multidimensional arrays as arrays whose elements are themselves arrays. Take this definition:
This creates an object that holds two elements, and each of those elements is an array of three int. In memory, the six integers form one continuous row of bytes. The elements with indices grid[0][0], grid[0][1], and grid[0][2] occupy the first three int slots. Right after those come grid[1][0], grid[1][1], and grid[1][2]. The rightmost subscript changes fastest as you move forward through memory.
The mapping from a pair of indices to an address follows a fixed rule. For a two by three grid like the definition above, the expression grid[i][j] refers to the element at offset (i * 3 + j) from the start, measured in units of int. That is what row order means here, the elements of the first row sit first, then the elements of the second row, and so on for larger grids.
The static type of each expression in that grid reflects the nesting. The object grid has type “array of 2 elements of type array of 3 int”. An expression such as grid[0] has type “array of 3 int”, and an expression such as grid[0][0] has type int. These distinctions affect pointer conversions and function parameters that work with a row, with the whole grid, or with a single element.
Larger grids follow the same rule, even when more than two dimensions take part. Code like this:
The code here allocates one block of memory that holds four layers of three by two int values. The rightmost subscript still varies fastest, so cube[0][0][0], cube[0][0][1], cube[0][1][0], and cube[0][1][1] sit side by side before moving on to later combinations of indices. The compiler reduces an access such as cube[i][j][k] to an offset based on the total number of elements in all the less significant dimensions.
When you embed multidimensional arrays in a struct, the same row order applies inside that aggregate. For example:
That creates a struct with one member that covers nine unsigned char elements in a row. Accesses such as icon.pixels[1][2] index into the same continuous byte range that starts at the address of icon.pixels[0][0]. The fact that the array lives inside a struct does not alter its contiguity or ordering.
Pointers With Array Indexing
C treats arrays and pointers in a way that ties them closely in day to day code, but the language still makes a distinction between an array object and a pointer value. That distinction sits underneath array to pointer conversions, the way indexing expressions are evaluated, and what happens when arrays cross a function boundary.
Why Array Names Behave Like Pointers
In most expression contexts, a value of array type turns into a pointer to its first element. Many texts call this array to pointer decay. The rule applies whenever the array expression needs to produce a value rather than stand for the whole object as such.
Code like this shows the basic conversion:
The array values has its own type, which is “array of 4 int”, but in the assignment to p, the expression values converts to int *. That pointer carries the address of values[0], so dereferencing p reads the first element. The array itself is not stored as a pointer, and there is no automatic way to turn p back into a values array object with length information.
This conversion step applies all through expression evaluation. When you pass an array to a function that takes int *, when you compare an array expression to another pointer, or when you add an integer offset, the compiler treats the array expression as a pointer to element zero. The original array type still matters for sizeof and for pointer to array types, but the general value form is a pointer.
There are specific contexts where this conversion does not occur. An array expression remains an array when used as the operand of sizeof, as the operand of the unary & operator, and when a string literal initializes a character array in a declaration. Those are the main situations where you still interact with the full array object rather than the pointer value.
The difference can be seen in code like this:
The variable total_size holds the number of bytes occupied by every element in values combined, while elem_size holds the size of a single int. The expression &values yields a pointer to the array object with type int (*)[4], which is distinct from int *. Pointer to array types allow code to pass or store references to entire fixed size arrays, not just to single elements.
String literals show another form of this behavior. Like when you write:
The compiler creates an array large enough to hold the characters and the terminating zero byte, then copies those bytes into the array during initialization. The type of title is an array of char, not a pointer, and sizeof title gives the total byte count for that array. In expressions, title still converts to char *, so the decay rule applies there too.
Overall, the language treats array names as pointer like in most expressions while still keeping array objects as distinct entities that carry a length as part of their type.
How Indexing Works With Pointer Arithmetic
C defines the indexing operator in terms of pointer arithmetic and dereference. An expression of the form:
This has the same meaning as:
Whenever a is a pointer or an array expression and i is an integer expression. The compiler evaluates a as a pointer value, adds i times the size of the pointed to type, then dereferences the result. That rule matches the contiguous layout of array elements in memory.
Code that sets up a small array makes the connection make more sense visually:
The pointer p0 holds the address of the first element, p1 points to the second element, and p2 points to the third. The index expression data[1] reads the same location as *(data + 1), so there is no hidden table or runtime lookup. The type of data as an array of int tells the compiler that each element occupies sizeof(int) bytes, and that fact drives the pointer arithmetic.
Because a[i] is defined as *(a + i), the expression i[a] has the same meaning, although that style rarely appears in regular code:
The language rule allows either form, but most developers stick to array[index] for readability.
Pointer arithmetic is only valid inside one array object and the position just past its last element. Code that walks across an array with a pointer follows that rule carefully:
The pointer p starts at the first element and moves forward one element at a time. The variable end holds an address one step beyond readings[4]. The comparison p != end is valid because both pointers refer to positions within the same array plus the one past boundary. Dereferencing end would be invalid, and stepping p beyond end would produce a pointer value outside the allowed range.
Negative indexes expose the same arithmetic in another way. An expression like a[-1] is defined as *(a + (-1)), which means a pointer that moves before the element that a points to. If a refers to array[0], that places the pointer outside the array, so accessing it is undefined behavior even if a particular test run appears to return some value.
Arrays Passed To Functions
C adjusts array parameters in function declarations so that they behave as pointers. When a function signature writes an array type in a parameter slot, the compiler treats that parameter as a pointer to the element type. The caller passes an address, and the function works with that address as it would with an ordinary pointer variable.
Two declarations like these describe the same function type:
In both cases the function receives a pointer to int and an element count. The caller passes an array expression such as values or a pointer they already hold. The function can index into arr or use pointer arithmetic, but it does not receive the full array object as a value parameter.
An example that uses this function makes that behavior visible:
The definition of call_sum declares a four element array and passes values to sum_array. The expression values converts to int * at the call site, and sum_array receives that pointer in its parameter arr. Any changes to arr[i] modify the original values elements, because both refer to the same memory.
Inside sum_array, the expression sizeof arr gives the size of a pointer, not the size of the caller’s array. The language does not carry array length into the parameter type in this situation, which is why functions that process arrays also take a separate length argument or rely on a terminator value in the data.
When code needs access to an entire fixed size array as a single object, a pointer to array type comes into play. That gives the function enough information about the bound to compute offsets for multidimensional data or to treat the array as one unit.
The parameter row has type int (*)[5], which is a pointer to an array of five int. The call passes &line, so row refers to the whole array object line. Dereferencing row yields the array, and indexing (*row)[i] steps across its elements. This form keeps the bound as part of the parameter type, unlike the plain int * parameter.
Multidimensional arrays need that size information for all but the first dimension. A common example is a grid that has a fixed column count and a row count passed at run time:
The parameter grid in fill_grid is adjusted to int (*)[4], a pointer to an array of four int. The compiler needs that column count to translate grid[i][j] into the correct offset. The caller’s table array matches that type because it has three rows and four columns.
Structs with array members act differently when passed to functions, because the parameter receives either a full copy of the struct or a pointer to the struct, not a bare array parameter. That difference can be seen in code like this:
The call to process_buffer passes buf by value, so b is a separate copy with its own data array. Writing to b.data[0] does not change buf.data[0] in the caller. The array decay rule did not apply to the member when the struct was copied. If a function needs to modify the caller’s data array, it would take a pointer to struct buffer and the caller would pass &buf.
Conclusion
C arrays occupy contiguous regions of memory, and pointer behavior grows directly from that layout. Index expressions advance through those regions by applying offsets based on element size, array expressions usually convert to pointers to the first element, and function parameters that look like arrays actually receive addresses that refer back to the caller’s storage. Multidimensional arrays follow the same row based order, and pointer to array types or array members inside structs keep size information available where it is part of the type. With these mechanics in mind, it’s easier to relate a line of C code to the exact bytes it accesses and to see when pointer arithmetic stays within a valid array and when it moves past the bounds into undefined behavior.


![void log_temps(void) { int temps[4]; } void log_temps(void) { int temps[4]; }](https://substackcdn.com/image/fetch/$s_!z1_t!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc12baac0-cddb-4d9e-9845-8e0f727c2005_1674x166.png)
![int counters[8]; int counters[8];](https://substackcdn.com/image/fetch/$s_!2gic!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0756c405-8131-4855-a741-875ce70a23ef_1669x57.png)
![size_t n = 100; int *samples = malloc(n * sizeof *samples); if (samples != NULL) { samples[0] = 7; samples[1] = 9; samples[2] = samples[0] + samples[1]; } size_t n = 100; int *samples = malloc(n * sizeof *samples); if (samples != NULL) { samples[0] = 7; samples[1] = 9; samples[2] = samples[0] + samples[1]; }](https://substackcdn.com/image/fetch/$s_!JmcG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ee1b8d8-5200-430c-9862-03ebe7a4ca4c_1658x448.png)
![int a[4]; int b[8]; int a[4]; int b[8];](https://substackcdn.com/image/fetch/$s_!omlH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F931af207-62fb-4e76-9271-22ee25867133_1694x118.png)
![void read_values(size_t count) { int buffer[count]; /* use buffer here */ } void read_values(size_t count) { int buffer[count]; /* use buffer here */ }](https://substackcdn.com/image/fetch/$s_!Cb86!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faab69f8a-21d7-4bc6-9847-3cd667ef1f96_1673x223.png)
![int grid[2][3]; int grid[2][3];](https://substackcdn.com/image/fetch/$s_!Rco1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b55837c-ebd5-469d-8fbb-e656647fd076_1693x56.png)
![int cube[4][3][2]; int cube[4][3][2];](https://substackcdn.com/image/fetch/$s_!6OXw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12576988-b21d-49a6-b47b-5a1c2d23a7cd_1662x56.png)
![struct image { unsigned char pixels[3][3]; }; struct image icon; struct image { unsigned char pixels[3][3]; }; struct image icon;](https://substackcdn.com/image/fetch/$s_!dWfl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff68d9e63-f4ce-418c-92fe-a92d7ae15f12_1677x279.png)
![void sample(void) { int values[4] = {10, 20, 30, 40}; int *p = values; /* p receives &values[0] */ int first = *p; /* reads values[0] */ } void sample(void) { int values[4] = {10, 20, 30, 40}; int *p = values; /* p receives &values[0] */ int first = *p; /* reads values[0] */ }](https://substackcdn.com/image/fetch/$s_!2z7p!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89bf69b0-9d75-4675-88f3-d4077c66d97e_1681x279.png)
![void stats(void) { int values[4] = {1, 2, 3, 4}; size_t total_size = sizeof values; /* bytes in the whole array */ size_t elem_size = sizeof values[0]; int (*ptr_to_array)[4] = &values; /* pointer to the array object */ } void stats(void) { int values[4] = {1, 2, 3, 4}; size_t total_size = sizeof values; /* bytes in the whole array */ size_t elem_size = sizeof values[0]; int (*ptr_to_array)[4] = &values; /* pointer to the array object */ }](https://substackcdn.com/image/fetch/$s_!JX_o!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57a83d81-a5aa-4bb7-bb55-be705727e2b8_1598x334.png)
![char title[] = "C arrays"; char title[] = "C arrays";](https://substackcdn.com/image/fetch/$s_!Y2Fc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7c3e254-5fc7-4380-bbea-1e6f724b90bb_1682x56.png)
![a[i] a[i]](https://substackcdn.com/image/fetch/$s_!Y_E-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5760a99f-54cb-4db3-9903-482f6c4906da_1612x72.png)

![void offsets(void) { int data[3] = {5, 6, 7}; int *p0 = data; /* &data[0] */ int *p1 = data + 1; /* &data[1] */ int *p2 = data + 2; /* &data[2] */ int x = data[1]; /* same as *(data + 1) */ } void offsets(void) { int data[3] = {5, 6, 7}; int *p0 = data; /* &data[0] */ int *p1 = data + 1; /* &data[1] */ int *p2 = data + 2; /* &data[2] */ int x = data[1]; /* same as *(data + 1) */ }](https://substackcdn.com/image/fetch/$s_!PLfb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe7cb2e01-a999-4557-93b0-a02aa36e5a8e_1653x502.png)
![void mirrored_index(void) { int nums[3] = {11, 22, 33}; int v1 = nums[2]; int v2 = 2[nums]; /* same element as nums[2] */ } void mirrored_index(void) { int nums[3] = {11, 22, 33}; int v1 = nums[2]; int v2 = 2[nums]; /* same element as nums[2] */ }](https://substackcdn.com/image/fetch/$s_!crfe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe02671d5-f784-45f5-823e-0cdd1852fe80_1687x335.png)
![void sum_readings(void) { int readings[5] = {2, 4, 6, 8, 10}; int *p = readings; int *end = readings + 5; /* one past the last */ int total = 0; while (p != end) { total += *p; p = p + 1; } } void sum_readings(void) { int readings[5] = {2, 4, 6, 8, 10}; int *p = readings; int *end = readings + 5; /* one past the last */ int total = 0; while (p != end) { total += *p; p = p + 1; } }](https://substackcdn.com/image/fetch/$s_!dLss!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6571bae-38a4-4c7b-8a82-cfdcc7ac7f81_1644x614.png)
![void sum_array(int arr[], size_t n); void sum_array(int *arr, size_t n); void sum_array(int arr[], size_t n); void sum_array(int *arr, size_t n);](https://substackcdn.com/image/fetch/$s_!WSKn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe248545b-d494-4b7c-9222-f45fb44139b6_1697x111.png)
![void sum_array(int arr[], size_t n) { int total = 0; for (size_t i = 0; i < n; i++) { total += arr[i]; } printf("total = %d\n", total); } void call_sum(void) { int values[4] = {3, 5, 7, 9}; sum_array(values, 4); } void sum_array(int arr[], size_t n) { int total = 0; for (size_t i = 0; i < n; i++) { total += arr[i]; } printf("total = %d\n", total); } void call_sum(void) { int values[4] = {3, 5, 7, 9}; sum_array(values, 4); }](https://substackcdn.com/image/fetch/$s_!W-PV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f286bb1-58e7-4bcd-b029-0a7b093a0c01_1642x782.png)
![void fill_row(int (*row)[5]) { for (size_t i = 0; i < 5; i++) { (*row)[i] = (int)(i * 2); } } void use_fill_row(void) { int line[5]; fill_row(&line); } void fill_row(int (*row)[5]) { for (size_t i = 0; i < 5; i++) { (*row)[i] = (int)(i * 2); } } void use_fill_row(void) { int line[5]; fill_row(&line); }](https://substackcdn.com/image/fetch/$s_!6zk4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F27a854ac-316a-4958-b05a-feef7e729a71_1661x558.png)
![void fill_grid(int rows, int grid[][4]) { for (int i = 0; i < rows; i++) { for (int j = 0; j < 4; j++) { grid[i][j] = i * 10 + j; } } } void call_fill_grid(void) { int table[3][4]; fill_grid(3, table); } void fill_grid(int rows, int grid[][4]) { for (int i = 0; i < rows; i++) { for (int j = 0; j < 4; j++) { grid[i][j] = i * 10 + j; } } } void call_fill_grid(void) { int table[3][4]; fill_grid(3, table); }](https://substackcdn.com/image/fetch/$s_!caSw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0a835aed-18ad-4b0c-8a22-03554680a5a4_1660x672.png)
![struct buffer { char data[256]; }; void process_buffer(struct buffer b) { b.data[0] = 'A'; } void call_process(void) { struct buffer buf = {{0}}; process_buffer(buf); } struct buffer { char data[256]; }; void process_buffer(struct buffer b) { b.data[0] = 'A'; } void call_process(void) { struct buffer buf = {{0}}; process_buffer(buf); }](https://substackcdn.com/image/fetch/$s_!sdaq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe709eb4b-7914-412f-9b22-b1aca3c5eb8f_1644x673.png)