---
title: 'Data Types'
source: 'https://academia.sh/en/courses/c-fundamentals/data-types'
course: 'C Fundamentals'
language: en
updated: '2026-09-26T03:02:41+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Measures and shows that a type's value range is decided at the compiler stage"
    - 'Distinguishes integer promotion from the signed-unsigned conversion with an example'
    - "Separates unsigned overflow's defined wraparound from signed overflow's undefined behavior"
---

# Data Types

A type's value range, the signed–unsigned distinction and integer promotion being decided at the compiler stage; no size is printed except sizeof(char) == 1.

The Compiler Warnings and Standards lesson measured at which stage a warning arises and
what the standard does and does not guarantee: `sizeof(char) == 1` was a guarantee, other
sizes were not. This lesson builds on that distinction and measures a more fundamental
question: at which stage does a type determine the boundary of the values it can carry?

The answer is the compiler. When `int` is written in a source file, the range that name
can carry is fixed neither in the source itself nor at runtime; it is decided the moment
the unit is translated, together with the representation chosen for the target. **This
lesson is not confused with the question measured in M08/K05, "which class path a type
must be found on," nor with the question measured in M09/K05, "the source of a piece of
information"** — both were axes outside this course; what is measured here is at which
stage the value a type carries is decided, and where the boundaries of that decision come
from. The Programming Fundamentals course (M01/K02) already established the concepts of
variable and type with Python; there, the values a name could hold could change during
runtime, and a type's boundary was never asked separately. C separates these two
questions: the type itself carries a boundary, and that boundary is fixed at compile time
together with the type name written in the source. Once a variable is declared, the type
it carries never changes for the rest of the program; the value may change, but the type
that determines which range and which rule the value is interpreted by stays fixed. This
lesson measures what that fixedness means through four separate decisions — range,
promotion, sign and explicit conversion.

The size of this boundary varies by target, and this variability is deliberate: the
language lets the same type name be represented with different widths on different
hardware, letting an efficient mapping be built for each target. But this flexibility
should not make a lesson's measurements dependent on that target — if a measurement is
true on the machine it runs on here, it must stay true on another target five years from
now. For this reason, no real size value is printed in this lesson; instead, the relative
relationship between two types is measured. A relationship itself can also vary from
target to target, but for a given target the compiler's decision is fixed, and the same
binary reports the same relationship on every run.

## Size Unit and Comparative Result

The size of the range a type carries depends on the target and is never printed anywhere
in this course — the exception is the one line the standard explicitly guarantees:
`sizeof(char)` is always `1`, because the "byte" unit of measure is already defined
through `char`. The measurement below reads this line and the relative size of two types
as a logical outcome — larger or smaller, `1`/`0`; no real size value is printed.

```c
// stage.c — the type carrying a value being decided at the compiler stage
#include <stdio.h>
#include <limits.h>

enum Color { RED = 1, GREEN = 2, BLUE = 4 };

static void observe(const char *name, int value) {
    printf("%-40s%d\n", name, value);
}

int main(void) {
    observe("type size unit", (int)sizeof(char));
    observe("enumeration constant", (int)BLUE);
    observe("is int wider than char", (int)(sizeof(int) > sizeof(char)));
    observe("is long wider than int", (int)(sizeof(long) > sizeof(int)));
    observe("is short narrower than int", (int)(sizeof(short) < sizeof(int)));

    char small_a = 100, small_b = 100;
    int promoted_sum = small_a + small_b;
    observe("promoted sum", promoted_sum);

    unsigned char wrapped = UCHAR_MAX;
    wrapped = (unsigned char)(wrapped + 1);
    observe("unsigned wraparound result", wrapped);

    int negative_one = -1;
    unsigned int threshold = 1;
    observe("is it less after converting to unsigned", (int)((unsigned int)negative_one < threshold));

    return 0;
}
```

```
type size unit                          1
enumeration constant                    4
is int wider than char                  1
is long wider than int                  1
is short narrower than int              1
promoted sum                            200
unsigned wraparound result              0
is it less after converting to unsigned 0
```

The type size unit line confirms the standard's guarantee: `char` is always a single
byte, and this will remain the only size value this course measures. The remaining three
comparison lines do not give a real number at all, only the result of a relationship —
they ask whether `int` is wider than `char` on the target, whether `long` is wider than
`int`, whether `short` is narrower than `int`, and all three come back with the answer `1`
(true). **BS1.** These comparisons themselves are also decided at the compiler stage: each
relationship can vary from target to target, but it is fixed while a given unit is being
translated and does not change during a run — the same binary produces the same `1` or
`0` value on every run.

