Skip to main content
C beginner Lesson 4 of 23

Data Types in C

Explore C's built-in data types including integers, floating-point numbers, characters, and how to use sizeof and type limits.

Integer Types

C provides several integer types of varying sizes. Choosing the right one matters: too small and you overflow, too large and you waste memory. The <limits.h> header defines the exact range of each type on the current platform.

#include <stdio.h>
#include <limits.h>   /* INT_MAX, CHAR_MIN, etc. */

int main(void) {
    char   c = 127;           /* typically 1 byte, -128 to 127 */
    short  s = 32767;         /* at least 2 bytes */
    int    i = 2147483647;    /* at least 2 bytes, typically 4 */
    long   l = 2147483647L;   /* at least 4 bytes */
    long long ll = 9223372036854775807LL;  /* at least 8 bytes */

    printf("char:      %d bytes, max = %d\n",  (int)sizeof(char),  CHAR_MAX);
    printf("short:     %d bytes, max = %d\n",  (int)sizeof(short), SHRT_MAX);
    printf("int:       %d bytes, max = %d\n",  (int)sizeof(int),   INT_MAX);
    printf("long:      %d bytes, max = %ld\n", (int)sizeof(long),  LONG_MAX);
    printf("long long: %d bytes, max = %lld\n",(int)sizeof(long long), LLONG_MAX);
    return 0;
}

Unsigned Variants

Adding unsigned to any integer type removes negative values and doubles the positive range. Use unsigned types when a value is inherently non-negative (counts, sizes, bit masks).

unsigned char  uc = 255;          /* 0 to 255 */
unsigned short us = 65535;        /* 0 to 65535 */
unsigned int   ui = 4294967295U;  /* 0 to 4,294,967,295 */
unsigned long  ul = 4294967295UL;

Beware of unsigned arithmetic. Subtracting from zero wraps around rather than going negative:

unsigned int x = 0;
x--;   /* x is now 4294967295, not -1 — wraps around */

Fixed-Width Integer Types (stdint.h)

The built-in types like int have platform-dependent sizes. When the exact size matters — network protocols, file formats, hardware registers — use the fixed-width types from <stdint.h>. These guarantee the exact number of bits on every platform.

#include <stdint.h>
#include <inttypes.h>  /* PRId32 etc. for printf */

int8_t   a = -128;
uint8_t  b = 255;
int16_t  c = -32768;
uint16_t d = 65535;
int32_t  e = -2147483648;
uint32_t f = 4294967295U;
int64_t  g = -9223372036854775807LL - 1;
uint64_t h = 18446744073709551615ULL;

/* Use the PRI* macros to print fixed-width types portably */
printf("32-bit int: %" PRId32 "\n", e);
printf("64-bit uint: %" PRIu64 "\n", h);

Also useful:

  • intptr_t / uintptr_t — wide enough to hold a pointer
  • size_t — returned by sizeof, used for array indices and memory sizes
  • ptrdiff_t — result of subtracting two pointers

Floating-Point Types

Floating-point types represent real numbers with fractional parts. They trade off range and precision — double is the default choice for most numeric work because it offers roughly 15-17 significant decimal digits of precision, which is sufficient for the vast majority of applications.

#include <stdio.h>
#include <float.h>  /* FLT_MAX, DBL_MAX, etc. */

int main(void) {
    float  f = 3.14f;          /* ~7 significant decimal digits */
    double d = 3.141592653589793;  /* ~15-17 significant decimal digits */
    long double ld = 3.141592653589793238L;  /* 18-21 digits (platform dependent) */

    printf("float:       %f  (size: %d bytes)\n", f,  (int)sizeof(float));
    printf("double:      %.15f (size: %d bytes)\n", d, (int)sizeof(double));
    printf("long double: %.18Lf (size: %d bytes)\n", ld, (int)sizeof(long double));

    printf("float max:  %e\n", FLT_MAX);
    printf("double max: %e\n", DBL_MAX);
    return 0;
}

Floating-point is not exact. Numbers like 0.1 cannot be represented exactly in binary. Never compare floats with == — always compare within a small tolerance:

double a = 0.1 + 0.2;
double b = 0.3;

/* WRONG — may not print "equal" even though the values look the same */
if (a == b) printf("equal\n");

/* CORRECT — compare within a small epsilon */
#include <math.h>
if (fabs(a - b) < 1e-9) printf("equal\n");

The char Type

char holds a single character and is also an integer type — you can do arithmetic on it. Characters are stored as their ASCII (or Unicode) integer values, so 'A' is just the number 65.

#include <stdio.h>

int main(void) {
    char letter = 'A';
    printf("%c = %d\n", letter, letter);   /* A = 65 */

    /* Characters are just small integers — loop through the alphabet */
    for (char c = 'a'; c <= 'z'; c++) {
        printf("%c", c);
    }
    printf("\n");  /* prints: abcdefghijklmnopqrstuvwxyz */

    /* Arithmetic on chars works naturally */
    char upper = 'a' - 32;   /* 'A' — uppercase is 32 less than lowercase in ASCII */
    char digit = '7' - '0';  /* 7 — subtract '0' to get the integer value of a digit */
    printf("upper=%c, digit=%d\n", upper, digit);
    return 0;
}

Whether plain char is signed or unsigned is implementation-defined. Use signed char or unsigned char explicitly when it matters.

The sizeof Operator

sizeof returns the size in bytes of a type or expression at compile time. It never evaluates the expression — only its type matters. This is essential for writing portable code that doesn’t assume a particular type size.

#include <stdio.h>

int main(void) {
    printf("sizeof(char)      = %zu\n", sizeof(char));       /* always 1 */
    printf("sizeof(short)     = %zu\n", sizeof(short));      /* at least 2 */
    printf("sizeof(int)       = %zu\n", sizeof(int));        /* typically 4 */
    printf("sizeof(long)      = %zu\n", sizeof(long));       /* 4 or 8 */
    printf("sizeof(long long) = %zu\n", sizeof(long long));  /* at least 8 */
    printf("sizeof(float)     = %zu\n", sizeof(float));      /* typically 4 */
    printf("sizeof(double)    = %zu\n", sizeof(double));     /* typically 8 */
    printf("sizeof(pointer)   = %zu\n", sizeof(void *));     /* 4 or 8 */

    /* sizeof on an array gives the total byte size — not the pointer size */
    int arr[10];
    printf("array size = %zu bytes, elements = %zu\n",
           sizeof(arr), sizeof(arr) / sizeof(arr[0]));
    return 0;
}

Type Conversion

C performs implicit conversions in arithmetic expressions, always promoting to the “wider” type. Understanding this prevents subtle bugs where you expect floating-point division but get integer truncation.

#include <stdio.h>

int main(void) {
    int   i = 7;
    float f = 2.5f;

    /* int promotes to float when combined with float — result is float */
    float result = i + f;   /* 9.5 */
    printf("%.1f\n", result);

    /* Integer division — both operands are int, result truncates toward zero */
    int quotient = 7 / 2;   /* 3, not 3.5 */
    printf("%d\n", quotient);

    /* Force floating-point division by casting one operand */
    double exact = (double)7 / 2;   /* 3.5 */
    printf("%.1f\n", exact);

    /* Narrowing conversion — truncates, does not round */
    double big = 3.99;
    int truncated = (int)big;   /* 3, not 4 — truncates toward zero */
    printf("%d\n", truncated);

    return 0;
}

Explicit casts signal to the compiler (and the reader) that you know a conversion is happening. Always cast explicitly when narrowing to document your intent.

Type Qualifiers

Type qualifiers modify how a variable can be accessed. They communicate intent to both the compiler and future readers, enabling optimizations and catching mistakes.

const

const marks a variable as read-only after initialization. The compiler will reject any attempt to modify it, which prevents accidental changes and allows the compiler to optimize.

const double PI = 3.14159265358979;
/* PI = 3.0; */ /* compile error — cannot assign to const */

const int DAYS_IN_WEEK = 7;

volatile

volatile tells the compiler not to cache or optimize reads/writes to this variable, because it may change outside the program’s control — for example, a hardware register, a signal handler, or shared memory.

volatile int hardware_flag = 0;  /* re-read from hardware on every access */

while (!hardware_flag) {
    /* spin-wait — compiler must re-read hardware_flag each iteration */
    /* without volatile, the compiler might cache the value and loop forever */
}

restrict (C99)

restrict tells the compiler that a pointer is the only way to access the memory it points to. This promise lets the compiler generate better-optimized code, particularly for loops.

void add_arrays(int *restrict dst,
                const int *restrict src,
                int n) {
    for (int i = 0; i < n; i++) {
        dst[i] += src[i];  /* compiler can safely vectorize this */
    }
}

Frequently Asked Questions

Why does int have different sizes on different platforms?
The C standard only guarantees minimum sizes (int is at least 16 bits). In practice, int is 32 bits on all modern 32- and 64-bit platforms, but to be safe use stdint.h types like int32_t when size matters.
When should I use float vs double?
Use double by default — it's more precise and modern CPUs handle it just as fast. Use float only when you need to save memory (e.g., large arrays of numbers, GPU programming).
What is the difference between signed and unsigned?
A signed type can represent negative values. An unsigned type can only represent non-negative values but has twice the positive range. For example, a signed 8-bit char ranges from -128 to 127, while an unsigned char ranges from 0 to 255.