C Basics · beginner · ~10 min
- Name the four families of C's built-in types (integer, floating-point, boolean, `void`) and give an example of each. - Choose the right primitive type for a piece of data based on its range, sign, and precision needs. - Use the fixed-width integer types from `<stdint.h>` (such as `int32_t`, `uint8_t`, `uint64_t`) when an exact size matters. - Use `bool`, `true`, and `false` after including `<stdbool.h>`. - Predict the result of signed-vs-unsigned comparisons and avoid the bug where `-1 < 1U` is false. - Recognise integer overflow and floating-point precision as real correctness and safety hazards.
Every value a C program works with has a type. A type is a promise about two things: how many bytes the value occupies in memory, and how the bits in those bytes are interpreted. The same eight bits can be the number 200, the number -56, or the character 'È', depending on the type you declared. In the Variables lesson you learned how to name a box and put a value in it; this lesson is about choosing what kind of box — because the type you pick decides what range of values fits, whether negatives are allowed, and how arithmetic behaves.
C gives you a small set of built-in (also called primitive or fundamental) types. They fall into four families:
int, char, long.float and double._Bool / bool, holding true or false.void type — "no value at all."Why does this matter so early? Because in C, unlike many higher-level languages, you are responsible for matching the type to the data. Pick a type that is too small and your value silently wraps around or gets truncated. Pick a signed type where you meant unsigned and a comparison flips. These are not theoretical — they are among the most common sources of real bugs in C programs, from miscalculated array sizes to financial rounding errors. Getting comfortable with types now is what lets every later topic — arrays, pointers, structs, file I/O — make sense.
One theme runs through this whole lesson: the sizes of most integer types are platform-dependent. The C standard guarantees minimums, not exact sizes. When the exact width matters, you reach for <stdint.h>.
Choosing types is not academic housekeeping; it directly determines whether software is correct and safe.
uint8_t instead of int for a small flag can cut memory use 4x. Networking and file formats specify exact field widths, so you must use exact-width types to match a protocol on disk or on the wire.malloc size wrap to a tiny number, or a signed length treated as unsigned so a negative value becomes a gigantic buffer size. These lead to buffer overflows and remote code execution. Choosing and validating types defensively is a frontline defence.int is 32 bits or that char is signed breaks when moved to a different compiler or CPU. Fixed-width types make intent explicit and portable.Definition. A type is the combination of (a) a size in bytes and (b) a representation — the rule for turning those bytes into a value.
Plain language. The CPU only ever sees bits. The type is what tells the compiler "read these 4 bytes as a signed whole number" versus "read these 4 bytes as a floating-point number." Same memory, different meaning.
How it works internally. When you declare int x;, the compiler reserves sizeof(int) bytes (commonly 4) and records that reads and writes of x use the signed-integer interpretation. Arithmetic instructions are chosen accordingly.
The eight bits 1 1 0 0 1 0 0 0 in one byte:
interpreted as unsigned char -> 200
interpreted as signed char -> -56 (two's complement)
interpreted as a character -> 'È' in Latin-1
Same bits. The TYPE decides the meaning.
When to care: always, the moment you declare a variable. Pitfall: assuming the bit pattern alone has a meaning — it never does without a type.
Knowledge check: In your own words, why can the byte 11001000 be both 200 and -56?
Definition. Integer types hold whole numbers. The standard ones, smallest to largest, are char, short, int, long, long long. Each has an unsigned variant.
Signed vs unsigned. A signed type can hold negatives; one bit of range is effectively spent on the sign. An unsigned type holds only non-negatives but reaches a larger positive maximum for the same number of bits.
For an 8-bit type:
signed char range: -128 .. 127
unsigned char range: 0 .. 255
Same 8 bits, the interpretation shifts the window.
How sizes are decided. The C standard guarantees only minimums and an ordering: char >= 8 bits, short >= 16, int >= 16, long >= 32, long long >= 64, and sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long). On a typical 64-bit desktop: int is 4 bytes, long is 8 bytes (Linux/macOS) or 4 bytes (Windows). This is why "int is always 32 bits" is a myth.
When to use: int for ordinary counting and loop variables; a wider type when the value may exceed roughly +/-2 billion; an unsigned type when negatives are impossible and you have thought about wraparound. When NOT to: do not reach for unsigned just because a number "can't be negative" — subtracting past zero wraps to a huge value and causes bugs (see core concept 5). Pitfall: char's signedness is implementation-defined — it may be signed or unsigned. For small numbers use signed char or unsigned char explicitly.
Knowledge check: On a machine where int is 16 bits, what is the largest value a plain int can hold? (Predict before checking: it is 32,767.)
<stdint.h>Definition. Typedefs that guarantee an exact size: int8_t, int16_t, int32_t, int64_t and their unsigned partners uint8_t, uint16_t, uint32_t, uint64_t.
Plain language. When the answer to "how big is this?" must be the same on every machine — file formats, network packets, hardware registers — you use these instead of int/long.
Structure. They are just aliases the compiler maps to whatever native type has that width. uint32_t might be unsigned int on your machine, but the name documents your intent and stays correct elsewhere.
uint8_t one byte 0 .. 255
uint16_t two bytes 0 .. 65,535
uint32_t four bytes 0 .. 4,294,967,295
uint64_t eight bytes 0 .. ~1.8 x 10^19
When to use: protocol/file fields, memory-constrained data, anywhere a size assumption would be a bug. When NOT to: ordinary local loop counters — plain int (or size_t for sizes/indices) is idiomatic and fine. Pitfall: forgetting to #include <stdint.h>; the names will not be recognised.
Definition. Types for numbers with fractional parts: float (typically 32-bit), double (typically 64-bit), long double (larger, platform-dependent).
How it works internally. Floating-point numbers store a sign, an exponent, and a fraction (mantissa) in the IEEE-754 format — like scientific notation in binary. This buys huge range but means most decimals (even 0.1) cannot be stored exactly.
double layout (IEEE-754, 64 bits):
[ sign : 1 ][ exponent : 11 ][ fraction : 52 ]
Value = (-1)^sign x 1.fraction x 2^(exponent - 1023)
When to use: measurements, ratios, scientific math — use double by default (more precision than float at little cost). When NOT to: money or anything needing exact decimal results; use integer cents instead. Pitfall: comparing floats with ==. Because of rounding, 0.1 + 0.2 == 0.3 is false. Compare with a small tolerance.
Knowledge check (predict the output): Will printf("%d\n", 0.1 + 0.2 == 0.3); print 1 or 0? (It prints 0.)
Boolean. _Bool holds 0 or 1. After #include <stdbool.h> you write bool, true, false. Any nonzero value assigned to a bool becomes 1.
The conversion trap. When you mix a signed and an unsigned operand in one expression, C converts the signed value to unsigned first (this is part of the usual arithmetic conversions). A negative number becomes a very large positive number.
( -1 ) compared with ( 1U )
-1 as unsigned 32-bit -> 4,294,967,295
so -1 < 1U evaluates to FALSE
When to care: any comparison or arithmetic mixing signed and unsigned — extremely common with sizeof, strlen, and size_t, which are all unsigned. Pitfall: loops like for (int i = n - 1; i >= 0; i--) are fine, but for (size_t i = n - 1; i >= 0; i--) never ends, because an unsigned i is always >= 0.
void typeDefinition. void means "no value." A function returning void returns nothing; f(void) takes no parameters; void * is a generic pointer (covered in pointer lessons). You cannot declare a variable of type void.
Knowledge check: Find the bug: a programmer writes unsigned int gap = small - big; expecting a negative gap when small < big. What goes wrong, and what type should they use?
Declaring variables of each family, with the headers that unlock the friendly names:
#include <stdint.h> // fixed-width integers: int32_t, uint8_t, ...
#include <stdbool.h> // bool, true, false
int count = 42; // signed whole number, platform-sized
unsigned flags = 0u; // 'u' suffix => unsigned literal
long long big = 9000000000LL; // 'LL' suffix => long long literal
uint8_t red = 255; // exactly one byte, 0..255
int32_t delta = -1000; // exactly 32 bits, signed
float ratio_f = 1.5f; // 'f' suffix => float literal
double ratio_d = 1.5; // a plain decimal literal is a double
bool enabled = true; // needs <stdbool.h>
Key points: a plain decimal literal (1.5) is a double; add f for a float. Integer literals are int by default; suffixes u, L, LL widen or unsign them. Use sizeof(type) to see how many bytes a type takes on your machine.
C groups its built-in types into a few families.
Whole numbers, signed or unsigned:
char, short, int, long, long longunsigned variant (no negative values, larger positive range).Sizes are platform-dependent — they can differ from one machine or compiler to another.
When you need an exact width, use the fixed-width types from <stdint.h>, such as int32_t (exactly 32 bits) or uint64_t (exactly 64 bits, unsigned).
Numbers with a fractional part:
float — 32-bitdouble — 64-bitlong double — larger; the exact size varies by platform._Bool holds a true/false value.#include <stdbool.h>, you can write bool and use the names true and false.void typevoid means "no value." For example, a function that returns nothing has a return type of void.
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <limits.h> // INT_MAX and friends
int main(void) {
// 1) Sizes are platform-dependent: print them for THIS machine.
printf("sizeof(char) = %zu byte(s)\n", sizeof(char));
printf("sizeof(int) = %zu byte(s)\n", sizeof(int));
printf("sizeof(long) = %zu byte(s)\n", sizeof(long));
printf("sizeof(double) = %zu byte(s)\n", sizeof(double));
// 2) Fixed-width types guarantee an exact size everywhere.
uint8_t red = 255; // one byte, max value 255
uint32_t pixels = 1024u * 768u; // 786432, fits easily in 32 bits
printf("red = %u, pixels = %u\n", red, pixels);
// 3) Floating point: use double for fractional math.
double ratio = (double)pixels / 2.0; // cast keeps the division in double
printf("ratio = %.1f\n", ratio);
// 4) Boolean values.
bool enabled = true;
printf("enabled = %s\n", enabled ? "yes" : "no");
// 5) The classic signed/unsigned comparison trap.
int minus_one = -1;
unsigned one_u = 1u;
printf("(-1 < 1u) is %s\n", (minus_one < one_u) ? "true" : "false");
// 6) Overflow is undefined for signed ints: stay inside the range.
printf("INT_MAX = %d\n", INT_MAX); // largest int on this platform
return 0;
}
What it does. It prints the byte sizes of several types for the machine it runs on (proving sizes vary), then demonstrates fixed-width types, a floating-point division, a boolean, and the signed/unsigned comparison surprise.
Expected output (typical 64-bit Linux/macOS). Exact numbers may differ on other platforms — that is the whole point of lines under (1):
sizeof(char) = 1 byte(s)
sizeof(int) = 4 byte(s)
sizeof(long) = 8 byte(s)
sizeof(double) = 8 byte(s)
red = 255, pixels = 786432
ratio = 393216.0
enabled = yes
(-1 < 1u) is false
INT_MAX = 2147483647
Edge cases. On 64-bit Windows sizeof(long) prints 4, not 8. Assigning 256 to a uint8_t would wrap to 0. Writing pixels / 2 instead of pixels / 2.0 would do integer division and lose the fraction.
Walking the key example top to bottom:
| Step | Line | What happens |
|---|---|---|
| 1 | the four sizeof prints |
sizeof yields the byte count of a type as a size_t, printed with %zu. These reveal the real sizes on the current platform. |
| 2 | uint8_t red = 255; |
Reserves exactly one byte; 255 is its maximum, so it fits exactly. |
| 3 | uint32_t pixels = 1024u * 768u; |
Both literals are unsigned; the product 786,432 fits comfortably in 32 bits (max ~4.29 billion). |
| 4 | double ratio = (double)pixels / 2.0; |
pixels is cast to double, so the division is floating-point and keeps the fractional part. Result 393216.0. |
| 5 | bool enabled = true; |
true is 1 from <stdbool.h>; the ternary prints "yes". |
| 6 | (minus_one < one_u) |
Mixed signed/unsigned: -1 is converted to unsigned (4,294,967,295), which is not less than 1, so the comparison is false. |
| 7 | INT_MAX |
A macro from <limits.h> giving the largest int; 2147483647 where int is 32 bits. |
The most instructive moment is step 6: nothing looks wrong in the source, yet the result contradicts ordinary arithmetic intuition. Remembering why (the signed value is promoted to unsigned) is what protects you from this bug in real code.
1. Assuming a fixed size for int or long.
// WRONG: assumes int is 32 bits / 4 bytes
int header_field;
read_exactly_4_bytes(&header_field); // breaks where int is 2 or 8 bytes
Why it's wrong: the standard only guarantees minimums. Code that hard-codes a width breaks across compilers/CPUs.
// CORRECT: state the width you need
#include <stdint.h>
int32_t header_field;
Prevent it: use <stdint.h> whenever an exact size matters, and never assume a value from sizeof.
2. Mixing signed and unsigned in comparisons.
// WRONG: -1 becomes a huge unsigned value
if (-1 < 1u) { /* never runs */ }
Why: usual arithmetic conversions turn -1 into UINT_MAX. The condition is false.
// CORRECT: compare like with like
int a = -1, b = 1;
if (a < b) { /* runs */ }
Recognise it: compiler warnings such as "comparison between signed and unsigned" (-Wsign-compare). Turn warnings on.
3. Using float/double for money.
// WRONG: 0.10 + 0.20 != 0.30 exactly
double total = 0.10 + 0.20;
if (total == 0.30) { /* never true */ }
Why: most decimals are inexact in binary floating point.
// CORRECT: count in the smallest unit (cents) using integers
long total_cents = 10 + 20; // 30
if (total_cents == 30) { /* true */ }
4. Overflowing a small or signed type.
// WRONG: 256 does not fit in uint8_t; signed overflow is undefined behaviour
uint8_t b = 255; b = b + 1; // wraps to 0
int big = INT_MAX; big = big + 1; // undefined behaviour!
Fix: pick a type large enough for the worst case, and validate inputs before arithmetic that could overflow.
Compiler errors
unknown type name 'uint32_t' / 'bool' undeclared -> you forgot #include <stdint.h> or #include <stdbool.h>.format '%d' expects argument of type 'int', but argument has type 'long' -> use the right printf length modifier (%ld, %zu for size_t, %u for unsigned).Compiler warnings (enable them!) Compile with -Wall -Wextra. Watch for -Wsign-compare (signed/unsigned mix) and -Wconversion (a value may be truncated when stored in a smaller type). These flag the exact bugs in this lesson before they ever run.
Runtime / logic errors
unsigned i counting down past 0 — it wraps instead of going negative.5 / 2 is 2); make one operand a double (5 / 2.0).Questions to ask when it doesn't work
A quick printf("%zu\n", sizeof(x)) and printing intermediate values are the fastest tools for type bugs.
Although this is not a security lesson, type choices are a direct source of undefined behaviour and exploitable bugs in C:
INT_MAX + 1 is not guaranteed to wrap; the compiler may assume it never happens and optimise your checks away. Validate ranges before adding/multiplying.uint32_t n = 0; n - 1; becomes 4,294,967,295. When this value is used as a buffer size or array index, it leads to massive over-reads/writes. This is the root of many real CVEs (e.g. a length subtraction that underflows).int -> uint8_t, or size_t -> int) drops high bits. If a 4 GB length truncates to a small int, a later malloc allocates too little and the following write overflows.char signedness. Indexing a table with a plain char that may be negative can read before the array. Use unsigned char for byte values (e.g. with ctype.h functions).Defensive habits: compile with -Wall -Wextra -Wconversion; use size_t for sizes/indices; validate that lengths are non-negative and within bounds before using them in arithmetic; prefer fixed-width types for anything that crosses a trust boundary (files, network, user input).
Concrete uses.
uint16_t, uint32_t, etc., so the same bytes mean the same thing on every machine.uint8_t flags and int16_t sensor readings to fit; choosing int everywhere would waste scarce memory.uint8_t (0..255); coordinates and counts are 32-bit integers; ratios and transforms use double.Professional best practices.
int for ordinary counters and double for fractional math; include <stdbool.h> and use bool instead of 0/1 for flags; always initialise variables; turn on -Wall -Wextra.size_t for object sizes and array indices; use <stdint.h> exact-width types at I/O and protocol boundaries; never compare floats with ==; treat signed overflow as a bug, not a feature; prefer unsigned only when you have considered wraparound; document why a non-default type was chosen.Beginner 1 — Size report. Print sizeof for char, short, int, long, long long, float, double, and uint8_t/uint32_t. Requirement: use %zu for each. Hint: include <stdint.h>. Concepts: sizeof, integer and floating families. Expected: one line per type with its byte count on your machine.
Beginner 2 — Range checker. Declare a uint8_t and try to store the values 0, 200, 255, and 256 in turn, printing each result with %u. Requirement: observe and comment in code on what happens to 256. Hint: a uint8_t holds 0..255. Concepts: unsigned range, wraparound. Expected: 256 prints as 0.
Intermediate 1 — Pick the type. Given these quantities, declare a suitably typed variable for each and justify in a comment: (a) a person's age, (b) the number of bytes in a 10 GB file, (c) a temperature in Celsius with one decimal, (d) a single red colour channel. Constraint: no type may be unnecessarily large. Concepts: range analysis, fixed-width vs floating types. Hint: 10 GB exceeds a 32-bit range.
Intermediate 2 — Spot the comparison bug. Write a function int safe_less(int a, unsigned b) that returns 1 if a is truly less than b (treating a as a real integer, even when negative) and 0 otherwise. Requirement: handle a = -1, b = 1 correctly (must return 1). Hint: check the sign of a before comparing. Concepts: signed/unsigned conversion, defensive comparison.
Challenge — Safe addition. Write int add_checked(int a, int b, int *result) that stores a + b in *result and returns 1 on success, or 0 if the addition would overflow int (without ever performing the overflowing addition). Constraint: use INT_MAX/INT_MIN from <limits.h>; do not rely on undefined behaviour. Hint: if b > 0, overflow happens when a > INT_MAX - b. Concepts: signed overflow, range checking, output parameters. Expected: add_checked(INT_MAX, 1, &r) returns 0.
char, short, int, long, long long, signed/unsigned), floating-point (float, double, long double), boolean (_Bool/bool), and void.long double sizes are platform-dependent; the standard guarantees only minimums. Use <stdint.h> (int32_t, uint8_t, uint64_t, ...) when the exact width matters.<stdbool.h> to use bool, true, false. A plain decimal literal is a double; add f for float.int is 32 bits, mixing signed/unsigned (-1 < 1U is false), using floats for money, integer division losing fractions, and overflow/truncation.-Wall -Wextra, and validate ranges before arithmetic that could overflow.