Data Types & Conversions
Primitive Types
There are two families: integer types (whole numbers, including char) and floating-point types (approximate real numbers). The table shows typical 64-bit platform values — the standard only guarantees minimum ranges, so always think in terms of limits.h.
| Type | Typical size | Typical range | printf specifier |
|---|---|---|---|
char | 1 byte | -128 .. 127 (or 0..255) | %c / %d |
short | 2 bytes | -32,768 .. 32,767 | %hd |
int | 4 bytes | -2,147,483,648 .. 2,147,483,647 | %d |
long | 8 bytes (Windows 4) | ±9×1018 (Windows ±2×109) | %ld |
long long | 8 bytes | ±9×1018 | %lld |
float | 4 bytes | ~±3.4×1038, ~7 digits | %f |
double | 8 bytes | ~±1.8×10308, ~15 digits | %f / %g |
Note the trap in the table: long is 8 bytes on Linux/macOS but 4 on Windows. Code that assumes a size is not portable. If you need an exact width, use the fixed-width types below.
Fixed-Width Integers
For precise control — the C way — include <stdint.h> and use types that state their width in the name: int8_t, uint16_t, int32_t, int64_t, and friends. These are typedef aliases selected by the compiler for the current platform, so they are portable by construction.
#include <stdint.h> // exact-width integer types
#include <inttypes.h> // PRId64 macro for printing fixed-width types
int main(void) {
uint32_t port_count = 65535; // exactly 32 bits, unsigned
int64_t big_total = 1000000000000LL; // exactly 64 bits
// PRId64 expands to the right format specifier ("ld" or "lld")
printf("port=%" PRIu32 " total=%" PRId64 "\n", port_count, big_total);
return 0;
}
Use size_t (from <stddef.h>) for array sizes and lengths: it is the unsigned type that fits any object size on the platform.
Booleans
C has no dedicated boolean keyword; _Bool is the C11 spelling, and <stdbool.h> provides bool, true, and false as conveniences. Any nonzero integer value is truthy — if (x) is identical to if (x != 0) — which is exactly why if (x = 5) (assignment, always true) is so dangerous next to if (x == 5).
#include <stdbool.h> // bool, true, false
int main(void) {
bool ready = true; // stores 0 or 1 internally
if (ready) {
// taken because ready is true (nonzero)
}
return 0;
}
Floating-Point Pitfalls
Floats approximate real numbers in binary, so many decimals cannot be represented exactly — compare with a tolerance, never with ==. Prefer double unless memory matters: float's 7 digits of precision silently corrupts accumulated values.
#include <math.h> // fabs()
#include <stdio.h>
int main(void) {
double a = 0.1 + 0.2; // in binary this is 0.30000000000000004...
if (a == 0.3) { // false! comparing floats with == is a bug
printf("equal\n");
}
// correct: compare the distance with a tolerance
if (fabs(a - 0.3) < 1e-12) {
printf("close enough\n");
}
return 0;
}
Qualifiers: const, volatile, signed/unsigned
Type qualifiers adjust how a type may be used. const promises the object will not be modified (the compiler rejects writes). volatile tells the compiler a value may change outside the program — memory-mapped hardware registers and signal handlers. signed/unsigned pick the interpretation of integer bits: unsigned doubles the positive range but wraps on overflow instead of producing undefined behavior.
const double TAX_RATE = 0.19; // read-only; TAX_RATE = 0.2 would not compile
volatile int clock_ticks = 0; // may be updated by hardware or a signal
unsigned int max_bytes = 4000000000U; // fits 4 billion without sign bit
Signed overflow is undefined behavior; unsigned overflow is defined (wraps modulo 2N). This single line in the standard is why security-critical counters are usually unsigned.
Type Conversions
When an operator combines values of different types, C applies implicit conversion: small integers are promoted to int (integer promotion), then both operands are converted to the "usual arithmetic conversion" type — ints become floats, floats become double. Promotions are safe; the danger is narrowing, where a larger value is squeezed into a smaller type and silently loses bits.
Explicit Casts
A cast (type)expr forces a conversion. Use it when the intent must be explicit — and especially when mixing integer division with floating results: 7/2 is 3 in C (integer division truncates), so you need (double)7/2 to get 3.5.
#include <stdio.h>
int main(void) {
int numerator = 7, denominator = 2;
printf("int: %d\n", numerator / denominator); // 3 (truncated!)
printf("float: %.1f\n", (double)numerator / denominator); // 3.5
// narrowing cast: compiler warns, bits are dropped silently
double big = 3.99;
int truncated = (int)big; // 3 — fractional part discarded
printf("truncated: %d\n", truncated);
return 0;
}
-Wconversion so the compiler shows you every place a value could silently change meaning. Explicit casts make your intention readable and reviewable.
Next: arrays, structs, and unions — how C groups data.