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)
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.The Integer Family: char, short, int, long, long long
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.
#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; }
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
| Type | Typical size | Use when... |
|---|---|---|
| char | 1 byte | Storing a single character or a very small number (-128 to 127) |
| short | 2 bytes | Small counters where memory is tight (rare in modern code) |
| int | 4 bytes | The default choice for whole numbers — loop counters, ages, counts |
| long | 4 or 8 bytes | Larger counts — file sizes, timestamps |
| long long | 8 bytes | Very large values — guaranteed at least 64-bit by the standard |
Signed vs Unsigned — Where the Range Comes From
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.
#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; }
signed char with -1 = -1 unsigned char with -1 = 255 unsigned int 0 - 1 = 4294967295 (wrapped around!)
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.The Floating-Point Family: float, double, long double
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.
#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; }
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
| Type | Size | Precision | Format specifier |
|---|---|---|---|
| float | 4 bytes | ~6-7 significant digits | %f |
| double | 8 bytes | ~15-16 significant digits | %f (also used for double in printf) |
| long double | 8, 12, or 16 bytes | ~18-19 significant digits | %Lf |
double is the standard default for decimal numbers in C — float's precision loss shows up surprisingly fast in real calculations.char — A Number Wearing a Letter's Costume
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.
#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; }
'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)
Type Conversion & Casting
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.
#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; }
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)
(int)9.99 becomes 9, not 10. If you need rounding, use round() from math.h before casting, not the cast itself.| Format specifier | Type |
|---|---|
| %d | int |
| %c | char |
| %f | float / double |
| %Lf | long double |
| %ld | long |
| %lld | long long |
| %u | unsigned int |
| %zu | size_t (what sizeof returns) |
| %s | string (char array) |
Quick Quiz
What does a data type's size in bytes directly determine?
Why can an unsigned int hold larger positive values than a signed int of the same size?
Which type gives the most decimal precision?
What does a char actually store internally?
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