What is C Array Memory Layout- C Programming Training Institute In Hyderabad

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

Free C Programming Course · Pointers · Lecture 16

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

array memory layout in C base address of array contiguous memory sizeof element embedded C programming free C tutorial

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

adc[0] = 512adc[1] = 730adc[2] = 1024adc[3] = 2048adc[4] = 4095
2 bytes eachno gapstotal 10 byteslowest address on the left

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.
  • adc by 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

adcdecays to&adc[0]=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.

IndexAddressBytes occupiedValue
0B + 0first 2 bytes512
1B + 2next 2 bytes730
2B + 4next 2 bytes1024
3B + 6next 2 bytes2048
4B + 8last 2 bytes4095

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 a char is exactly one byte. Subtracting two char pointers therefore gives a difference measured in bytes, which is what we want to display.
  • sizeof adc is the size of the whole array (5 elements times 2 bytes), while sizeof adc[0] is the size of one element.
  • %td is the correct format for a pointer difference and %zu for a size_t value. 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.

TopicOld 16-bit styleModern practice
Size of int2 bytesUsually 4 bytes on PC and ARM; check with sizeof
Printing an address%u or %d%p with (void *) cast
Typical address valuesSmall numbers like 2000Large hex values like 0x7ffd…
Main functionmain() without a typeint main(void)
Fixed widthRarely useduint8_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

MistakeWhat happensFix
Assuming addresses 2000, 2002 in your own outputConfusion when real output shows large hex valuesCompare differences between addresses, not absolute numbers
Printing addresses with %dTruncated or garbage values, compiler warningUse %p and cast to void *
Subtracting typed pointers and expecting bytesYou get an element count insteadCast to char * first when you need bytes
Reading adc[5] in a 5-element arrayUndefined behaviour, reads whatever sits after the arrayValid indices are 0 to 4; loop with i < 5
Assuming int is always 4 bytesCode breaks when ported to a smaller microcontrollerUse 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 -Wextra so 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 just a in most expressions.
  • Address of a[i] equals base plus i times the element size.
  • Element size depends on the type; measure it with sizeof rather than assuming.
  • Use %p with 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

Leave a Reply

Your email address will not be published. Required fields are marked *