4 min read
On this page

Primitive Types

C has a small set of built-in types. Every other data structure you will ever use in C — structs, arrays, linked lists — is built from these primitives. Understanding their sizes, behaviors, and quirks is essential because C exposes the machine-level representation in ways that higher-level languages hide.

Integer Types

The Basic Integer Types

char c = 'A';           // at least 8 bits
short s = 32000;        // at least 16 bits
int i = 2000000;        // at least 16 bits (usually 32)
long l = 2000000000L;   // at least 32 bits
long long ll = 9000000000000000000LL;  // at least 64 bits

The keyword here is "at least." The C standard does not specify exact sizes — it specifies minimums. On a 64-bit Linux system, int is 32 bits and long is 64 bits. On 64-bit Windows, int is 32 bits but long is also 32 bits. This is why portable code avoids relying on the size of int or long.

Fixed-Width Integers: stdint.h

When you need a specific size, use <stdint.h>:

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

int main(void) {
    int8_t   a = 127;            // exactly 8 bits, signed
    uint8_t  b = 255;            // exactly 8 bits, unsigned
    int16_t  c = 32767;          // exactly 16 bits, signed
    uint16_t d = 65535;          // exactly 16 bits, unsigned
    int32_t  e = 2147483647;     // exactly 32 bits, signed
    uint32_t f = 4294967295U;    // exactly 32 bits, unsigned
    int64_t  g = 9223372036854775807LL;  // exactly 64 bits, signed
    uint64_t h = 18446744073709551615ULL; // exactly 64 bits, unsigned

    printf("int32_t: %d\n", e);
    printf("uint64_t: %lu\n", (unsigned long)h);

    return 0;
}

Use int for loop counters and local arithmetic. Use stdint.h types when the size matters — network protocols, file formats, hardware registers, cross-platform data structures.

The sizeof Operator

sizeof tells you the size of a type or variable in bytes. It is evaluated at compile time, not runtime.

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

int main(void) {
    printf("char:      %zu bytes\n", sizeof(char));
    printf("short:     %zu bytes\n", sizeof(short));
    printf("int:       %zu bytes\n", sizeof(int));
    printf("long:      %zu bytes\n", sizeof(long));
    printf("long long: %zu bytes\n", sizeof(long long));
    printf("float:     %zu bytes\n", sizeof(float));
    printf("double:    %zu bytes\n", sizeof(double));
    printf("void*:     %zu bytes\n", sizeof(void *));

    return 0;
}
char:      1 bytes
short:     2 bytes
int:       4 bytes
long:      8 bytes
long long: 8 bytes
float:     4 bytes
double:    8 bytes
void*:     8 bytes

The only guarantee: sizeof(char) is always 1. Everything else depends on the platform. Always use sizeof instead of hardcoding sizes.

Floating-Point Types

float f = 3.14f;        // 32 bits, ~7 decimal digits of precision
double d = 3.14159265;  // 64 bits, ~15 decimal digits of precision
long double ld = 3.14159265358979323846L;  // at least 64 bits

Use double by default. float has limited precision and is only worth using when memory is tight (large arrays, GPU programming). Floating-point arithmetic is not exact:

#include <stdio.h>

int main(void) {
    double a = 0.1 + 0.2;
    printf("0.1 + 0.2 = %.20f\n", a);
    printf("Equal to 0.3? %d\n", a == 0.3);

    return 0;
}
0.1 + 0.2 = 0.30000000000000004441
Equal to 0.3? 0

Never compare floats with ==. Use a tolerance:

#include <math.h>

int doubles_equal(double a, double b, double epsilon) {
    return fabs(a - b) < epsilon;
}

Signed vs Unsigned

Every integer type comes in signed and unsigned variants. Signed integers can be negative. Unsigned integers are always non-negative but can represent larger positive values.

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

int main(void) {
    printf("int range:          %d to %d\n", INT_MIN, INT_MAX);
    printf("unsigned int range: 0 to %u\n", UINT_MAX);

    return 0;
}
int range:          -2147483648 to 2147483647
unsigned int range: 0 to 4294967295

The Unsigned Underflow Bug

Unsigned integers cannot be negative. Subtracting past zero wraps around to a large positive value:

#include <stdio.h>

int main(void) {
    unsigned int x = 0;
    x = x - 1;
    printf("0 - 1 as unsigned: %u\n", x);

    // This loop never terminates
    unsigned int len = 5;
    for (unsigned int i = len - 1; i >= 0; i--) {
        printf("%u ", i);
        // when i is 0, i-- wraps to 4294967295, which is >= 0
        // this is an infinite loop
        if (i == 0) break;  // must explicitly break
    }
    printf("\n");

    return 0;
}
0 - 1 as unsigned: 4294967295
4 3 2 1 0

