Data Types & Conversions

C's type system is small but strict. Every object has a type chosen at declaration; the compiler uses it to size variables and to decide what operators mean. This page covers the primitive types, how to pick the right one for a job, and the conversion rules — where most subtle C bugs are born.

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.

TypeTypical sizeTypical rangeprintf specifier
char1 byte-128 .. 127 (or 0..255)%c / %d
short2 bytes-32,768 .. 32,767%hd
int4 bytes-2,147,483,648 .. 2,147,483,647%d
long8 bytes (Windows 4)±9×1018 (Windows ±2×109)%ld
long long8 bytes±9×1018%lld
float4 bytes~±3.4×1038, ~7 digits%f
double8 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;
}
Rule of thumb: never rely on implicit narrowing; enable -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.