Lesson Progress
0%
Lesson  ·  Data Types, Fully Explained

Every Data Type in C — Size, Range & Precision

int, float, long, double, char — they all "store numbers or letters," but each one trades off memory size against range and precision differently. This lesson covers every major type, why it exists, and the bugs that show up when you pick the wrong one.

Integer family
Signed vs unsigned
Floating-point family
char & strings
Conversion & casting
📖

Why Data Types Even Exist

Every variable in C reserves a fixed number of bytes in memory the moment it's declared. The data type you choose decides exactly how many bytes get reserved — and that byte count directly decides two things: the largest/smallest value the variable can hold, and how precisely it can represent fractional numbers.

Typical sizes on a 64-bit system (always confirm with sizeof — sizes can vary by compiler/platform)

char
1 byte
short
2 bytes
int / float
4 bytes
long
4 or 8 bytes (platform-dependent)
double / long long
8 bytes
long double
8, 12, or 16 bytes
⚠️ C never guarantees exact sizes. The C standard only guarantees minimum ranges, not exact byte counts — int is usually 4 bytes today but was 2 bytes on very old systems. Always use sizeof() to check on your actual compiler instead of assuming.
example 1
1

The Integer Family: char, short, int, long, long long

Whole numbers, different ranges

These five types all store whole numbers — the only difference is how many bytes each reserves, which directly sets the largest value it can hold. Use the smallest type that comfortably fits your data; using long long everywhere "to be safe" wastes memory for no benefit in most programs.

Example 1 · integer_family.c
integer_family.c
C
#include <stdio.h>
#include <limits.h>

int main() {
    printf("%-12s size=%zu bytes   range: %d to %d\n",
           "char", sizeof(char), CHAR_MIN, CHAR_MAX);
    printf("%-12s size=%zu bytes   range: %d to %d\n",
           "short", sizeof(short), SHRT_MIN, SHRT_MAX);
    printf("%-12s size=%zu bytes   range: %d to %d\n",
           "int", sizeof(int), INT_MIN, INT_MAX);
    printf("%-12s size=%zu bytes   range: %ld to %ld\n",
           "long", sizeof(long), LONG_MIN, LONG_MAX);
    printf("%-12s size=%zu bytes   range: %lld to %lld\n",
           "long long", sizeof(long long), LLONG_MIN, LLONG_MAX);

    return 0;
}
terminal — typical 64-bit output
output
char         size=1 bytes   range: -128 to 127
short        size=2 bytes   range: -32768 to 32767
int          size=4 bytes   range: -2147483648 to 2147483647
long         size=8 bytes   range: -9223372036854775808 to 9223372036854775807
long long    size=8 bytes   range: -9223372036854775808 to 9223372036854775807
TypeTypical sizeUse when...
char1 byteStoring a single character or a very small number (-128 to 127)
short2 bytesSmall counters where memory is tight (rare in modern code)
int4 bytesThe default choice for whole numbers — loop counters, ages, counts
long4 or 8 bytesLarger counts — file sizes, timestamps
long long8 bytesVery large values — guaranteed at least 64-bit by the standard
example 2
2

Signed vs Unsigned — Where the Range Comes From

Trading negative numbers for extra positive range

Every integer type is signed by default — half its range is negative, half positive. Adding unsigned tells C "this variable will never be negative," which frees up that entire negative half for extra positive range instead.

Example 2 · signed_unsigned.c
signed_unsigned.c
C
#include <stdio.h>

int main() {
    signed char   s = -1;
    unsigned char u = -1;   // same bit pattern, different interpretation!

    printf("signed char with -1   = %d\n", s);
    printf("unsigned char with -1 = %d\n", u);   // wraps to the top of its range

    // The classic unsigned underflow bug
    unsigned int count = 0;
    count--;   // going below 0 wraps AROUND instead of going negative
    printf("unsigned int 0 - 1    = %u  (wrapped around!)\n", count);

    return 0;
}
terminal
output
signed char with -1   = -1
unsigned char with -1 = 255
unsigned int 0 - 1    = 4294967295  (wrapped around!)
⚠️ This is a real, common bug. A loop like for (unsigned int i = length - 1; i >= 0; i--) never terminates when i reaches 0 and wraps around to a huge number instead of going negative — an infinite loop that's easy to miss during code review.
example 3
3

The Floating-Point Family: float, double, long double

Trading memory for decimal precision

These three types all store numbers with a decimal point, but they differ in precision — how many significant digits they can represent accurately before rounding error creeps in. More bytes generally means more precision.

Example 3 · float_precision.c
float_precision.c
C
#include <stdio.h>

int main() {
    float       f = 1.0 / 3.0;
    double      d = 1.0 / 3.0;
    long double ld = 1.0L / 3.0L;

    printf("float       (~7  digits): %.10f\n", f);
    printf("double      (~15 digits): %.10f\n", d);
    printf("long double (~18 digits): %.10Lf\n", ld);

    printf("\nsizeof(float)       = %zu bytes\n", sizeof(float));
    printf("sizeof(double)      = %zu bytes\n", sizeof(double));
    printf("sizeof(long double) = %zu bytes\n", sizeof(long double));

    return 0;
}
terminal — typical output
output
float       (~7  digits): 0.3333333433
double      (~15 digits): 0.3333333333
long double (~18 digits): 0.3333333333

sizeof(float)       = 4 bytes
sizeof(double)      = 8 bytes
sizeof(long double) = 16 bytes
TypeSizePrecisionFormat specifier
float4 bytes~6-7 significant digits%f
double8 bytes~15-16 significant digits%f (also used for double in printf)
long double8, 12, or 16 bytes~18-19 significant digits%Lf
💡 Default choice: use double. Unless you're storing millions of values and need to save memory, double is the standard default for decimal numbers in C — float's precision loss shows up surprisingly fast in real calculations.
example 4
4

char — A Number Wearing a Letter's Costume

Plus a quick recap of strings

A char isn't fundamentally different from a small integer — internally it just stores a number from -128 to 127 (or 0 to 255 if unsigned), and that number is interpreted as an ASCII character code. This is why you can do arithmetic directly on characters.

Example 4 · char_and_strings.c
char_and_strings.c
C
#include <stdio.h>

int main() {
    char letter = 'A';

    printf("'A' printed as %%c : %c\n", letter);
    printf("'A' printed as %%d : %d  (its ASCII code)\n", letter);

    // char arithmetic: adding a number shifts through the alphabet
    char next = letter + 1;
    printf("'A' + 1 = %c\n", next);

    // A string is just an array of these char boxes, ending in '\0'
    char word[] = "Hi";
    printf("\nword[0] = %c (code %d)\n", word[0], word[0]);
    printf("word[1] = %c (code %d)\n", word[1], word[1]);
    printf("word[2] = %d (the hidden null terminator)\n", word[2]);

    return 0;
}
terminal
output
'A' printed as %c : A
'A' printed as %d : 65  (its ASCII code)
'A' + 1 = B

word[0] = H (code 72)
word[1] = i (code 105)
word[2] = 0 (the hidden null terminator)
💡 This connects directly to the Strings lesson: since char is just a small int underneath, and a string is just a char array, "string" was never a distinct data type in C at all — it's these two ideas stacked together.
example 5
5

Type Conversion & Casting

Implicit vs explicit, and where data gets lost

C freely converts between types when needed — this is called implicit conversion. Sometimes you want to force a conversion yourself, called explicit casting, written as (type)value. Both can silently lose data if you're not careful.

Example 5 · type_conversion.c
type_conversion.c
C
#include <stdio.h>

int main() {
    // Implicit: int automatically becomes float during division
    int a = 7, b = 2;
    printf("7 / 2 (int/int)        = %d   (truncated, no cast)\n", a / b);
    printf("(float)7 / 2           = %.1f (explicit cast forces float math)\n", (float)a / b);

    // Explicit: forcing a float DOWN into an int truncates the decimal part
    float price = 9.99;
    int wholePart = (int)price;
    printf("(int)9.99              = %d   (decimal part simply dropped)\n", wholePart);

    // Overflow: forcing a big int into a small type wraps around
    int big = 300;
    char small = (char)big;   // char only holds up to 127
    printf("(char)300              = %d   (wrapped — 300 doesn't fit in a char)\n", small);

    return 0;
}
terminal
output
7 / 2 (int/int)        = 3   (truncated, no cast)
(float)7 / 2           = 3.5 (explicit cast forces float math)
(int)9.99              = 9   (decimal part simply dropped)
(char)300              = 44   (wrapped — 300 doesn't fit in a char)
⚠️ Casting doesn't round — it truncates. (int)9.99 becomes 9, not 10. If you need rounding, use round() from math.h before casting, not the cast itself.
Format specifierType
%dint
%cchar
%ffloat / double
%Lflong double
%ldlong
%lldlong long
%uunsigned int
%zusize_t (what sizeof returns)
%sstring (char array)
quiz
Q

Quick Quiz

Question 1 of 5

What does a data type's size in bytes directly determine?

Question 2 of 5

Why can an unsigned int hold larger positive values than a signed int of the same size?

Question 3 of 5

Which type gives the most decimal precision?

Question 4 of 5

What does a char actually store internally?

Question 5 of 5

What does (int)9.99 evaluate to?

Lesson Checklist

  • I understand why data type size determines range and precision
  • I can list char, short, int, long, long long in order of typical size
  • I understand the difference between signed and unsigned
  • I can explain the unsigned underflow/wraparound bug
  • I know float, double, and long double differ mainly in precision
  • I understand char is really a small integer interpreted as ASCII
  • I can explain implicit vs explicit type conversion
  • I know casting truncates rather than rounds
  • I can match common format specifiers to their types
  • I completed the quiz