What are C Data Types Explained-Free Online C Programming Course

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.

4
fundamental types
4
type qualifiers
4
runnable examples

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, float and double.
  • 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_t and 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 family of C data types
Basiccharintfloatdouble
Size qualifiersshortlonglong long
Sign qualifierssignedunsigned
Derivedarraypointerstructunionfunction
Specialvoidenum_Bool / bool

The Four Fundamental C Data Types

TypeStoresTypical useprintf format
charOne character or a small integer; exactly one byteText, raw bytes, tiny counters%c or %d
intWhole numbers; the natural word size for the machineLoop counters, general arithmetic%d
floatSingle-precision real numbersSensor values where memory or speed matters%f
doubleDouble-precision real numbersDefault 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: short makes an int no larger than a plain int, and long or long long make it at least as large. They give types such as short int, long int and long long int, usually written without the word int.
  • Sign qualifiers: signed allows negative values; unsigned allows 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.

TypeGuaranteed minimumMinimum range (signed)
char8 bits-127 to 127 if signed, 0 to 255 if unsigned
short16 bits-32,767 to 32,767
int16 bits-32,767 to 32,767
long32 bits-2,147,483,647 to 2,147,483,647
long long64 bitsabout -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

Type8/16-bit microcontroller32-bit ARM (Cortex-M, ILP32)64-bit Linux (LP64)64-bit Windows (LLP64)
char1111
short2222
int2444
long4484
long long8888
pointer2488

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.

The byte 0xF6 read two ways
Bit position76543210
Bit value11110110
Unsigned weight1286432168421
Signed weight-1286432168421
ResultUnsigned 246Signed -10

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.

TypeTypical sizeReliable decimal digitsApproximate maximum
float4 bytes63.4 x 1038
double8 bytes151.8 x 10308
long double8 to 16 bytes15 to 18depends 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.

TypeMeaningTypical use
int8_t, uint8_tExactly 8 bits, signed and unsignedBytes, small sensors
int16_t, uint16_tExactly 16 bitsADC samples, audio
int32_t, uint32_tExactly 32 bitsRegister values
int64_t, uint64_tExactly 64 bitsTimestamps
size_tUnsigned type for object sizes and indicesArray indexing, sizeof results
uintptr_tUnsigned integer wide enough to hold a pointerAddress 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
  • sizeof returns size_t, so the matching format is %zu.
  • On this 64-bit Linux build, long and pointers are 8 bytes and long double is 16.
  • Compile with -m32 (if the 32-bit libraries are installed) and watch long and void * 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_MIN is one larger in magnitude than INT_MAX, the extra negative value mentioned earlier.
  • FLT_DIG is 6 and DBL_DIG is 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 char holding 200 became -56 because this platform treats char as 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.3 is false because the left side is a hair above 0.3, as the 5.551e-17 difference shows. Comparing with a small tolerance, here with fabs, gives the answer people intend.
  • Setting bits 5 and 0 of a 32-bit value gives 0x21; the use of 1u avoids 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

MistakeResultFix
Assuming int is 32 bits everywhereOverflow on 16-bit microcontrollersUse int32_t when width matters
Storing a pointer in int or longTruncated address on 64-bit Windows or any 64-bit system with 32-bit intUse uintptr_t
Looping with an unsigned counter down to below zeroInfinite loopTest i-- in the condition or use a signed index
Comparing floats with ==Unexpected false resultsCompare with a tolerance
Mixing signed and unsigned in a comparisonThe signed value is converted to unsigned, so -1 becomes hugeKeep both operands the same signedness
Using plain char for bytesSign extension on readUse uint8_t
Printing with the wrong format, such as %d for a longGarbage output or undefined behaviourMatch %ld, %zu, %lld; compile with -Wall
Integer division when a fraction is wantedTruncated resultConvert 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 int or size_t for loop indices; the compiler picks the fastest width.
  • Use unsigned for 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 float in 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, float and double; qualifiers short, long, signed and unsigned create the rest.
  • The standard guarantees minimum ranges, not exact sizes. Measure with sizeof and read the limits from limits.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 char may be signed or unsigned, depending on the platform.
  • Floating-point values are approximate; compare with a tolerance.
  • Use stdint.h types 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

Leave a Reply

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