Use size_t (an unsigned type) for sizes and indices. But be careful when decrementing — the pattern for (size_t i = n - 1; i >= 0; i--) is an infinite loop because i can never be less than 0.

Type Promotion Rules

When you mix types in an expression, C implicitly converts (promotes) narrower types to wider types:

#include <stdio.h>

int main(void) {
    char c = 10;
    int i = 20;
    double d = 3.14;

    // char is promoted to int
    int result1 = c + i;        // 30

    // int is promoted to double
    double result2 = i + d;     // 23.14

    // integer division vs floating-point division
    int a = 7, b = 2;
    printf("7 / 2 = %d\n", a / b);           // 3 (integer division)
    printf("7 / 2.0 = %f\n", a / 2.0);       // 3.500000

    printf("result1: %d\n", result1);
    printf("result2: %f\n", result2);

    return 0;
}
7 / 2 = 3
7 / 2.0 = 3.500000
result1: 30
result2: 23.140000

The promotion rules: char/short promote to int. If one operand is double, the other promotes to double. If one is unsigned and the other signed of the same rank, the signed value converts to unsigned — which is where bugs hide.

Boolean: stdbool.h

C did not have a boolean type until C99. Before that, programmers used int with 0 for false and nonzero for true.

#include <stdio.h>
#include <stdbool.h>

int main(void) {
    bool is_valid = true;
    bool is_empty = false;

    if (is_valid) {
        printf("valid\n");
    }

    // Any nonzero value is truthy in C
    int x = 42;
    if (x) {
        printf("42 is truthy\n");
    }

    // Zero is falsy
    int y = 0;
    if (!y) {
        printf("0 is falsy\n");
    }

    return 0;
}
valid
42 is truthy
0 is falsy

Use stdbool.h for readability. Understand that underneath, bool is still an integer — true is 1 and false is 0.

char Is Just a Small Integer

char is an integer type that happens to hold character values. ASCII maps characters to numbers: 'A' is 65, '0' is 48, '\n' is 10.

#include <stdio.h>

int main(void) {
    char c = 'A';
    printf("character: %c\n", c);
    printf("integer value: %d\n", c);
    printf("next character: %c\n", c + 1);

    // Character arithmetic
    char digit = '7';
    int value = digit - '0';   // converts char '7' to int 7
    printf("digit '%c' has value %d\n", digit, value);

    return 0;
}
character: A
integer value: 65
next character: B
digit '7' has value 7

Whether char is signed or unsigned is implementation-defined. Use signed char or unsigned char when the signedness matters. Use uint8_t for raw byte data.

printf Format Specifiers

Every type needs the correct format specifier in printf. Using the wrong one is undefined behavior.

Specifier Type Example
%d int printf("%d", 42)
%u unsigned int printf("%u", 42U)
%ld long printf("%ld", 42L)
%f double printf("%f", 3.14)
%e double (scientific) printf("%e", 3.14)
%c char printf("%c", 'A')
%s char * (string) printf("%s", "hello")
%p void * (pointer) printf("%p", (void *)ptr)
%zu size_t printf("%zu", sizeof(int))
%x unsigned int (hex) printf("0x%x", 255)

For <stdint.h> types, use the macros from <inttypes.h>: printf("%" PRId64 "\n", my_int64). These expand to the correct format string for the platform.

Common Pitfalls

  • Assuming int is 32 bits — it is on most modern systems, but the standard only guarantees 16 bits minimum. Use int32_t when the size matters.
  • Comparing signed and unsigned integers — when a negative signed value is compared to an unsigned value, the signed value is converted to unsigned, producing a huge positive number. -1 < 1U evaluates to false because -1 becomes 4294967295U.
  • Using float when you need doublefloat has only ~7 digits of precision. Accumulated rounding errors in float arithmetic cause real bugs in real programs.
  • Forgetting that integer division truncates7 / 2 is 3, not 3.5. Cast one operand to double if you need the fractional part.
  • Using %d for size_tsize_t is unsigned and may be 64 bits. Use %zu.

Key Takeaways

  • C has a small set of primitive types: char, short, int, long, long long, float, double. Their sizes are platform-dependent.
  • Use <stdint.h> types (int32_t, uint64_t) when you need exact sizes. Use sizeof instead of hardcoding byte counts.
  • Unsigned underflow (subtracting past zero) wraps to a large positive value. This is the source of many C bugs.
  • char is an integer type. Boolean values are integers. C's type system is thinner than it looks.
  • Always use the correct printf format specifier. Mismatches are undefined behavior.
  • Type promotion rules govern how C converts between types in expressions. Signed/unsigned mixing is especially dangerous.