Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
C Array Memory Layout
See exactly how an array sits in RAM, how far apart its elements are, and what the base address really means. A hands-on lecture from our free C tutorial for embedded systems students.
Topics in This Lecture
Welcome to the next lesson of our free C programming course. To understand pointers and arrays together, you first need a precise mental picture of array memory layout in C. Once you can see where each element lives and how far the next one is, pointer arithmetic stops feeling like magic and starts feeling like simple multiplication. This matters a great deal in C programming for embedded systems, where you routinely hand array addresses to DMA engines, walk through peripheral register blocks, and index lookup tables stored in flash.
This lecture is part of our free embedded C course. Every example here was compiled and run on a modern 64-bit Linux machine with GCC, so the output you see is real, not copied from an old textbook.
What You Will Learn
- How the elements of an array are stored one after another with no gaps
- What the base address of an array is and which expression gives it
- The formula that turns an element index into a memory address
- How element size changes with the data type
- How to print real addresses safely with
%p - How old textbook numbers (2-byte int, addresses like 5000) differ from modern compilers
- Why this knowledge matters for embedded C programming
Prerequisites
Before starting, you should be comfortable with declaring and initialising one-dimensional arrays, the address-of operator &, pointer declaration and the dereference operator *, and the idea that every variable has an address. If any of these feel shaky, revisit the earlier lectures of this free C tutorial first.
Array Memory Layout in C: The Big Picture
When you write uint16_t adc[5];, the compiler reserves one single block of memory big enough for five 16-bit values. That block is contiguous: element 0 is followed immediately by element 1, then element 2, and so on. There is no padding between elements of the same array, no hidden header, and no length field stored next to the data. An array in C is nothing more than that raw block.
This is very different from arrays in languages such as Python or Java, where an array object carries extra information. In C, the only thing that tells you how many elements exist is the number you wrote in the declaration, and the compiler remembers it only while it is compiling. At run time, the machine sees bytes and nothing else. That is both the power and the danger of C: it is fast and predictable, but nothing stops you from reading past the end.
Array adc[5] of 16-bit values in RAM
Read the diagram from left to right as increasing addresses. Each box is one element. Every box is the same width because every element has the same type, and that fixed width is the secret behind everything you will learn about arrays and pointers in this chapter.
Base Address of an Array
The address of the very first element is called the base address of the array. Every other element is located relative to it. If you know the base address and the size of one element, you can compute the address of any element without storing anything extra.
In C there are two common ways to write the base address:
&adc[0]means “the address of element zero”, stated explicitly.adcby itself, in most expressions, is automatically converted to the address of element zero. This conversion is often called array decay.
Both give the same numeric address and the same pointer type (uint16_t *). We will prove this with code shortly. The detail about when decay does not happen, for example with sizeof, is covered in the last lecture of this group.
Two spellings of the base address
The Address Formula for Array Elements
Because all elements are the same size and sit side by side, the address of element i follows a simple rule:
address of a[i] = base address + i * sizeof(a[0])
Here is the same idea for our ADC array, assuming the base address is some value called B. We use B instead of a fixed number because the real address changes from run to run.
| Index | Address | Bytes occupied | Value |
|---|---|---|---|
| 0 | B + 0 | first 2 bytes | 512 |
| 1 | B + 2 | next 2 bytes | 730 |
| 2 | B + 4 | next 2 bytes | 1024 |
| 3 | B + 6 | next 2 bytes | 2048 |
| 4 | B + 8 | last 2 bytes | 4095 |
Notice the step is 2 because uint16_t is two bytes wide. If the array held 32-bit values the step would be 4, and for double it would be 8. The compiler does this multiplication for you, which is why you never write base + i * 2 by hand in normal C code.
Example 1: Seeing the Array Memory Layout
Instead of printing raw addresses, which change every run, this program prints each element’s offset from the base. That makes the output identical on every run and every machine that uses a 2-byte uint16_t.
#include <stdio.h>
#include <stdint.h>
int main(void)
{
uint16_t adc[5] = {512, 730, 1024, 2048, 4095};
const char *base = (const char *)adc;
printf("element size : %zu bytes\n", sizeof adc[0]);
printf("array size : %zu bytes\n", sizeof adc);
for (size_t i = 0; i < 5; i++) {
const char *here = (const char *)&adc[i];
printf("adc[%zu] = %4u sits at base + %td\n", i, adc[i], here - base);
}
printf("adc and &adc[0] are the same address: %s\n",
((const void *)adc == (const void *)&adc[0]) ? "yes" : "no");
return 0;
}
Build and run
gcc -Wall -Wextra -std=c11 -o l16_layout l16_layout.c
./l16_layout
Expected output
element size : 2 bytes
array size : 10 bytes
adc[0] = 512 sits at base + 0
adc[1] = 730 sits at base + 2
adc[2] = 1024 sits at base + 4
adc[3] = 2048 sits at base + 6
adc[4] = 4095 sits at base + 8
adc and &adc[0] are the same address: yes
How the program works
- We cast the address to
const char *because acharis exactly one byte. Subtracting twocharpointers therefore gives a difference measured in bytes, which is what we want to display. sizeof adcis the size of the whole array (5 elements times 2 bytes), whilesizeof adc[0]is the size of one element.%tdis the correct format for a pointer difference and%zufor asize_tvalue. Using the right specifiers avoids undefined behaviour and compiler warnings.- The last line compares the two spellings of the base address after casting both to
const void *, and the answer is yes.
Example 2: Element Size Depends on the Type
The distance to the next element is not a fixed number; it equals the size of the element type. This short program measures it for four types.
#include <stdio.h>
#define GAP(a) ((const char *)&(a)[1] - (const char *)&(a)[0])
int main(void)
{
char c[2];
short s[2];
int n[2];
double d[2];
printf("char array: next element is %td byte(s) away\n", GAP(c));
printf("short array: next element is %td byte(s) away\n", GAP(s));
printf("int array: next element is %td byte(s) away\n", GAP(n));
printf("double array: next element is %td byte(s) away\n", GAP(d));
return 0;
}
gcc -Wall -Wextra -std=c11 -o l16_gap l16_gap.c
./l16_gap
Output on a typical 64-bit Linux machine:
char array: next element is 1 byte(s) away
short array: next element is 2 byte(s) away
int array: next element is 4 byte(s) away
double array: next element is 8 byte(s) away
The macro looks at the gap between element 1 and element 0. The arrays are left uninitialised because we never read their values, only their addresses. On a small microcontroller you may see different sizes for int, which is one more reason embedded teams prefer fixed-width types such as uint16_t and uint32_t.
Example 3: Printing Real Addresses with %p
Sometimes you do want to see the actual addresses, for instance while debugging. The correct format specifier for a pointer is %p, and the argument must be cast to void *.
#include <stdio.h>
int main(void)
{
int rpm[4] = {800, 1200, 2400, 3600};
for (int i = 0; i < 4; i++)
printf("rpm[%d] = %4d at %p\n", i, rpm[i], (void *)&rpm[i]);
return 0;
}
gcc -Wall -Wextra -std=c11 -o l16_addr l16_addr.c
./l16_addr
Sample output (your addresses will differ, because modern systems randomise stack addresses on every run):
rpm[0] = 800 at 0x7ffd1a3ee200
rpm[1] = 1200 at 0x7ffd1a3ee204
rpm[2] = 2400 at 0x7ffd1a3ee208
rpm[3] = 3600 at 0x7ffd1a3ee20c
Look at the last hexadecimal digits: 00, 04, 08, 0c. Each step is 4 bytes because an int is 4 bytes here. The exact start value is irrelevant; the pattern of equal steps is what proves the memory is contiguous.
Do not print addresses with %d or %u. On a 64-bit machine an address does not fit in an int, so the output is wrong and the behaviour is undefined. Older books often use %u because their compilers had 16-bit pointers. Always use %p with a (void *) cast today.
Old Textbooks vs Modern Compilers
Many classic C books show addresses such as 2000, 2002, 2004 and assume an int takes two bytes. Those numbers were fine for 16-bit compilers, but they mislead students on current hardware. Use this table to translate old examples into today’s reality.
| Topic | Old 16-bit style | Modern practice |
|---|---|---|
| Size of int | 2 bytes | Usually 4 bytes on PC and ARM; check with sizeof |
| Printing an address | %u or %d | %p with (void *) cast |
| Typical address values | Small numbers like 2000 | Large hex values like 0x7ffd… |
| Main function | main() without a type | int main(void) |
| Fixed width | Rarely used | uint8_t, uint16_t, uint32_t from stdint.h |
The relationships you learn here, however, never change. Whatever the size of the type, elements are contiguous and the step equals sizeof of the element.
Why Array Memory Layout Matters in Embedded C
Understanding this layout is not an academic exercise. In embedded C programming you meet it every day:
- DMA buffers: a DMA controller is given the base address and a length in bytes. If you compute the length as
count * sizeof(element)you are using exactly the formula from this lecture. - Peripheral register blocks: a group of consecutive 32-bit registers can be treated as an array of
uint32_t, with the register number acting as the index. - Lookup tables: sine tables, CRC tables, and character maps are arrays placed in flash. Access is a single address calculation, which is why they are so fast.
- Ring buffers: UART and sensor drivers store samples in arrays and move head and tail indices, relying on the fixed element stride.
- Arrays of structures: the stride equals
sizeof(struct), which may include padding added by the compiler for alignment. The elements are still contiguous, but the gap can be larger than the sum of the fields.
Common Mistakes and Troubleshooting
| Mistake | What happens | Fix |
|---|---|---|
| Assuming addresses 2000, 2002 in your own output | Confusion when real output shows large hex values | Compare differences between addresses, not absolute numbers |
Printing addresses with %d | Truncated or garbage values, compiler warning | Use %p and cast to void * |
| Subtracting typed pointers and expecting bytes | You get an element count instead | Cast to char * first when you need bytes |
Reading adc[5] in a 5-element array | Undefined behaviour, reads whatever sits after the array | Valid indices are 0 to 4; loop with i < 5 |
Assuming int is always 4 bytes | Code breaks when ported to a smaller microcontroller | Use stdint.h types for data with a fixed size |
Best Practices, Performance and Security
Best practices
- Use
sizeof(array) / sizeof(array[0])to compute element counts instead of hard-coding numbers. - Prefer fixed-width integer types when the exact size matters, such as register images and communication buffers.
- Print addresses only for debugging and always with
%p. - Compile with
-Wall -Wextraso the compiler warns you about wrong format specifiers.
Performance: contiguous storage is friendly to the CPU cache. Walking an array from the first element to the last touches neighbouring memory in order, which is the fastest access pattern on most processors. Jumping around randomly in a large array is slower.
Security: because C stores no length with the array, writing beyond the last element corrupts adjacent memory. This is the root of many buffer overflow vulnerabilities. Always keep loop bounds tied to the declared size.
Summary and Key Takeaways
- An array occupies one contiguous block with no gaps between elements.
- The address of the first element is the base address, written
&a[0]or justain most expressions. - Address of
a[i]equals base plusitimes the element size. - Element size depends on the type; measure it with
sizeofrather than assuming. - Use
%pwith a(void *)cast to print addresses. - Offsets and differences are reliable; absolute addresses vary between runs.
Conclusion
You now have a solid picture of how a C array is laid out in memory and how an index becomes an address. This is the foundation for the next lecture, where we use the array name together with the + operator to reach any element through pointer arithmetic. Keep the formula base + i * sizeof(element) in your head; it explains almost everything about arrays and pointers. If you are following our free embedded C course in order, try the three programs above on your own machine and change the types to see how the gaps change.
Frequently Asked Questions
Are array elements always stored next to each other in C?
Yes. The C standard guarantees that the elements of an array are allocated contiguously, in increasing order of index, with no padding between elements of the array.
What is the base address of an array?
It is the address of the first element, a[0]. You can write it as &a[0] or simply use the array name in an expression where it converts to a pointer.
Why do my printed addresses look different from the textbook?
Textbooks often use made-up small numbers for clarity. Real addresses are large and change between runs because of address space randomisation. Only the differences between addresses are meaningful.
How big is an int in embedded C programming?
It depends on the compiler and target. It is commonly 4 bytes on ARM Cortex-M and PC, but 2 bytes on some 8-bit and 16-bit microcontrollers. Use uint16_t or uint32_t when the width matters.
Does the array store its own length?
No. C arrays do not carry a length. You must track the number of elements yourself, usually with sizeof or a separate variable.
Is this free C tutorial suitable for embedded systems beginners?
Yes. The examples use fixed-width types and realistic embedded scenarios such as ADC samples, GPIO registers and DMA buffers, while keeping the explanations beginner friendly.
Continue Your Free Embedded C Course
Next, learn how adding an integer to an array name moves you to the right element. Keep learning with our free C programming course for embedded systems.
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments