Data Types in Embedded C: char, int, long, float, double and void Explained-Free Embedded C Training in Hyderabad

Need Help or More Information?

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

MODULE 3 · LESSON 3.1 · THEORY

Data Types in Embedded C: char, int, long, float, double and void Explained

The size, range and signedness of every standard C data type, why those sizes change between ARM, x86 and 8-bit microcontrollers, and how to print and choose each type correctly in firmware.

55 min
Read Time
Beginner
Level
M3 · L1
Module / Lesson

What You Will Learn

✓ What a data type is and why embedded C needs them
✓ Size and range of char, short, int, long, long long, float, double and void
✓ Signed and unsigned variants and the default signedness rules
✓ Why type sizes depend on the architecture (the portability problem)
✓ Sizes on ARM Cortex-M4, x86-64 Linux, Windows and 8/16-bit MCUs
✓ The guarantees the C standard actually gives you (limits.h)
✓ printf format specifiers for every type
✓ How floating point is stored and what the Cortex-M4F FPU can do
✓ How to choose the right type for registers, sensors and counters
✓ Common mistakes and best practices

Key Terms

Data typecharshortintlonglong longfloatdoublevoidsigned / unsignedsizeoflimits.hprintf specifierPortabilityILP32 / LP64

1. What Are Data Types in Embedded C?

Every variable in C has a type. The type tells the compiler three things: how many bytes of memory to reserve, how to interpret the bit pattern stored there, and which machine instructions to use when you operate on it. Understanding data types in embedded C is therefore the foundation for everything that follows: registers, structures, pointers, memory maps and drivers are all built from a handful of basic types.

A type = size + interpretation + operations
Size
How many bytes does the variable occupy in SRAM or in a register?
Interpretation
Are the bits an unsigned integer, a 2’s complement integer or an IEEE-754 float?
Operations
Which instruction: integer ADD, unsigned compare, or a floating-point VADD?

On a PC, picking a slightly wrong type costs you nothing noticeable. On a microcontroller such as the STM32F407 (192 KB of SRAM, Cortex-M4 core, 32-bit registers) a wrong type can waste scarce RAM, break a hardware register access, or silently give wrong answers when a temperature goes negative. That is why this lesson treats data types in embedded C with much more care than a desktop tutorial would.

Remember: The CPU stores only bits. int, unsigned char and float exist only in the compiler’s mind; the type you choose decides what those bits mean.

2. The Basic Data Types at a Glance

The C language gives you a small set of fundamental types. Everything else (structures, enums, typedefs, pointers) is built from them. The diagram shows how many bytes each type occupies on the STM32F407 when compiled with arm-none-eabi-gcc. Each blue square is one byte.

Size in bytes on ARM Cortex-M4 (arm-none-eabi-gcc)
char
B0
1 byte
short
B0
B1
2 bytes
int
B0
B1
B2
B3
4 bytes (one 32-bit register)
long
B0
B1
B2
B3
4 bytes on ARM
long long
B0
B1
B2
B3
B4
B5
B6
B7
8 bytes (two registers)
float
B0
B1
B2
B3
4 bytes, IEEE-754 single
double
B0
B1
B2
B3
B4
B5
B6
B7
8 bytes, IEEE-754 double
void *
B0
B1
B2
B3
4 bytes: an address on a 32-bit MCU
TypeCategoryTypical use in firmwareSize on Cortex-M4
charInteger (character)Text, raw bytes, small values1
shortIntegerADC samples, 16-bit registers2
intIntegerGeneral counters, loop indexes (native word)4
longIntegerLarger counters, timestamps4
long longInteger64-bit counters, microsecond time8
floatFloating pointSensor maths, PID gains (hardware FPU)4
doubleFloating pointRarely: software-emulated on Cortex-M48
voidNo valueNo return value, generic pointer void *n/a
Notice: The word “typically” appears in every C textbook for a reason: only char is guaranteed to be 1 byte. All other sizes depend on the compiler and CPU. Section 8 explains the consequences.

3. The char Type: One Byte, Text and Raw Data

char is the smallest addressable unit in C. By definition sizeof(char) is always 1, and on every mainstream platform that one byte has 8 bits (CHAR_BIT == 8). It serves two jobs: holding a text character ('A' is just the number 65) and holding a small integer or raw byte.

One char = 8 bits = values 0..255 (unsigned) or −128..127 (signed)
‘A’ = 65 = 0x41
7
0
6
1
5
0
4
0
3
0
2
0
1
0
0
1
char_basics.c — a character is just a number
#include <stdio.h> int main(void) { char c = ‘A’; printf(“c as character : %c\n”, c); printf(“c as number : %d\n”, c); printf(“c + 1 : %c\n”, c + 1); printf(“sizeof(char) : %zu\n”, sizeof(char)); printf(“‘0’ is %d, so digit 7 -> character %c\n”, ‘0’, ‘0’ + 7); return 0; }
Output (verified by compiling and running)
c as character : A c as number : 65 c + 1 : B sizeof(char) : 1 ‘0’ is 48, so digit 7 -> character 7
Plain char has no fixed signedness: The standard says char may be signed or unsigned. GCC on x86 makes it signed; GCC for ARM makes it unsigned. Code that stores 200 in a plain char and then compares c < 0 behaves differently on the two machines. For numbers, use uint8_t or int8_t (Lesson 3.2); reserve plain char for text.

Use plain char only for characters and strings. For raw bytes, UART buffers and register-sized fields prefer unsigned char or, better, uint8_t.

4. short, int, long and long long: The Integer Family

C provides four integer sizes beyond char. The standard fixes only their minimum ranges and their order: sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long). On the STM32F407 (32-bit ARM) the actual values and ranges are:

TypeBytesSigned rangeUnsigned range
char1−128 to 1270 to 255
short2−32,768 to 32,7670 to 65,535
int4−2,147,483,648 to 2,147,483,6470 to 4,294,967,295
long4−2,147,483,648 to 2,147,483,6470 to 4,294,967,295
long long8−9,223,372,036,854,775,808 to 9,223,372,036,854,775,8070 to 18,446,744,073,709,551,615
Range grows with size (bar length is logarithmic, not to scale)
8 bit
256 values
16 bit
65536 values
32 bit
4.29 billion values
64 bit
1.8 × 1019 values

Why int matters most

int is defined as the compiler’s “natural” integer, usually the width of a CPU register. On the Cortex-M4 that is 32 bits, so int arithmetic maps directly to single-cycle ADD and SUB instructions. Smaller types such as char and short are automatically widened to int before arithmetic (integer promotion, Lesson 3.3). That is why a plain int loop counter is often both the smallest and the fastest choice on ARM, while uint8_t is the right choice for stored data such as buffers and structure fields.

long versus long long

long is where portability breaks first: it is 4 bytes on ARM and 32-bit systems but 8 bytes on 64-bit Linux and macOS. long long (C99) is at least 64 bits everywhere. On a 32-bit MCU every 64-bit operation takes several instructions (and division calls a library routine), so use long long deliberately, not by habit.

5. float and double: Floating-Point Types

Integers cannot represent fractions. For quantities such as 3.14 or 0.0005 C provides float (32 bit) and double (64 bit), both stored in IEEE-754 format: a sign bit, an exponent and a fraction (mantissa). The value is roughly ±1.fraction × 2exponent.

IEEE-754 single precision (float): 1 + 8 + 23 = 32 bits
Sign1 bit
Exponent8 bits
Fraction (mantissa)23 bits
IEEE-754 double precision (double): 1 + 11 + 52 = 64 bits
Sign1 bit
Exponent11 bits
Fraction (mantissa)52 bits
floatdouble
Size4 bytes8 bytes
Significant decimal digitsabout 6–7about 15–16
Largest value≈ 3.4 × 1038≈ 1.8 × 10308
Smallest normal value≈ 1.2 × 10−38≈ 2.2 × 10−308
Literal suffix3.14f3.14 (no suffix)
STM32F407 hardwareFPv4-SP FPU: single cycle opsNo hardware: software library, 10–50× slower
float_precision.c — floats are approximations
#include <stdio.h> int main(void) { float f = 0.1f; double d = 0.1; printf(“float 0.1 = %.10f\n”, f); printf(“double 0.1 = %.20f\n”, d); printf(“0.1 + 0.2 == 0.3 ? %d\n”, (0.1 + 0.2) == 0.3); printf(“1/3 as int division : %d\n”, 1 / 3); printf(“1/3 as float division : %f\n”, 1.0f / 3); return 0; }
Output (verified by compiling and running)
float 0.1 = 0.1000000015 double 0.1 = 0.10000000000000000555 0.1 + 0.2 == 0.3 ? 0 1/3 as int division : 0 1/3 as float division : 0.333333
Cortex-M4F trap: The STM32F407 has a single-precision FPU only. A literal such as 2.5 is a double, so x * 2.5 forces the slow software path. Write x * 2.5f, and compile with -mfpu=fpv4-sp-d16 -mfloat-abi=hard.
Never compare floats with ==: Because most decimal fractions cannot be stored exactly, compare with a tolerance: fabsf(a - b) < 1e-5f.

6. The void Type: Representing “No Type”

void is the type that has no values. You cannot declare a variable of type void, but it appears in three essential places:

UseExampleMeaning
No return valuevoid led_toggle(void);The function returns nothing
No parametersint main(void)The function takes no arguments (in C, empty () means “unspecified”)
Generic pointervoid *p = &x;A pointer to something of unknown type; needs a cast before dereferencing
Discard a result(void)HAL_Delay(10);Tells the reader and the compiler the value is deliberately ignored
void_demo.c
void delay_ms(unsigned int ms); /* no result */ int read_adc(void); /* no parameters */ void *memcpy(void *dst, const void *src, unsigned int n); /* generic pointers */ void fill(void *buf, unsigned char value, unsigned int n) { unsigned char *p = (unsigned char *)buf; /* cast before use */ while (n–) { *p++ = value; } }

Because sizeof(void) is not defined by the standard (GCC allows it as 1), pointer arithmetic on a void * is non-portable. Cast to uint8_t * first, as the example above does.

7. Signed and Unsigned Variants and Default Signedness

All integer types come in a signed and an unsigned form (Module 2 explained the 2’s complement representation behind this). The keywords are signed and unsigned:

DeclarationSame asSignedness rule
int x;signed int x;Signed by default
short x;signed short int x;Signed by default
long x;signed long int x;Signed by default
long long x;signed long long int x;Signed by default
unsigned x;unsigned int x;int is implied
char x;neither of the next twoImplementation-defined: unsigned on ARM, signed on x86
signed char x;–Always signed (−128..127)
unsigned char x;–Always unsigned (0..255)
The same byte 0xC8 (200) read three ways
bits
7
1
6
1
5
0
4
0
3
1
2
0
1
0
0
0
unsigned char
200
signed char
−56
plain char
200 (ARM) / −56 (x86)
signedness.c — same bits, different values
#include <stdio.h> int main(void) { unsigned char u = 200; signed char s = (signed char)200; /* implementation-defined conversion */ char c = (char)200; printf(“unsigned char : %d\n”, u); printf(“signed char : %d\n”, s); printf(“plain char : %d (%s on this compiler)\n”, c, (char)-1 < 0 ? “signed” : “unsigned”); printf(“-1 < 1u : %d\n”, –1 < 1u); printf(“unsigned 0-1 : %u\n”, 0u – 1u); return 0; }
Output on a 64-bit Linux PC (an ARM build prints &ldquo;unsigned&rdquo; and 200 for plain char)
unsigned char : 200 signed char : -56 plain char : -56 (signed on this compiler) -1 < 1u : 0 unsigned 0-1 : 4294967295

Which one should I use?

  • Unsigned for register contents, bit masks, byte buffers, counters that never go below zero, sizes and array indexes.
  • Signed for physical quantities that can be negative: temperature offsets, speed error, audio samples.
  • Never use plain char for numbers. Never compare signed and unsigned values without a cast.

8. Why Data Type Sizes Depend on the Architecture

The C standard was written for machines of many different word sizes, so it does not say “int is 32 bits”. It says only that each type can hold at least a minimum range. As a result, the same data types in embedded C occupy different numbers of bytes on different targets (all values in bytes):

Platformcharshortintlonglong longfloatdoublepointer
8-bit AVR (ATmega)1224844*2
16-bit MSP43012248482
ARM Cortex-M4 (STM32F407)12448484
32-bit x86 (Linux/Windows)12448484
64-bit Linux / macOS (LP64)12488488
64-bit Windows (LLP64)12448488

* avr-gcc makes double the same size as float by default.

The same declaration, different memory: long counter;
ARM Cortex-M4
B0
B1
B2
B3
long = 4 bytes: wraps after 4,294,967,295 (unsigned)
64-bit Linux PC
B0
B1
B2
B3
B4
B5
B6
B7
long = 8 bytes: wraps after 1.8 × 10^19
16-bit MSP430
B0
B1
B2
B3
long = 4 bytes, but int = only 2

The portability problem

Imagine a driver developed on a PC and then ported to the STM32F407. A variable declared int counter; holds 2 billion on ARM but only 32,767 on an 8-bit AVR. A structure that maps a hardware register with long fields would be 4 bytes per field on ARM and 8 bytes on a 64-bit PC, so the layout changes. A %d in printf given a 64-bit argument prints garbage. All of these are silent failures. This is the motivation for the fixed-width types of Lesson 3.2.

ModelMeaningWhere
ILP32int, long, pointer = 32 bitARM Cortex-M, 32-bit x86
LP64long, pointer = 64 bit; int = 32 bit64-bit Linux, macOS
LLP64only long long and pointer = 64 bit64-bit Windows
IP16int, pointer = 16 bitAVR, MSP430

9. What the C Standard Guarantees (limits.h and float.h)

Because sizes vary, the standard provides headers that report the actual limits of the compiler you use. <limits.h> covers integers and <float.h> covers floating point. The guaranteed minimum sizes are:

TypeMinimum bits guaranteedMinimum signed rangeHeader macros
char8−127..127 or 0..255CHAR_BIT, CHAR_MIN, CHAR_MAX
short16−32,767..32,767SHRT_MIN, SHRT_MAX, USHRT_MAX
int16−32,767..32,767INT_MIN, INT_MAX, UINT_MAX
long32−2,147,483,647..2,147,483,647LONG_MIN, LONG_MAX, ULONG_MAX
long long64±9.2 × 1018LLONG_MIN, LLONG_MAX, ULLONG_MAX
limits_demo.c — let the compiler tell you
#include <stdio.h> #include <limits.h> #include <float.h> int main(void) { printf(“CHAR_BIT = %d\n”, CHAR_BIT); printf(“SCHAR = %d .. %d\n”, SCHAR_MIN, SCHAR_MAX); printf(“UCHAR_MAX = %d\n”, UCHAR_MAX); printf(“SHRT = %d .. %d\n”, SHRT_MIN, SHRT_MAX); printf(“USHRT_MAX = %d\n”, USHRT_MAX); printf(“INT = %d .. %d\n”, INT_MIN, INT_MAX); printf(“UINT_MAX = %u\n”, UINT_MAX); printf(“LLONG_MAX = %lld\n”, LLONG_MAX); printf(“FLT_MAX = %e, FLT_EPSILON = %e\n”, FLT_MAX, FLT_EPSILON); printf(“DBL_MAX = %e\n”, DBL_MAX); return 0; }
Output (verified by compiling and running)
CHAR_BIT = 8 SCHAR = -128 .. 127 UCHAR_MAX = 255 SHRT = -32768 .. 32767 USHRT_MAX = 65535 INT = -2147483648 .. 2147483647 UINT_MAX = 4294967295 LLONG_MAX = 9223372036854775807 FLT_MAX = 3.402823e+38, FLT_EPSILON = 1.192093e-07 DBL_MAX = 1.797693e+308
Embedded tip: Use these macros instead of hard-coding 255 or 65535. UINT16_MAX and friends from <stdint.h> are even better (Lesson 3.2).

10. printf Format Specifiers for Each Data Type

When you print values (over UART, SWO/ITM or semihosting) the format specifier must match the type of the argument. A mismatch is undefined behaviour: wrong digits at best, a crash at worst. Here are the specifiers you will use with data types in embedded C:

SpecifierArgument typePrintsExample output
%d / %iintsigned decimal-42
%uunsigned intunsigned decimal4294967295
%x / %Xunsigned inthexadecimal, lower/upperff / FF
%ounsigned intoctal377
%cint (char)one characterA
%schar *null-terminated stringhello
%fdouble (float promoted)decimal float3.141593
%edoublescientific3.141593e+00
%gdoubleshorter of %f/%e3.14159
%pvoid *pointer address0x20000000
%ld / %lulong / unsigned longlong decimal123456
%lld / %llulong long64-bit decimal9000000000
%hd / %hhdshort / char (promoted to int)narrow decimal-5
%zusize_tresult of sizeof4
%%–literal percent sign%
printf_specifiers.c — every common specifier
#include <stdio.h> int main(void) { int i = –42; unsigned int u = 4000000000u; unsigned char b = 0xAB; short sh = –5; long l = 123456L; long long ll = 9000000000LL; float f = 3.14159f; double d = 2.718281828; printf(“%%d : %d\n”, i); printf(“%%u : %u\n”, u); printf(“%%x : %x %%X: %X %%#x: %#x\n”, b, b, b); printf(“%%o : %o\n”, b); printf(“%%c : %c\n”, 65); printf(“%%hd : %hd\n”, sh); printf(“%%ld : %ld\n”, l); printf(“%%lld: %lld\n”, ll); printf(“%%f : %f %%.2f: %.2f %%e: %e\n”, f, f, d); printf(“%%zu : %zu\n”, sizeof(long long)); printf(“%%5d|%%-5d|%%05d : |%5d|%-5d|%05d|\n”, 42, 42, 42); return 0; }
Output (verified by compiling and running)
%d : -42 %u : 4000000000 %x : ab %X: AB %#x: 0xab %o : 253 %c : A %hd : -5 %ld : 123456 %lld: 9000000000 %f : 3.141590 %.2f: 3.14 %e: 2.718282e+00 %zu : 8 %5d|%-5d|%05d : | 42|42 |00042|
Embedded gotchas: The default newlib-nano C library used by STM32CubeIDE does not print floats unless you add the linker flag -u _printf_float; %lld may also be missing. When in doubt, print floats as scaled integers (for example %d.%02d).

11. Choosing the Right Type in Firmware

What you storeGood choiceWhy
Hardware register valueuint32_t (Lesson 3.2)Exact width, never negative, no sign surprises
Byte received over UART/SPIuint8_tExactly 1 byte, unsigned
Text charactercharMatches C string functions
ADC reading (12 bit)uint16_tFits 0..4095; half the RAM of int
Temperature −40..+125 °Cint8_t or int16_tNeeds negative values
Loop counterint or unsigned intNative word is fastest on ARM
Millisecond tick counteruint32_tWraps after 49.7 days; handle with subtraction
Microsecond time since bootuint64_t32 bits wrap after only 71 minutes
Sensor value with fractionsfloatHardware FPU on Cortex-M4F

The guiding principle: pick the smallest type that is always large enough, unless the CPU word size makes a larger type faster, and always document the range in a comment.

12. Common Mistakes With C Data Types

Mistake 1 — assuming int is 32 bits: It is 16 bits on AVR and MSP430. Code that stores 100000 in an int silently breaks when ported.
Mistake 2 — using plain char for numbers: char x = 200; is 200 on ARM and −56 on x86.
Mistake 3 — wrong printf specifier: printf("%d", 3.5f) and printf("%f", 10) are undefined behaviour and print garbage.
Mistake 4 — forgetting the f suffix: float x = 0.1; works but computes in double and then converts; on a Cortex-M4F this wastes time and code size.
Mistake 5 — comparing floats with ==: 0.1 + 0.2 == 0.3 is false in double arithmetic (the sum is 0.30000000000000004). Use a tolerance.
Mistake 6 — storing a timestamp in a too-small type: uint16_t milliseconds wraps every 65.5 seconds.
Mistake 7 — integer division where a fraction was intended: float avg = sum / count; performs integer division first. Cast: (float)sum / count.
Mistake 8 — declaring a register with a plain int: If the hardware register is exactly 32 bits, use uint32_t so the layout can never change with the compiler.

13. Best Practices for Data Types in Embedded C

  • Prefer fixed-width types from <stdint.h> for anything that touches hardware, protocols or memory layout.
  • Use plain char only for text; use uint8_t for bytes.
  • Make signedness explicit: unsigned for bits, signed for quantities.
  • Use float (with the f suffix) on Cortex-M4F; avoid double unless required.
  • Print sizeof results with %zu and fixed-width types with the PRIu32 family from <inttypes.h>.
  • Compile with -Wall -Wextra -Wconversion -Wformat so mismatches are reported.
  • Add compile-time size checks for portability-sensitive types, for example _Static_assert(sizeof(int) == 4, "int must be 32 bits");.

Key Takeaways — Lesson 3.1

  • A type fixes the size, the interpretation and the operations of a variable.
  • Only sizeof(char) == 1 is guaranteed; every other size depends on the architecture.
  • On the STM32F407: char 1, short 2, int 4, long 4, long long 8, float 4, double 8, pointer 4 bytes.
  • On 64-bit Linux long is 8 bytes; on 16-bit MCUs int is 2 bytes. This is the portability problem.
  • Integer types default to signed; plain char is implementation-defined (unsigned on ARM).
  • float has hardware support on the Cortex-M4F; double does not.
  • void means no value, no parameters, or a generic pointer.
  • Use <limits.h> to query real limits and match each printf specifier to the argument type.
  • Mastering data types in embedded C leads directly to fixed-width types in the next lesson.

Frequently Asked Questions

What are the basic data types in embedded C?
The fundamental types are char, short, int, long, long long, float, double and void, each in signed and unsigned variants for the integer types. They are the building blocks of every embedded C program.
What is the size of int in embedded C?
It depends on the target. On ARM Cortex-M (STM32F407) int is 4 bytes; on 8-bit AVR and 16-bit MSP430 it is 2 bytes. Use int32_t when you need exactly 32 bits.
Is char signed or unsigned in C?
It is implementation-defined. GCC for x86 treats plain char as signed, whereas the ARM EABI makes it unsigned. Use signed char or unsigned char (or int8_t/uint8_t) when the sign matters.
Why is the size of long different on different platforms?
The standard only requires at least 32 bits. 32-bit systems and 64-bit Windows use 4 bytes, while 64-bit Linux and macOS (LP64) use 8 bytes.
What is the difference between float and double?
float is 32-bit single precision (about 7 digits); double is 64-bit double precision (about 15 digits). The Cortex-M4F has an FPU only for float.
What does void mean in C?
It means “no type”: a function that returns nothing, a function with no parameters, or a generic pointer (void *) that must be cast before it is dereferenced.
Which printf specifier prints sizeof?
Use %zu, because sizeof yields size_t. On small embedded libraries that lack it, cast to unsigned long and use %lu.
Why should embedded code avoid plain int?
Because its width varies by platform and it is signed by default. For registers, protocols and buffers use fixed-width types such as uint8_t and uint32_t.

Ready to practise?

Next: sizeof output on different platforms, every printf specifier in action, and signed versus unsigned overflow demonstrated with real programs.

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 *