Basic Types and Values

This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

C’s types describe two things at once: the values a program computes with, and the representation those values have in memory. Most of C’s reputation for sharp edges comes from places where those two views disagree — signed overflow, narrowing conversions, and the promotion rules below.

The Abstract State Machine

The standard does not describe your CPU. It describes an abstract machine whose observable behavior — I/O, volatile accesses, and the state at program exit — an implementation must reproduce. Everything else (register allocation, instruction order, whether an addition happens at all) is free.

Three consequences run through every page in this section:

  • Types have values and representations. int means "an integer in at least the range ±32767"; that it is usually 32 bits of two’s complement is a property of the implementation, though C23 finally requires two’s complement for signed integers.

  • Some operations have no defined result. Signed overflow, reading an uninitialized object, dereferencing a null pointer — these are undefined behavior, and the optimizer is entitled to assume they never happen. See Error Handling and Program Failure.

  • sizeof and <limits.h> are the portable way to ask, never a guess.

bool

In C23 bool, true and false are keywords; <stdbool.h> still exists and is now redundant. bool converts any scalar to 0 or 1 — which makes it the only integer type that never truncates surprisingly:

#include <stdio.h>

int main(void)
{
    bool flag = true;
    bool from_int = 256;            // any nonzero value becomes true (1), not 0
    bool from_ptr = "text";         // any non-null pointer becomes true

    printf("%d %d %d %zu\n", (int)flag, (int)from_int, (int)from_ptr, sizeof(bool));
    return 0;
}

sizeof(bool) is 1 on every mainstream implementation but is not required to be. Before C23, write _Bool or include <stdbool.h>.

Character Types

There are three distinct character types, and the plain one has implementation-defined signedness:

Type Signedness Use it for

char

Implementation-defined (signed on x86 Linux, unsigned on ARM Linux)

Text, and only text.

signed char

Signed, at least -127..127

Small signed integers.

unsigned char

Unsigned, 0..UCHAR_MAX

Raw bytes — the object-representation type.

#include <ctype.h>
#include <limits.h>
#include <stdio.h>

int main(void)
{
    char c = 'A';
    unsigned char byte = 0xFF;

    // <ctype.h> takes an int that must be representable as unsigned char (or EOF):
    // cast through unsigned char, or a negative plain char is undefined behavior.
    int upper = toupper((unsigned char)c);

    printf("CHAR_BIT=%d  c=%c  upper=%c  byte=%u  char is %s\n",
           CHAR_BIT, c, upper, byte, (char)-1 < 0 ? "signed" : "unsigned");
    return 0;
}

CHAR_BIT is at least 8 and is 8 everywhere that matters. A char is by definition one byte, so sizeof(char) == 1 always.

Signed and Unsigned Integers

The standard specifies minimum ranges and a rank ordering, not exact widths:

Type Minimum width Typical (LP64) Range macro

signed char

8 bits

8 bits

SCHAR_MIN/SCHAR_MAX

short

16 bits

16 bits

SHRT_MIN/SHRT_MAX

int

16 bits

32 bits

INT_MIN/INT_MAX

long

32 bits

64 bits (32 on Windows)

LONG_MIN/LONG_MAX

long long

64 bits

64 bits

LLONG_MIN/LLONG_MAX

Each has an unsigned counterpart with the same width and range 0..2^N-1. The two behave fundamentally differently on overflow:

#include <limits.h>
#include <stdio.h>

int main(void)
{
    unsigned int u = UINT_MAX;
    u += 1;                     // defined: wraps modulo 2^N, u == 0

    int i = INT_MAX;
    // i += 1;                  // UNDEFINED BEHAVIOR -- not "wraps to INT_MIN"
    (void)i;

    printf("wrapped unsigned = %u\n", u);
    return 0;
}

Signed overflow is undefined, which is why -fsanitize=undefined exists and why if (x + 1 < x) cannot be used to detect it — the compiler folds it to false. Use <stdckdint.h> (see Numbers and Math) or check before the operation.

Fixed-Width Integers — <stdint.h>

When the width matters — file formats, wire protocols, hardware registers — name it:

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

int main(void)
{
    int32_t exact = -2147483648;        // exactly 32 bits, or the type does not exist
    uint_least16_t at_least = 65535;    // smallest type with >= 16 bits
    uint_fast8_t fastest = 255;         // fastest type with >= 8 bits
    intmax_t widest = INTMAX_MAX;       // widest signed integer type
    size_t count = 3;                   // result of sizeof; unsigned
    ptrdiff_t delta = -1;               // pointer difference; signed
    uintptr_t as_int = (uintptr_t)&count;   // an integer wide enough to hold a pointer

    printf("%" PRId32 " %u %u %" PRIdMAX " %zu %td %" PRIuPTR "\n",
           exact, (unsigned)at_least, (unsigned)fastest, widest, count, delta, as_int);
    return 0;
}
  • intN_t/uintN_t are optional — but int8_tint64_t exist on every ordinary platform.

  • intptr_t/uintptr_t are the only correct way to hold a pointer in an integer.

  • Print them with the <inttypes.h> macros (PRId32, PRIu64, …); %d for an int32_t is wrong on a platform where it is a long.

  • size_t for sizes and indices, ptrdiff_t for differences: %zu and %td.

Bit-Precise Integers — _BitInt(N) (C23)

C23 adds integers of an exact bit width, including widths no hardware has:

#include <limits.h>
#include <stdio.h>

int main(void)
{
    _BitInt(12) small = 2047;               // exactly 12 bits, signed
    unsigned _BitInt(3) tiny = 7;           // exactly 3 bits, unsigned
    _BitInt(128) huge = 170141183460469231731687303715884105727wb;   // wb suffix

    printf("%d %u %d\n", (int)small, (unsigned)tiny, (int)(huge >> 120));
    printf("BITINT_MAXWIDTH = %d\n", (int)BITINT_MAXWIDTH);
    return 0;
}

Two rules make _BitInt different from everything else: it does not participate in the integer promotions (a _BitInt(3) stays 3 bits in arithmetic), and it has no default printf conversion — cast it to print it. Use it for bitfield-heavy protocol code and fixed-point maths, not as a general integer.

Floating-Point Types

Type Typical Notes

float

IEEE-754 binary32

~7 decimal digits. printf promotes it to double, so %f is correct for both.

double

IEEE-754 binary64

~15 decimal digits; the default for floating-point constants and maths functions.

long double

80-bit x87 on x86 Linux, = double on ARM/MSVC

Print with %Lf. Its width is the least portable thing in C.

C23 optionally adds _Float16, _Float32, _Float64, _Float128 and the decimal types _Decimal32/64/128, each guarded by a feature macro. <float.h> describes what you actually have:

#include <float.h>
#include <math.h>
#include <stdio.h>

int main(void)
{
    printf("double: %d decimal digits, epsilon %g, max %g\n", DBL_DIG, DBL_EPSILON, DBL_MAX);

    double a = 0.1 + 0.2;
    printf("0.1 + 0.2 == 0.3 ? %s\n", a == 0.3 ? "yes" : "no");          // no
    printf("close enough ? %s\n", fabs(a - 0.3) < 8 * DBL_EPSILON ? "yes" : "no");

    printf("nan is unordered: %s\n", (NAN == NAN) ? "equal" : "never equal");
    printf("isnan says %d, isinf says %d\n", isnan(NAN), isinf(1.0 / 0.0));
    return 0;
}

Constant Types

The type of a constant is not always the obvious one, and it is decided before any assignment:

Constant Type

42

int (or the first of int, long, long long that fits)

42u, 42U

unsigned int, then unsigned long, …

42l, 42LL, 42ull

long, long long, unsigned long long

0x2A

int, but an octal or hex constant may become unsigned if it does not fit a signed type

3.14

double

3.14f / 3.14L

float / long double

'A'

int — not char (this is why sizeof('A') == sizeof(int))

L’A' / u’A' / U’A'

wchar_t / char16_t / char32_t

"text"

char[5] — an array, which decays to char ; *not writable

nullptr

nullptr_t (C23)

#include <stdio.h>

int main(void)
{
    printf("sizeof('A') = %zu, sizeof(char) = %zu\n", sizeof('A'), sizeof(char));
    printf("sizeof(\"text\") = %zu\n", sizeof("text"));     // 5 -- includes the NUL
    printf("1 / 2 = %d but 1 / 2.0 = %g\n", 1 / 2, 1 / 2.0);
    return 0;
}

That third line is the single most common C surprise: 1 / 2 is integer division because both operands are integers, and no context (double x = 1 / 2;) changes it.

Implicit Conversions

Conversions happen at assignment, in function arguments, in return, and around every binary operator. Three rule sets, in order of how often they bite:

The Integer Promotions

Any type narrower than int — bool, char, short, bit-fields, enums — is converted to int (or unsigned int if int cannot hold every value) before arithmetic. There is no char arithmetic in C.

The Usual Arithmetic Conversions

For a binary operator whose operands (after promotion) still differ, both convert to the higher type: floating beats integer, wider rank beats narrower, and at equal rank unsigned beats signed.

Integer promotion and conversion ladder: bool

That last clause is the trap:

#include <stdio.h>
#include <string.h>

int main(void)
{
    if (-1 < 1u) {
        puts("mathematics");
    } else {
        puts("C: -1 converts to UINT_MAX");        // this branch runs
    }

    // The same bug, in the form it actually appears in:
    const char *s = "abc";
    int i = -1;
    if (i < (int)strlen(s)) {           // cast needed: strlen returns size_t (unsigned)
        puts("in range");
    }
    return 0;
}

Compile with -Wsign-compare (included in -Wextra) and never mix signed and unsigned in a comparison.

Narrowing Conversions

Assigning a wider value to a narrower type discards information:

  • To a narrower unsigned type: well-defined, keeps the low bits (modulo 2^N).

  • To a narrower signed type: the result is implementation-defined (in practice, the low bits) if the value does not fit — not undefined, but not portable either.

  • Floating to integer: truncates toward zero; undefined if the truncated value does not fit.

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

int main(void)
{
    uint8_t small = (uint8_t)300;       // 300 % 256 == 44, well-defined
    int truncated = (int)3.99;          // 3, toward zero
    int negative = (int)-3.99;          // -3, toward zero, not -4

    printf("%u %d %d\n", small, truncated, negative);
    return 0;
}

Make every narrowing conversion an explicit cast: it documents the intent and silences -Wconversion.

sizeof and alignof

#include <stdalign.h>
#include <stddef.h>
#include <stdio.h>

int main(void)
{
    int numbers[10];

    printf("sizeof(int)      = %zu\n", sizeof(int));            // parentheses for a type
    printf("sizeof numbers   = %zu\n", sizeof numbers);         // no parentheses for an object
    printf("element count    = %zu\n", sizeof numbers / sizeof numbers[0]);
    printf("alignof(max_align_t) = %zu\n", alignof(max_align_t));
    return 0;
}

sizeof yields a size_t and is evaluated at compile time (except for variable-length arrays), so its operand is not evaluated: sizeof(i++) does not increment i.

See Also