Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
C Data Types Explained
Lecture 4 of the Elements of C chapter. Sizes, ranges, signed versus unsigned, qualifiers, floating point and fixed-width types, with tested programs from our free embedded systems course.
Every value in a C program has a type, and the type decides how many bytes it occupies, how those bytes are interpreted, and which operations are legal. Mastering C data types is the single most valuable step in this free embedded systems course, because most firmware bugs, from a counter that wraps at the wrong moment to a register write that clobbers neighbouring bits, come from misjudging the size or sign of a type.
This lecture explains the four fundamental types, the size and sign qualifiers, how ranges are calculated, why the same program prints different sizes on different machines, and how stdint.h gives you exact-width types for hardware work.
What You Will Learn
- The four fundamental C data types:
char,int,floatanddouble. - How size qualifiers (
short,long) and sign qualifiers (signed,unsigned) create the other integer types. - How to compute the range of any integer type, and what the sign bit means.
- Why type sizes depend on the platform, and what the standard actually guarantees.
- How overflow behaves for signed and unsigned values.
- How floating-point numbers store approximate values, and why 0.1 + 0.2 is not 0.3.
- When to use
int8_t,uint32_tand the other fixed-width types.
Prerequisites
- Lectures on the character set, escape sequences and keywords (cp1_1 to cp1_3).
- Basic binary numbers: bits, bytes and powers of two.
- A C compiler; the outputs below come from GCC on 64-bit Linux.
What Is a Data Type in C?
A data type tells the compiler three things: how much memory to reserve, how to interpret the bits stored there, and which operations make sense. The same eight bits can mean the number 65, the letter A, or the number -191 on a different encoding, depending entirely on the declared type. C has no run-time type tags; the compiler uses the declared type to generate the right machine instructions, and if you lie about the type, nothing stops you.
The Four Fundamental C Data Types
| Type | Stores | Typical use | printf format |
|---|---|---|---|
char | One character or a small integer; exactly one byte | Text, raw bytes, tiny counters | %c or %d |
int | Whole numbers; the natural word size for the machine | Loop counters, general arithmetic | %d |
float | Single-precision real numbers | Sensor values where memory or speed matters | %f |
double | Double-precision real numbers | Default for floating-point maths | %f or %lf |
Two points deserve attention. First, char is both a character type and the smallest integer type; in C a char is by definition one byte, whatever the number of bits in that byte (CHAR_BIT, which is 8 on essentially every modern machine). Second, int is meant to match what the processor handles most efficiently, which is why its size has changed over the decades: 16 bits on old DOS compilers and many 8-bit and 16-bit microcontrollers, 32 bits on almost everything since.
Type Qualifiers: Size and Sign Modifiers
You rarely need a brand-new type; you modify the basic ones. There are two kinds of integer modifiers.
- Size qualifiers:
shortmakes anintno larger than a plain int, andlongorlong longmake it at least as large. They give types such asshort int,long intandlong long int, usually written without the word int. - Sign qualifiers:
signedallows negative values;unsignedallows only zero and positive values and doubles the positive range. If you write neither, integer types are signed by default.
The one exception: plain char is neither guaranteed signed nor unsigned. It is signed on typical x86 Linux builds and unsigned by default on many ARM and PowerPC Linux builds. Use signed char or unsigned char whenever the sign matters, and uint8_t for raw bytes.
What the C Standard Guarantees
The language does not fix exact sizes. It only guarantees minimum ranges, and an ordering of sizes.
| Type | Guaranteed minimum | Minimum range (signed) |
|---|---|---|
char | 8 bits | -127 to 127 if signed, 0 to 255 if unsigned |
short | 16 bits | -32,767 to 32,767 |
int | 16 bits | -32,767 to 32,767 |
long | 32 bits | -2,147,483,647 to 2,147,483,647 |
long long | 64 bits | about -9.2 x 1018 to 9.2 x 1018 |
The ordering rule is sizeof(char) equals 1, and short is no bigger than int, which is no bigger than long, which is no bigger than long long. Anything beyond that is the compiler’s decision.
Typical Sizes on Real Platforms
| Type | 8/16-bit microcontroller | 32-bit ARM (Cortex-M, ILP32) | 64-bit Linux (LP64) | 64-bit Windows (LLP64) |
|---|---|---|---|---|
char | 1 | 1 | 1 | 1 |
short | 2 | 2 | 2 | 2 |
int | 2 | 4 | 4 | 4 |
long | 4 | 4 | 8 | 4 |
long long | 8 | 8 | 8 | 8 |
| pointer | 2 | 4 | 8 | 8 |
The row for long is the famous trap: it is 8 bytes on 64-bit Linux but 4 bytes on 64-bit Windows. Code that stores a pointer or a file size in a long works on one and breaks on the other.
How Integer Ranges Are Calculated
An integer type with n bits can hold 2n different bit patterns. For an unsigned type those patterns represent 0 to 2n – 1. For a signed type on all mainstream hardware (two’s complement), they represent -2n-1 to 2n-1 – 1. That is why signed 8-bit values run from -128 to 127 while unsigned 8-bit values run from 0 to 255, and why the signed range has one extra negative number.
The Sign Bit and Two’s Complement
In an unsigned type every bit adds its place value. In a signed two’s complement type, the leftmost bit carries a negative weight instead. The same pattern therefore means two different numbers depending on the declared sign. This is also why the book statement “signed types have a smaller range because one bit is used for the sign” is a simplification: the count of values is the same, but half of them are shifted below zero.
Overflow: Wrap-Around Versus Undefined Behaviour
Unsigned arithmetic is defined to wrap modulo 2n. Adding 10 to an unsigned 8-bit 250 gives 260 mod 256, which is 4. Signed overflow is different: the standard calls it undefined behaviour, so the compiler may assume it never happens and optimise accordingly. Code such as if (x + 1 > x) can be compiled to “always true” for a signed x. For counters that are expected to wrap, use unsigned types.
Integer Promotion
Arithmetic is never done on types smaller than int. When you add two char or short values, both are first promoted to int (or unsigned int), and the result is converted back only if you assign it to a small variable. That is why adding 10 to a uint8_t holding 250 does not overflow in the expression, but the stored result after assignment is 4.
Floating-Point Types: float, double and long double
Real numbers are stored in a binary scientific notation: a sign bit, an exponent and a fraction (mantissa). On nearly every platform float follows the IEEE 754 32-bit format and double follows the 64-bit format. long double is whatever the platform offers beyond double: 80-bit extended precision stored in 16 bytes on x86-64 Linux, and the same as double on many embedded targets.
| Type | Typical size | Reliable decimal digits | Approximate maximum |
|---|---|---|---|
float | 4 bytes | 6 | 3.4 x 1038 |
double | 8 bytes | 15 | 1.8 x 10308 |
long double | 8 to 16 bytes | 15 to 18 | depends on platform |
Most decimal fractions, such as 0.1, have no exact binary representation, in the same way that one third has no exact decimal one. Comparing floating-point values with == is therefore unreliable; compare with a tolerance. Many small microcontrollers have no floating-point unit, so float arithmetic is done in software and can be dozens of times slower than integer arithmetic. Fixed-point integers are the usual embedded alternative.
Fixed-Width Integer Types from stdint.h
When you work with hardware registers, file formats or network packets, you need exact sizes. Since C99, <stdint.h> provides them.
| Type | Meaning | Typical use |
|---|---|---|
int8_t, uint8_t | Exactly 8 bits, signed and unsigned | Bytes, small sensors |
int16_t, uint16_t | Exactly 16 bits | ADC samples, audio |
int32_t, uint32_t | Exactly 32 bits | Register values |
int64_t, uint64_t | Exactly 64 bits | Timestamps |
size_t | Unsigned type for object sizes and indices | Array indexing, sizeof results |
uintptr_t | Unsigned integer wide enough to hold a pointer | Address arithmetic |
In embedded work, prefer these types for anything that touches hardware and keep plain int for ordinary loop counters.
Hands-On Examples: C Data Types in Practice
All programs were compiled with gcc -Wall -Wextra -std=c17 on 64-bit Linux. Your output may differ for sizes on other systems, which is precisely the lesson.
Example 1: Measure the Size of Every Type
Goal: use sizeof to discover what your own compiler does, rather than trusting a table.
#include <stdio.h>
#include <stddef.h>
int main(void)
{
printf("char : %zu\n", sizeof(char));
printf("short : %zu\n", sizeof(short));
printf("int : %zu\n", sizeof(int));
printf("long : %zu\n", sizeof(long));
printf("long long : %zu\n", sizeof(long long));
printf("float : %zu\n", sizeof(float));
printf("double : %zu\n", sizeof(double));
printf("long double : %zu\n", sizeof(long double));
printf("void * : %zu\n", sizeof(void *));
printf("size_t : %zu\n", sizeof(size_t));
return 0;
}
gcc -Wall -Wextra -std=c17 sizes.c -o sizes
./sizes
char : 1
short : 2
int : 4
long : 8
long long : 8
float : 4
double : 8
long double : 16
void * : 8
size_t : 8
sizeofreturnssize_t, so the matching format is%zu.- On this 64-bit Linux build,
longand pointers are 8 bytes andlong doubleis 16. - Compile with
-m32(if the 32-bit libraries are installed) and watchlongandvoid *drop to 4.
Try it yourself: add short, unsigned and _Bool
Add lines for unsigned int and _Bool. The unsigned int has the same size as int; _Bool is 1 byte on mainstream compilers, because the smallest addressable unit is a byte.
Example 2: Print the Limits from limits.h and float.h
Goal: read ranges from the standard headers instead of memorising them.
#include <stdio.h>
#include <limits.h>
#include <float.h>
int main(void)
{
printf("CHAR_BIT = %d\n", CHAR_BIT);
printf("signed char : %d to %d\n", SCHAR_MIN, SCHAR_MAX);
printf("unsigned char : 0 to %d\n", UCHAR_MAX);
printf("short : %d to %d\n", SHRT_MIN, SHRT_MAX);
printf("unsigned short: 0 to %d\n", USHRT_MAX);
printf("int : %d to %d\n", INT_MIN, INT_MAX);
printf("unsigned int : 0 to %u\n", UINT_MAX);
printf("long : %ld to %ld\n", LONG_MIN, LONG_MAX);
printf("unsigned long : 0 to %lu\n", ULONG_MAX);
printf("float digits : %d, max %e\n", FLT_DIG, FLT_MAX);
printf("double digits : %d, max %e\n", DBL_DIG, DBL_MAX);
return 0;
}
CHAR_BIT = 8
signed char : -128 to 127
unsigned char : 0 to 255
short : -32768 to 32767
unsigned short: 0 to 65535
int : -2147483648 to 2147483647
unsigned int : 0 to 4294967295
long : -9223372036854775808 to 9223372036854775807
unsigned long : 0 to 18446744073709551615
float digits : 6, max 3.402823e+38
double digits : 15, max 1.797693e+308
- The signed char range is -128 to 127 and the unsigned range 0 to 255, matching the formula for n = 8.
INT_MINis one larger in magnitude thanINT_MAX, the extra negative value mentioned earlier.FLT_DIGis 6 andDBL_DIGis 15, which is how many decimal digits survive a round trip.
Try it yourself: compute INT_MAX without the header
For a 32-bit signed int, 231 – 1 equals 2147483647. In code, use (int)((1u << 31) - 1). Do not write 1 << 31 with a signed 1, which overflows.
Example 3: Wrap-Around, Sign Bit and Plain char
Goal: observe unsigned wrap-around, the two readings of 0xF6, and the signedness of plain char.
#include <stdio.h>
#include <stdint.h>
#include <limits.h>
int main(void)
{
uint8_t small = 250;
unsigned int zero = 0;
uint8_t raw = 0xF6;
int8_t as_signed = (int8_t)raw;
char c = (char)200;
small += 10;
zero--;
printf("250 + 10 stored in uint8_t = %u\n", small);
printf("0u - 1 = %u\n", zero);
printf("0x%02X read as unsigned = %u, as signed = %d\n", raw, raw, as_signed);
printf("char 200 became %d (plain char is %s here)\n", c,
CHAR_MIN < 0 ? "signed" : "unsigned");
return 0;
}
250 + 10 stored in uint8_t = 4
0u - 1 = 4294967295
0xF6 read as unsigned = 246, as signed = -10
char 200 became -56 (plain char is signed here)
- 250 + 10 is 260, which wraps to 4 in an 8-bit unsigned variable.
- Decrementing an unsigned zero gives the largest value, 4294967295 for a 32-bit unsigned int. This is the cause of the classic loop bug
for (unsigned i = 10; i >= 0; i--), which never ends. - The byte 0xF6 is 246 as unsigned and -10 as signed, matching the diagram above.
- The plain
charholding 200 became -56 because this platform treatscharas signed. On an ARM Linux build the same line prints 200.
Try it yourself: predict the value of (int8_t)0x80
It is -128, the most negative 8-bit value. The pattern 1000 0000 has only the negative weight set.
Example 4: Floating-Point Surprises and a Register Mask
Goal: see why decimal fractions are approximate, and use uint32_t to set register bits.
#include <stdio.h>
#include <stdint.h>
#include <math.h>
int main(void)
{
float f = 0.1f;
double d = 0.1;
uint32_t reg = 0;
printf("float 0.1 = %.20f\n", f);
printf("double 0.1 = %.20f\n", d);
printf("0.1 + 0.2 == 0.3 ? %s\n", (0.1 + 0.2 == 0.3) ? "yes" : "no");
printf("difference = %.3e\n", (0.1 + 0.2) - 0.3);
printf("close enough? %s\n", fabs((0.1 + 0.2) - 0.3) < 1e-9 ? "yes" : "no");
reg |= (1u << 5);
reg |= (1u << 0);
printf("reg = 0x%08X\n", (unsigned)reg);
printf("7 / 2 = %d, 7 / 2.0 = %.1f\n", 7 / 2, 7 / 2.0);
return 0;
}
gcc -Wall -Wextra -std=c17 floats.c -o floats -lm
./floats
float 0.1 = 0.10000000149011611938
double 0.1 = 0.10000000000000000555
0.1 + 0.2 == 0.3 ? no
difference = 5.551e-17
close enough? yes
reg = 0x00000021
7 / 2 = 3, 7 / 2.0 = 3.5
- The float and double versions of 0.1 differ after the seventh digit, matching the 6 and 15 reliable digits in the table.
- The comparison
0.1 + 0.2 == 0.3is false because the left side is a hair above 0.3, as the 5.551e-17 difference shows. Comparing with a small tolerance, here withfabs, gives the answer people intend. - Setting bits 5 and 0 of a 32-bit value gives 0x21; the use of
1uavoids signed shift problems. - Integer division throws away the fraction: 7 / 2 is 3, while 7 / 2.0 is 3.5.
Try it yourself: clear bit 5 of reg without touching other bits
Use reg &= ~(1u << 5);. The tilde builds a mask with every bit set except bit 5, and AND keeps all other bits unchanged. Expect 0x00000001 afterwards.
Common Mistakes with C Data Types
| Mistake | Result | Fix |
|---|---|---|
Assuming int is 32 bits everywhere | Overflow on 16-bit microcontrollers | Use int32_t when width matters |
Storing a pointer in int or long | Truncated address on 64-bit Windows or any 64-bit system with 32-bit int | Use uintptr_t |
| Looping with an unsigned counter down to below zero | Infinite loop | Test i-- in the condition or use a signed index |
Comparing floats with == | Unexpected false results | Compare with a tolerance |
| Mixing signed and unsigned in a comparison | The signed value is converted to unsigned, so -1 becomes huge | Keep both operands the same signedness |
Using plain char for bytes | Sign extension on read | Use uint8_t |
Printing with the wrong format, such as %d for a long | Garbage output or undefined behaviour | Match %ld, %zu, %lld; compile with -Wall |
| Integer division when a fraction is wanted | Truncated result | Convert one operand to a floating type first |
Best Practices, Performance and Security
- Use fixed-width types for data that crosses a boundary: registers, files, network packets.
- Use plain
intorsize_tfor loop indices; the compiler picks the fastest width. - Use
unsignedfor bit masks and counters that should wrap, and keep signed for values that can be negative. - Performance: smaller types save RAM in large arrays but may be slower than the native word size for single variables, because of extra masking after each operation.
- Performance: on a core without an FPU avoid
floatin hot loops; scale values to integers. - Security: integer overflow in a size calculation, such as
count * size, is a well-known source of heap overflows. Check for overflow before allocating. - Security: never use a signed length that comes from outside without checking that it is not negative.
Summary and Key Takeaways
- The four basic types are
char,int,floatanddouble; qualifiersshort,long,signedandunsignedcreate the rest. - The standard guarantees minimum ranges, not exact sizes. Measure with
sizeofand read the limits fromlimits.h. - An n-bit unsigned type spans 0 to 2n – 1; a signed one spans -2n-1 to 2n-1 – 1.
- Unsigned overflow wraps; signed overflow is undefined behaviour.
- Plain
charmay be signed or unsigned, depending on the platform. - Floating-point values are approximate; compare with a tolerance.
- Use
stdint.htypes for hardware work.
Coming next: constants
A constant is a value that cannot change while the program runs. Having seen the types, the next lecture looks at constants: integer, floating-point and character forms, suffixes such as u and L, and how #define compares with const.
Quiz: C Data Types
Question 1
What is the range of a signed 8-bit integer?
Show answer
-128 to 127.
Question 2
What is the largest value of a 16-bit unsigned type?
Show answer
65535, which is 216 – 1.
Question 3
Which types are guaranteed to have exactly one byte of size?
Show answer
char, signed char and unsigned char; sizeof of each is defined as 1.
Question 4
What does uint8_t x = 200; x += 100; leave in x?
Show answer
44. The sum is 300 and 300 mod 256 is 44.
Question 5
Is signed integer overflow defined in C?
Show answer
No, it is undefined behaviour. Unsigned overflow is defined and wraps.
Question 6
What type does sizeof return?
Show answer
size_t, an unsigned integer type; print it with %zu.
Question 7
Why is 0.1 + 0.2 == 0.3 false?
Show answer
Binary floating point cannot store 0.1 or 0.2 exactly, so the sum is slightly different from the stored 0.3.
Question 8
What is the value of 7 / 2 in C?
Show answer
3, because both operands are integers and the fraction is discarded.
Interview Questions on C Data Types
Q1. What is the size of int in C?
Show answer
It is implementation defined, at least 16 bits. On typical 32-bit and 64-bit Linux and Windows builds it is 32 bits; on many 8-bit and 16-bit microcontrollers it is 16. A good answer adds that you use sizeof(int) to find out and int32_t when you need exactly 32 bits.
Q2. Is char signed or unsigned?
Show answer
Implementation defined. It is usually signed on x86 and unsigned on many ARM ABIs. This causes bugs when a byte above 127 is promoted to int: it may sign-extend. Use unsigned char or uint8_t for data bytes.
Q3. What is printed by: unsigned int a = 5; int b = -10; if (a + b > 0) puts(“positive”);
Show answer
“positive”. The int is converted to unsigned, so -10 becomes 4294967286, and the sum wraps to 4294967291, which is greater than zero. Mixing signed and unsigned in an expression is a classic trap.
Q4. How would you detect whether a machine is little-endian?
Show answer
Store the value 1 in a uint16_t, then read the first byte through an unsigned char *. If that byte is 1, the lowest-order byte comes first: little-endian. Using memcpy into a byte array is equally valid and avoids aliasing questions.
Q5. What is the difference between int8_t and char?
Show answer
int8_t is exactly 8 bits and explicitly signed. char is at least 8 bits, one byte by definition, and its signedness depends on the platform. int8_t is optional in the standard and absent on machines with no 8-bit type.
Q6. How do you safely check that adding two ints will not overflow?
Show answer
Test before adding: for non-negative values, a > INT_MAX - b means overflow; for the negative side, a < INT_MIN - b. Alternatively use the compiler builtin __builtin_add_overflow or the C23 stdckdint.h helpers. Do not add first and check afterwards, since signed overflow is undefined.
Q7. Why use size_t rather than int for array indices?
Show answer
size_t can index any object the platform supports and matches the types returned by sizeof and strlen. An int may be too small for large arrays on 64-bit systems, and mixing it with size_t causes sign-comparison warnings.
Frequently Asked Questions
What are the basic data types in C?
The four fundamental types are char, int, float and double. Qualifiers short, long, signed and unsigned build the remaining integer variants.
Why does the size of int change between machines?
The language defines int as the natural word size of the machine, with only a minimum of 16 bits. Different processors therefore use different sizes.
What is the difference between float and double?
Float uses 32 bits and gives about 6 reliable decimal digits; double uses 64 bits and gives about 15. Double is the default type of floating constants such as 3.14.
What is an unsigned integer?
A type that cannot hold negative values. In exchange it can represent values twice as large on the positive side as the same-width signed type.
Should I use long or long long?
Use long long, or better int64_t, when you need at least 64 bits on every platform. Plain long is 32 bits on Windows and 64 bits on 64-bit Linux.
Which free course teaches C data types for embedded jobs?
This lecture is part of our free embedded systems course and free embedded Linux course, which combine theory, runnable code, quizzes and interview questions.
Conclusion
You now know what the compiler means by a data type and why a careless assumption about size or sign is one of the most common sources of embedded bugs. You measured types with sizeof, read ranges from the standard headers, saw one byte take two values depending on its sign, watched an unsigned counter wrap, and learned why floating-point comparison needs care. With these ideas in place, the rest of the language, from constants and operators to pointers and structures, becomes much easier to reason about. Continue with the next lecture of this free embedded C programming course, which covers constants in detail, and keep practising by modifying each example and predicting the result before you run it.
Keep building your foundation
Run the size program on any second machine you have, such as a Raspberry Pi, and compare the results with this page.
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments