Types, Variables, and Operators

In C, a type is a promise about how many bytes a value occupies and how to interpret them. There's no hidden bignum, no automatic string, no safety rail — just fixed-width integers, floating-point, and the bit patterns underneath. Understanding C's types means understanding sizes, signedness, and the surprising rules of integer promotion, because in C the difference between `int` and `unsigned` can be the difference between correct and catastrophically wrong.

The previous post compiled a program; now we look at the data it manipulates. C’s type system is small and low-level: it’s fundamentally about how many bytes a value takes and how those bytes are interpreted. This post covers the primitive types, their sizes and limits, the signed/unsigned distinction, and the integer-promotion rules that cause some of C’s most notorious bugs.

Types are about bytes

In a language like Python, an integer is an arbitrary-precision object. In C, an integer is a fixed number of bytes interpreted as a number. That’s the mental shift: a C type tells the compiler how much memory to reserve and how to read the bits. The primitive types:

A defining C quirk: the sizes are not fixed by the language. The standard guarantees only minimums and relative ordering (an int is at least 16 bits, a long at least 32, long long at least 64, and char ≤ short ≤ int ≤ long). On a typical modern platform int is 4 bytes and long is 8, but this varies. This is why portable C never assumes int is 4 bytes.

Knowing the exact size and limits

Because sizes vary, C gives you tools to ask. The sizeof operator returns a type’s size in bytes, and headers expose the limits:

#include <stdio.h>
#include <limits.h>   // INT_MAX, INT_MIN, etc.
#include <stdint.h>   // fixed-width types

int main(void) {
    printf("int is %zu bytes\n", sizeof(int));
    printf("int max is %d\n", INT_MAX);

    int32_t exactly_32_bits = 1000000;   // guaranteed 32 bits, everywhere
    printf("%d\n", exactly_32_bits);
    return 0;
}

When you need a guaranteed width, use the fixed-width types from <stdint.h>: int8_t, int16_t, int32_t, int64_t and their uint counterparts. Modern C code that cares about exact sizes (protocols, file formats, hardware registers) uses these instead of the platform-dependent built-ins. It’s a good default habit when precision matters.

Signed vs. unsigned: a real hazard

Every integer type is either signed (can hold negatives) or unsigned (only non-negatives, but roughly double the positive range). This isn’t a minor detail — it’s a source of serious bugs, because the two behave very differently at their boundaries and mixing them triggers surprising conversions.

The classic trap:

unsigned int u = 0;
if (u - 1 > 0) {           // TRUE — and probably not what you meant
    // u - 1 is not -1; it wraps to UINT_MAX (a huge positive number)
}

int  a = -1;
unsigned int b = 1;
if (a < b) { /* ... */ }   // a is converted to a huge UNSIGNED value; the
                           // comparison is likely FALSE, contradicting intuition

Unsigned arithmetic wraps around (modular arithmetic) rather than going negative — 0u - 1 is the largest unsigned value, not -1. And when you compare or combine a signed and an unsigned value, C converts the signed one to unsigned, so -1 becomes an enormous positive number. This is why size_t (an unsigned type returned by sizeof and used for sizes/indices) in a loop counting down past zero is a famous infinite-loop bug. The lesson: know the signedness of your values, avoid mixing signed and unsigned in comparisons, and enable -Wsign-compare (part of -Wextra) to be warned.

Integer promotion and conversion

C automatically converts between numeric types in expressions, following the usual arithmetic conversions — rules that are mostly invisible until they bite. The key ones:

These conversions are why C arithmetic sometimes produces baffling results, and why understanding your types’ sizes and signedness (the earlier sections) is not academic. The rules are consistent, but they’re not intuitive, and the compiler applies them silently.

Operators and the “everything is bits” view

C has the arithmetic (+ - * / %), comparison (== != < >), and logical (&& || !) operators you’d expect, plus a set that reveals C’s low-level nature: the bitwise operators. Because C exposes the actual bits, you can manipulate them directly:

unsigned int flags = 0;
flags |= (1u << 2);          // set bit 2
if (flags & (1u << 2)) { }   // test bit 2
flags &= ~(1u << 2);         // clear bit 2

This bit-twiddling — setting flags, packing data, masking — is everyday C, used in systems programming, protocols, and embedded work where you control individual bits of hardware registers. It’s a direct expression of C’s core idea: values are bit patterns, and you can operate on them at that level. A couple of caveats that connect to earlier sections: shifting a signed negative value or shifting by more than the type’s width is undefined behavior (post 8), which is one more reason to use unsigned types for bit manipulation.

Key takeaways

Further reading

Sources & References

Conversions, promotion, and operators