Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
C Memory and Addresses
Before you touch a single pointer, see exactly how bytes, addresses and variables fit together. Part of our free C programming course for embedded systems learners.
Welcome to Chapter 8 of our free C programming course. Pointers are the feature that makes C the language of firmware, operating systems and device drivers, and every pointer idea rests on one picture: memory is a long row of numbered bytes. In this lecture of the free C tutorial you will build that picture from scratch, so that when pointers arrive in the next lectures they feel obvious instead of mysterious.
If you are following this free embedded C course to write embedded C programming code for microcontrollers, this lecture matters even more. On a microcontroller you do not hide behind an operating system: you read and write real memory locations, and you need to know what each one is.
Table of Contents
What You Will Learn
By the end of this lecture you can:
- Explain why computer memory is called byte-addressable.
- Calculate the first and last valid address for any memory size.
- Describe what the compiler does when you declare a variable.
- Explain why a multi-byte variable has exactly one address.
- Print the address of a variable and read the result correctly.
- Connect all of this to c programming for embedded systems.
Prerequisites
You should already know how to declare variables, use the basic data types (char, int, float, double), and compile a small C file with gcc. You also need to be comfortable with binary and hexadecimal numbers from the number systems chapter, because addresses are almost always written in hex. If you are new here, this is a free C programming course for beginners, so go back to the earlier chapters first and return when you are ready.
How Memory Is Organised in C
Forget gigabytes and caches for a moment. From the point of view of a C program, memory is one enormous row of tiny boxes. Each box holds exactly one byte (8 bits). Every box has a number written on it, starting from zero and counting up by one. That number is the address of the byte.
The rule is simple: if a memory has N bytes, the valid addresses run from 0 to N - 1. The first address is zero, so the last address is always one less than the size. Here is how that looks for sizes you meet in real projects.
| Memory size | Total bytes | First address | Last address (decimal) | Last address (hex) |
|---|---|---|---|---|
| 1 KB (tiny MCU RAM) | 1024 | 0 | 1023 | 0x3FF |
| 64 KB (small MCU) | 65536 | 0 | 65535 | 0xFFFF |
| 1 MB (flash of a mid MCU) | 1048576 | 0 | 1048575 | 0xFFFFF |
| 4 GB (full 32-bit space) | 4294967296 | 0 | 4294967295 | 0xFFFFFFFF |
Notice the pattern in the hex column: a memory whose size is a power of two always ends in an all-F address. That is why embedded datasheets list memory maps in hex. A 32-bit address bus can name 4,294,967,296 different bytes, and that single fact is why a 32-bit microcontroller can reach at most 4 GB.
Why byte-addressable matters
The hardware does not give every int its own address. It gives every byte an address. A variable that needs four bytes simply uses four neighbouring addresses. This choice is what lets C treat a block of memory as an array of bytes, as an array of integers, or as a structure, depending only on how you choose to read it. We will exploit that idea heavily when we reach pointer arithmetic.
What Happens When You Declare a Variable
When you write a declaration such as uint16_t adc_raw;, three things happen at compile time:
- The compiler looks at the type and works out how many bytes the variable needs.
- It reserves that many consecutive bytes somewhere in memory (the exact place is decided by the compiler, linker and loader, not by you).
- It remembers a link between the name
adc_rawand the address of the first reserved byte.
That third point is the heart of this lecture. The address of a variable is the address of its first byte. A 4-byte variable occupies four addresses, but we say it lives at one address: the lowest one.
How many bytes does each type use?
Older textbooks, including the reference book this course was inspired by, assume that int takes 2 bytes. That was true on very old compilers and it is still true on some 8-bit and 16-bit microcontrollers, but it is not true on your laptop or on a 32-bit ARM Cortex-M. The size of int depends on the platform. This is exactly why embedded engineers prefer the fixed-width types from <stdint.h>.
| Type | 8-bit AVR (avr-gcc) | 32-bit Cortex-M (arm-none-eabi-gcc) | 64-bit Linux PC (gcc) |
|---|---|---|---|
| char | 1 | 1 | 1 |
| short | 2 | 2 | 2 |
| int | 2 | 4 | 4 |
| long | 4 | 4 | 8 |
| float | 4 | 4 | 4 |
| uint8_t / uint16_t / uint32_t | 1 / 2 / 4 | 1 / 2 / 4 | 1 / 2 / 4 |
sizeof, or use uint8_t, uint16_t and uint32_t when the exact size matters, for example when mapping a hardware register or a communication frame.
Memory Addresses Demo Program
Enough theory. Let us look at real addresses. The program below declares a few variables of different sizes plus a small byte array, and prints the size and the address of each one. The & symbol (the address operator) is the subject of the next lecture; for now just read it as “the address of”.
#include <stdio.h>
#include <stdint.h>
int main(void)
{
uint8_t sensor_id = 7;
uint16_t adc_raw = 2048;
uint32_t uptime_ms = 120000;
uint8_t frame[4] = {0xA5, 0x01, 0x02, 0x5A};
printf("sensor_id : %zu byte at %p\n", sizeof sensor_id, (void *)&sensor_id);
printf("adc_raw : %zu bytes at %p\n", sizeof adc_raw, (void *)&adc_raw);
printf("uptime_ms : %zu bytes at %p\n", sizeof uptime_ms, (void *)&uptime_ms);
for (int i = 0; i < 4; i++)
printf("frame[%d] = 0x%02X at %p\n", i, frame[i], (void *)&frame[i]);
return 0;
}
Build and run
gcc -Wall -Wextra -o mem_demo mem_demo.c
./mem_demo
Sample output
sensor_id : 1 byte at 0x7ffffac1f0e9
adc_raw : 2 bytes at 0x7ffffac1f0ea
uptime_ms : 4 bytes at 0x7ffffac1f0ec
frame[0] = 0xA5 at 0x7ffffac1f0f4
frame[1] = 0x01 at 0x7ffffac1f0f5
frame[2] = 0x02 at 0x7ffffac1f0f6
frame[3] = 0x5A at 0x7ffffac1f0f7
Reading the output
Your addresses will be different from the ones above, and they will probably change every time you run the program. That is normal and we explain why in the next section. What does not change is the pattern:
- Each address is just a number written in hex. It is not magic.
- The four elements of
framesit at addresses that differ by exactly 1, because each element is one byte. They are consecutive. - The 2-byte
adc_rawand the 4-byteuptime_mseach show one address, the address of their first byte. The gap betweenadc_raw(ending inea) anduptime_ms(ending inec) is exactly two bytes, which is the size ofadc_raw. - The compiler chose where to put every variable. You cannot assume the order or the gaps between separate variables.
| Variable | Size | Last hex digits of address | Bytes used |
|---|---|---|---|
| sensor_id | 1 | …e9 | e9 |
| adc_raw | 2 | …ea | ea, eb |
| uptime_ms | 4 | …ec | ec, ed, ee, ef |
| frame[0..3] | 4 | …f4 | f4, f5, f6, f7 |
Virtual vs Physical Addresses
The huge values such as 0x7ffffac1f0e9 look nothing like the small addresses in our tables. On Linux, every process lives inside its own virtual address space. The kernel and the MMU translate those virtual addresses to physical RAM behind the scenes, and the kernel also randomises the layout on each run (this is called ASLR, address space layout randomisation, and it is a security feature). So the numbers you print on a PC are virtual and change between runs.
| Environment | Address you see | Changes between runs? |
|---|---|---|
| Linux user program on a PC | Virtual address | Usually yes (ASLR) |
| Bare-metal microcontroller | Physical address | No, fixed by the linker script |
| Linux kernel code | Kernel virtual address | Depends on the region |
On a bare-metal microcontroller there is no MMU in the way on most small chips, so the address printed is the real physical address, and global variables land at the same place on every run because the linker script fixed them there. Both worlds follow the same C rules, which is why learning pointers on a PC transfers directly to firmware.
Memory Addresses in Embedded C Programming
Why does a free embedded C course spend a whole lecture on something as basic as addresses? Because almost everything interesting in embedded C programming is an address in disguise:
- Peripheral registers. A GPIO port, a UART data register or a timer counter is a memory location at a fixed address from the chip’s memory map. Writing to that address changes a pin or sends a byte.
- Buffers. A UART receive buffer, an ADC sample array or a DMA target is a block of consecutive bytes. DMA controllers are configured with raw addresses.
- Stack and heap sizing. You must know how many bytes you have. A tiny chip with 2 KB of RAM leaves no room for careless arrays.
- Linker maps. The map file lists the address and size of every symbol. Reading it is a daily skill once you work on firmware.
- Debugging. A hard fault report gives you an address. You need to know what an address is to use it.
Real-world picture
On a typical 32-bit ARM Cortex-M microcontroller, flash, RAM and peripherals all share one 32-bit address space. Flash usually sits low, RAM somewhere in the middle, and peripheral registers in a higher block. The same C pointer syntax that you learn in this chapter reaches all of them.
Common Mistakes
| Mistake | Why it is wrong | Fix |
|---|---|---|
Assuming int is always 2 bytes | Size depends on the platform | Use sizeof or stdint.h types |
| Thinking a 4-byte variable has 4 addresses | The address is only that of the first byte | Remember: one variable, one address |
| Hard-coding printed addresses into a program | Virtual addresses change per run | Never copy an address from output into code |
| Assuming two variables sit side by side | The compiler may reorder or pad | Use an array or struct if layout matters |
Printing an address with %d | Wrong format, may truncate on 64-bit | Use %p with a (void *) cast |
Best Practices
- Use
%pwith a(void *)cast to print addresses. It is the only format that is correct on every platform, and it avoids warnings. - Prefer fixed-width integer types when size matters.
- Print
sizeofresults with%zu. - Compile with
-Wall -Wextraso format mistakes are reported. - Treat addresses as opaque numbers. Do not do arithmetic on them until you have learned pointer arithmetic properly.
Performance and security note
Aligned data (a 4-byte value at an address divisible by 4) is read faster on most CPUs and is required by some, such as certain Cortex-M cores for specific instructions. The compiler normally handles alignment for you, which is one reason why variables are not always packed back to back. Address randomisation on Linux is also a defence that makes memory-corruption attacks harder, so a changing address is a feature, not a bug.
Key Takeaways
- Memory is a row of bytes, and each byte has a unique numeric address starting at 0.
- For a memory of N bytes the last address is N – 1.
- Declaring a variable reserves consecutive bytes and links the name to the first byte’s address.
- The size of a type depends on the platform, so use
sizeofand fixed-width types. - Addresses are plain whole numbers, shown in hex, and on Linux they are virtual and change between runs.
Conclusion
You now have the mental model that every later pointer lecture depends on. A pointer will turn out to be nothing more than a variable that stores one of these addresses. In the next lecture of this free C tutorial we meet the address operator, the tool that lets you ask C for the address of any variable. Keep the demo program from this page and modify it: add a double, add an array of five uint16_t values, and check that the addresses behave as described.
Frequently Asked Questions
What is a memory address in C?
A memory address is the unique number assigned to one byte of memory. A variable’s address is the address of its first byte.
Why does the address change every time I run my program?
On modern Linux, the kernel randomises the virtual address layout of each process for security. On a bare-metal microcontroller the addresses are fixed by the linker script, so they do not change.
How many bytes does an int take in C?
It depends on the platform. It is typically 4 bytes on 32-bit and 64-bit systems and 2 bytes on many 8-bit microcontrollers. Use sizeof(int) to check, or use int32_t when you need exactly 4 bytes.
Can two variables share the same address?
Not at the same time in ordinary code. Each live variable gets its own storage. Unions are the deliberate exception, where members share the same bytes.
Why are addresses printed in hexadecimal?
Hex maps neatly onto binary (one hex digit is 4 bits), so addresses and memory maps are shorter and easier to read than in decimal.
Is this important for embedded C programming?
Yes. Peripheral registers, DMA buffers, linker maps and fault reports are all based on addresses. This lecture is the foundation for all of them.
Is this course really free?
Yes. EmbeddedPathashala offers this free C programming course, free embedded systems lessons and free Linux courses to every student.
Continue the Free C Programming Course
Next up: the address operator. Learn how to ask C for the address of any variable, and which things you are not allowed to take the address of.
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments