What are C Memory and Addresses – Free C Programming Course

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 · Chapter 8 · Pointers · Lecture 1

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.

1
Byte = 1 address
N-1
Last address of N bytes
5
Lectures in this chapter

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.

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.

Memory as a row of numbered bytes
Each box is one byte. The label above it is its address.
address 0 address 1 address 2 address 3 address 4 address 5 … address N-1 (last 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 sizeTotal bytesFirst addressLast address (decimal)Last address (hex)
1 KB (tiny MCU RAM)1024010230x3FF
64 KB (small MCU)655360655350xFFFF
1 MB (flash of a mid MCU)1048576010485750xFFFFF
4 GB (full 32-bit space)4294967296042949672950xFFFFFFFF

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:

  1. The compiler looks at the type and works out how many bytes the variable needs.
  2. It reserves that many consecutive bytes somewhere in memory (the exact place is decided by the compiler, linker and loader, not by you).
  3. It remembers a link between the name adc_raw and 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.

One variable, several bytes, one address
A 4-byte variable named uptime_ms. Its address is the address of its first byte.
byte at base byte at base + 1 byte at base + 2 byte at base + 3
Address of uptime_ms = base

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>.

Type8-bit AVR (avr-gcc)32-bit Cortex-M (arm-none-eabi-gcc)64-bit Linux PC (gcc)
char111
short222
int244
long448
float444
uint8_t / uint16_t / uint32_t1 / 2 / 41 / 2 / 41 / 2 / 4
Do not memorise “int is 2 bytes”. Always ask the compiler with 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 frame sit at addresses that differ by exactly 1, because each element is one byte. They are consecutive.
  • The 2-byte adc_raw and the 4-byte uptime_ms each show one address, the address of their first byte. The gap between adc_raw (ending in ea) and uptime_ms (ending in ec) is exactly two bytes, which is the size of adc_raw.
  • The compiler chose where to put every variable. You cannot assume the order or the gaps between separate variables.
Layout of the demo (offsets from sensor_id)
VariableSizeLast hex digits of addressBytes used
sensor_id1…e9e9
adc_raw2…eaea, eb
uptime_ms4…ecec, ed, ee, ef
frame[0..3]4…f4f4, 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.

EnvironmentAddress you seeChanges between runs?
Linux user program on a PCVirtual addressUsually yes (ASLR)
Bare-metal microcontrollerPhysical addressNo, fixed by the linker script
Linux kernel codeKernel virtual addressDepends 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

MistakeWhy it is wrongFix
Assuming int is always 2 bytesSize depends on the platformUse sizeof or stdint.h types
Thinking a 4-byte variable has 4 addressesThe address is only that of the first byteRemember: one variable, one address
Hard-coding printed addresses into a programVirtual addresses change per runNever copy an address from output into code
Assuming two variables sit side by sideThe compiler may reorder or padUse an array or struct if layout matters
Printing an address with %dWrong format, may truncate on 64-bitUse %p with a (void *) cast

Best Practices

  • Use %p with 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 sizeof results with %zu.
  • Compile with -Wall -Wextra so 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 sizeof and 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

Leave a Reply

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