Skip to content

The C Type System and Conversions

C conversions are concise enough to disappear inside an expression. The compiler may change widths and signedness before an operator runs, so reasoning from the declared type of only the destination is too late.

Integer types describe minimum relationships

C provides signed and unsigned forms of char, short, int, long, and long long. Their exact widths are implementation-defined within required minimum ranges and ordering. sizeof(char) is always one byte, but a C byte need not be eight bits; CHAR_BIT in <limits.h> tells you.

Use <stdint.h> when an exact width is part of a format or protocol:

c
#include <stdint.h>

uint32_t sequence;
int64_t timestamp;

Exact-width typedefs exist only when the implementation has a matching representation. For array sizes and object sizes, use size_t; for pointer differences, use ptrdiff_t.

Promotions happen before ordinary arithmetic

Types narrower than int usually promote to int, or to unsigned int when necessary:

c
#include <stdio.h>

int main(void) {
    unsigned char a = 200;
    unsigned char b = 100;
    printf("%d\n", a + b); /* arithmetic occurs as int */
    return 0;
}

The result is not forced back to unsigned char unless assigned there. Narrowing conversion then applies modulo rules for unsigned destinations; conversions to signed types outside their range are implementation-defined or may raise an implementation-defined signal.

Mixed signedness can reverse intuition

The usual arithmetic conversions seek a common type. If a signed value meets an unsigned type of sufficient rank, the signed value may become unsigned:

c
#include <stdio.h>

int main(void) {
    int debt = -1;
    unsigned int balance = 1;
    printf("%s\n", debt < balance ? "less" : "not less");
    return 0;
}

On ordinary implementations this prints not less because -1 converts to a large unsigned value. Strong warning settings diagnose many such comparisons. Better still, model quantities with compatible domains and validate conversions at boundaries.

Signed and unsigned overflow differ

Unsigned arithmetic wraps modulo one more than the maximum representable value. Signed overflow is undefined behavior. This does not make unsigned arithmetic a universal safety solution: wraparound is often a logic or allocation-size vulnerability even when its semantics are defined.

Check before performing an operation, or use standardized checked-arithmetic facilities where the selected C version and implementation provide them.

Floating types trade range and precision

float, double, and long double have implementation-defined representations satisfying standard constraints. Most mainstream systems use IEC 60559/IEEE 754 formats for float and double, but portable code should check applicable macros when exact behavior matters.

Decimal literals without a suffix have type double; 1.0f is float; 1.0L is long double. Floating comparison and conversion deserve domain-specific tolerances and range checks, not a universal epsilon recipe.

Qualifiers describe permitted access

const prevents modification through a particular lvalue:

c
void print_values(const int *values, size_t count);

It does not prove the underlying object is immutable through all aliases. volatile tells the implementation that accesses are observable in special ways; it does not make operations atomic and is not a thread-synchronization primitive. _Atomic and <stdatomic.h> serve that separate responsibility in C11 and later.

restrict, introduced in C99, is an optimization-relevant promise about pointer-based access. Violating it creates undefined behavior. Use it only when an API's aliasing contract is genuinely enforceable.

Casts are claims, not validation

c
double ratio = (double)completed / total;

This cast deliberately selects floating division. A pointer cast, in contrast, cannot create correct alignment, lifetime, object representation, or ownership. Converting away const does not make an originally const object writable.

Avoid casting the result of malloc in C:

c
int *values = malloc(count * sizeof *values);

void * converts to object-pointer types in C. The unneeded cast can hide a missing declaration for malloc in poorly diagnosed code and duplicates type information.

Formatting is type-sensitive

Variadic functions such as printf do not receive enough type metadata to repair a mismatched format. Use %zu for size_t, <inttypes.h> macros for fixed-width integers, and a matching conversion for every argument.

c
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

int main(void) {
    uint64_t id = UINT64_C(42);
    printf("id=%" PRIu64 "\n", id);
    return 0;
}

Optional prompts

Explain: Why can comparing -1 with an unsigned count produce a surprising result?

Answer: The usual arithmetic conversions may convert -1 to the unsigned type before comparison, producing a large value rather than preserving mathematical signed ordering.

Explain: Does casting a misaligned byte pointer to int * make dereferencing it valid?

Answer: No. A cast changes the expression's type; it does not establish alignment, lifetime, representation, or aliasing requirements.

Further reference