The standard also already guarantees the relative order of these types: the width
between `char`, `short`, `int`, `long` and `long long` can never run backward — each one
is wide enough to carry every value the one before it could carry. This is not a
measurement but the language's own specification; this lesson only measures whether this
order turns out **strict** or **equal** on a particular target (here all three came out
strict). On some targets `int` and `long` may be the same width — the standard only
guarantees that `long` is **not narrower** than `int`, not that it is definitely
**wider**. The value `1` measured in this lesson is therefore an observation specific to
this target, not a promise the language itself makes.

## Integer Promotion

The enumeration constant line is a slice of the `enum Color` constant introduced in the
Compilation Stages lesson: the value of `BLUE`, `4`, is produced at compile time from the
constant expression written in the source. In the same measurement, the promoted sum line
shows a different decision: `small_a` and `small_b` are each a `char`, but their sum is
computed not within `char`'s bound but within `int`'s. The C standard promotes narrow
types like `char` and `short` to `int` before they enter an operation — this is called
integer promotion. If `100 + 100` were carried out on a `char` (typically in the range
`-128`–`127`), the result would not fit; but because the operation runs at `int` width,
`200` comes out without any trouble. This promotion is invisible in the source — no
conversion was written — but the compiler moves every `char` operand to `int` before the
operation, and this decision comes entirely from the types, not from the data.

**BS2.** The promotion rule applies regardless of what the operand values actually are:
even if `small_a` and `small_b` were `10` and `20`, the sum would still be computed at
`int` width, even though the result would stay within `char`'s bound. Before running the
operation, the compiler decides which width it will run at by looking only at the
operands' **types**; this is the first instance of the principle that operators behave
according to type, not data, and it will be developed further in the Operators lesson.

Promotion is not limited to arithmetic operators; when a function takes a variable number
of arguments (`printf` is the standard library's example of this), `char` and `short`
arguments are also promoted to `int` before the call. This is called the default argument
promotion, and it works only for arguments falling into the `...` part — after the
prototype's last declared parameter. `observe` is different: it is a function with a
prototype and a fixed signature, so even though the `wrapped` variable is `unsigned
char`, its value is carried to `int` by an ordinary assignment conversion because the
second parameter is declared `int`; the `%d` in `observe`'s body matches this declared
type as well. The two mechanisms are separate: one works only at the tail of
variable-argument calls, the other works on every prototyped call. Printing a narrow type
with a directly mismatched format specifier (using `%d` instead of `%hhu` for `unsigned
char`, for instance) works on `printf`'s side in most cases thanks to default promotion,
but this is a fragile match, and the subject of format strings will be taken up
separately in the Input and Output lesson.

## Narrowing with an Explicit Conversion

Promotion is an implicit rule — it works without leaving any trace in the source. But a
source writer may also want the exact opposite: to deliberately fit a wide value into a
narrow type. This is done with an explicit conversion (cast), and the decision itself is
again made at compile time, at the point where the type's name is written.

```c
// narrowing.c — an explicit conversion makes the narrowing visible in the source
#include <stdio.h>

static void observe(const char *name, int value) {
    printf("%-34s%d\n", name, value);
}

int main(void) {
    int wide_value = 300;
    unsigned char narrowed = (unsigned char)wide_value;
    observe("explicitly narrowed value", narrowed);

    int second_value = 600;
    unsigned char second_narrowed = (unsigned char)second_value;
    observe("second explicitly narrowed value", second_narrowed);
    return 0;
}
```

```
explicitly narrowed value         44
second explicitly narrowed value  88
```

The value `300` does not fit into an eight-bit unsigned type, but the conversion is still
defined: the same wraparound rule applies here too, giving `300 mod 256 = 44`. The same
rule applies for `600`, giving `88`. This is not a different event from the overflow in
the previous section, but the same rule triggered explicitly and deliberately in the
source. Implicit promotion was invisible in the source; an explicit conversion is visible
through the `(unsigned char)` notation — a reader can tell from the source that a
narrowing is intentional. In both cases, which rule applies is determined at compile time
by looking at the target type's sign, not the operand's value.

## The Signed–Unsigned Distinction and Defined Wraparound

Whether a type is signed or unsigned determines where the range it carries starts, and
this too is a property decided at compile time. The unsigned wraparound result line
measures a variable of type `unsigned char` being incremented once after it reaches its
upper bound (`UCHAR_MAX`): the result is `0`. This is not an error — the standard makes
overflow **defined** for unsigned types: the result "wraps" by taking the modulus against
the number of values the type can represent. `255 + 1`, on an eight-bit unsigned type,
gives `256 mod 256 = 0`, and this produces the same result on every run.

The is it less after converting to unsigned line shows another face of the
signed–unsigned distinction: a signed integer with the value `-1` is converted to
unsigned before being compared with an unsigned type. This conversion is also defined —
when a negative value is converted to unsigned, the number of values the type can
represent is added, moving it to a large unsigned number. As a result, `-1` appears not
smaller than the `1` in the `threshold` variable, but far larger, and the comparison
returns `0` (false). The signs written in the source are reconciled this way by the
compiler during comparison; which side gets converted to which type is determined by
looking at the two operands' declared types, not the data.

This reconciliation rule also explains why unsigned types are so often preferred for
carrying counters and sizes: the standard library's `sizeof` operator returns `size_t`,
and this type is unsigned because a negative size is conceptually meaningless. But the
same choice also carries a trap. Writing a loop bound backward with an unsigned counter —
trying to bring a counter down below `0` — never drops the counter into the negatives,
because it is unsigned; it wraps from the upper bound instead, and the loop condition
stays true forever. This situation is not undefined; the wraparound rule works exactly as
seen in the measurement above. But because it would lead to an infinite loop, this lesson
does not run this scenario, and only notes how unsigned wraparound can turn into an
**unexpected** result at a boundary condition — the behavior itself is defined,
interpreting the result is the source writer's job.

## The Bounding Measurement

Overflow behavior depends on a type's sign, and this dependency is not symmetric. For
unsigned types, overflow is defined as measured above, and wraparound always follows the
same rule. For a signed type, overflow is **undefined behavior**: incrementing an `int`
at the value `INT_MAX` drops into a state the standard gives no guarantee about — because
the compiler does not assume this situation can occur, optimizations may produce
unexpected results, the program may crash, or there may be no visible effect at all.
Under this course's general rule, this behavior is not tried in a code block.

The defined counterpart set alongside it is the `unsigned char` measurement above: the
same "crossing one past the upper bound" scenario, built with an unsigned type, gives a
fully predictable result. Both scenarios ask the same question — what happens when a
type's boundary is crossed — but the answer depends solely on the sign itself: an
arithmetic rule for unsigned, a gap for signed. Choosing a type as signed or unsigned is
therefore a decision that determines not only which values can be represented, but also
which category the program falls into when the boundary is crossed, and this decision is
made entirely at compile time, together with the type name written in the source.

The source of this asymmetry lies in how the two representations correspond in hardware.
An unsigned type reads its bits directly as a number's binary representation; wraparound
is a natural consequence of that representation's limited width, and it works the same
way on every target. In a signed type, the representation of negative values can be
target-dependent, and guaranteeing which result would arise on overflow for every
representation would have restricted certain optimizations the compiler could make for
values near the upper bound. For this reason, the language chose to leave signed overflow
undefined rather than defined — instead of fixing the behavior, it granted the compiler
the freedom to work under the assumption that "this will never happen."

The practical consequence of this asymmetry is that choosing between a signed and an
unsigned type requires asking not only "which values are needed" but also "what should
happen if the boundary is crossed." If a counter will never be negative, an unsigned type
both wins an extra bit of range at the upper bound and reduces overflow to a defined
wraparound. If a computation can temporarily drop negative in intermediate steps, a
signed type carries that transition naturally, but sits closer to undefined behavior in a
sum approaching the upper bound. The compiler does not make this choice in place of the
source writer; it only reads the type name written and fixes the boundary that name
carries and the rule for crossing it.

## Summary

- A type's value range is decided at the **compiler** stage; in this course no size value
  is printed except `sizeof(char) == 1`, only comparison results are measured.
- Integer promotion carries narrow types like `char` and `short` to `int` before an
  operation; this decision is made by looking at the operand **types**, not the data
  being carried.
- Overflow in unsigned types is **defined** and wraps according to the type's number of
  values; overflow in signed types is **undefined** and is never run in any code block in
  this course.
- Converting a signed value to unsigned is also defined: a negative value is moved to a
  large unsigned number by adding the type's number of values.
- Enumeration constants (like `BLUE` inside `enum Color`) are produced at compile time
  from constant expressions in the source; this subject will be developed further in the
  Structs, Unions and Enumerations lesson.
- An explicit conversion (like `(unsigned char)`), unlike implicit promotion, is visible
  in the source and is subject to the same wraparound rule; which rule applies is again
  determined at compile time by looking at the target type's sign.

## Next Step

Every decision measured in this lesson stayed within the boundary of a single unit: a
type's range, an operation's width, a conversion's direction — all decisions the compiler
alone could make by looking at the same file. The types measured in this lesson were
already bound to existing names; the next question concerns where that name itself comes
from and how long it lives. Which declaration a name is bound to and how long a value
lives are not always decided at the same stage. The Variables, Scope and Storage Duration
lesson separates these two: where a name is visible from is fixed at compile time, while
how long a value lives can only be observed at runtime — and the difference between the
two will also raise a third stage, one in which the same name can fall into two separate
objects in two separate units.
