Sizes, Limits & Overflow

C · Lesson 6 of 32 7 min read Updated

Every C type has a fixed size in memory, and that size limits the values it can hold. An int that counts cricket runs will never reach its limit, but an int that adds up a company's yearly sales in paise easily can. When a value goes past the limit, C does not stop with an error. It silently produces a wrong answer, or worse.

In this lesson you will measure types with sizeof, read their exact limits, use types with guaranteed sizes, and learn how to detect overflow before it causes damage.

Measuring sizes with sizeof

The sizeof operator gives the size of a type or a variable in bytes (one byte is 8 bits on every normal system). Its result has the type size_t, an unsigned whole-number type, and you print it with %zu.

C
#include <stdio.h>

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

    int marks = 90;
    printf("marks:     %zu\n", sizeof marks);
    return 0;
}
Output
char:      1
short:     2
int:       4
long:      8
long long: 8
float:     4
double:    8
bool:      1
marks:     4

This output is from 64-bit Linux, and macOS gives the same numbers. On 64-bit Windows, long is only 4 bytes, so the same program prints long: 4. This is the most common size difference between systems, and the reason careful programmers avoid long when they need a definite size. With a type name, sizeof needs brackets; with a variable name, the brackets are optional.

Limits from limits.h

The header limits.h defines the smallest and largest value of each whole-number type as named constants. The header float.h does the same for floating-point types.

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

int main(void)
{
    printf("Bits in a char: %d\n", CHAR_BIT);
    printf("int:       %d to %d\n", INT_MIN, INT_MAX);
    printf("unsigned:  0 to %u\n", UINT_MAX);
    printf("long long: %lld to %lld\n", LLONG_MIN, LLONG_MAX);
    printf("double keeps about %d significant digits\n", DBL_DIG);
    return 0;
}
Output
Bits in a char: 8
int:       -2147483648 to 2147483647
unsigned:  0 to 4294967295
long long: -9223372036854775808 to 9223372036854775807
double keeps about 15 significant digits

The pattern comes from binary. A signed type with N bits holds values from -2^(N-1) up to 2^(N-1) - 1, where ^ here means "to the power of". For a 32-bit int, that is about ±2.1 billion. The unsigned version holds 0 up to 2^N - 1, about 4.2 billion. C23 requires signed integers to use two's complement, the binary format every modern processor already uses, so these ranges are now the same everywhere for a given size.

Fixed-width types: stdint.h

When a value must have an exact size, for example in a file format, a network message or code for a microcontroller, use the types from stdint.h. The name says the size: int8_t, int16_t, int32_t and int64_t are signed, and uint8_t to uint64_t are unsigned. The header inttypes.h includes stdint.h and adds the matching printf macros.

C
#include <inttypes.h>
#include <stdio.h>

int main(void)
{
    int64_t distance_mm = 3000000000;   // 3,000 km in millimetres
    uint8_t brightness = 200;           // a pixel value, 0 to 255
    int16_t temperature = -12;          // a winter night in Leh

    printf("Distance: %" PRId64 " mm\n", distance_mm);
    printf("Brightness: %" PRIu8 "\n", brightness);
    printf("Temperature: %" PRId16 " C\n", temperature);
    printf("int32_t uses %zu bytes\n", sizeof(int32_t));
    return 0;
}
Output
Distance: 3000000000 mm
Brightness: 200
Temperature: -12 C
int32_t uses 4 bytes

PRId64 is a macro that turns into the right letters, such as "ld" or "lld", for the current system. C joins string literals that sit next to each other, so "Distance: %" PRId64 " mm\n" becomes one complete format string. It looks odd at first, but it prints a 64-bit value correctly on every platform.

Overflow: when a value does not fit

Overflow happens when the result of a calculation is too big or too small for its type. What happens next depends on whether the type is signed or unsigned.

Unsigned types wrap around. The value is reduced modulo 2^N (256 for an 8-bit type), like a car's odometer rolling over from 99999 to 00000. This behaviour is fully defined and sometimes useful.

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

int main(void)
{
    unsigned char level = 250;
    level = level + 10;   // 260 does not fit in 0..255
    printf("level = %d\n", level);

    unsigned int counter = 0;
    counter = counter - 1;
    printf("0 - 1 as unsigned = %u\n", counter);
    printf("UINT_MAX          = %u\n", UINT_MAX);
    return 0;
}
Output
level = 4
0 - 1 as unsigned = 4294967295
UINT_MAX          = 4294967295

260 is 4 more than 256, so level wraps to 4. Going below zero wraps to the largest value.

Signed overflow is undefined behaviour. If an int calculation goes past INT_MAX, the C standard says nothing about the result. The program might print a negative number, or the compiler's optimiser might remove a safety check that it assumed could never fail. You must make sure signed overflow never happens. A common trap is multiplying two ordinary numbers: 50000 * 50000 is 2.5 billion, which does not fit in an int. Write 50000LL * 50000 so the calculation is done in long long.

Checking for overflow with ckd_add

C
#include <stdckdint.h>
#include <stdio.h>

int main(void)
{
    int a = 1500000000;
    int b = 900000000;
    int sum;

    if (ckd_add(&sum, a, b)) {
        printf("%d + %d overflows an int\n", a, b);
    } else {
        printf("Sum: %d\n", sum);
    }

    if (ckd_add(&sum, 1200, 99)) {
        printf("1200 + 99 overflows an int\n");
    } else {
        printf("Sum: %d\n", sum);
    }
    return 0;
}
Output
1500000000 + 900000000 overflows an int
Sum: 1299

The & before sum passes the variable's memory address so that ckd_add can store the result there; you will learn this in Pointers, NULL & nullptr. stdckdint.h is available from GCC 14 and Clang 18. With older compilers, check before you add: for positive b, the sum overflows exactly when a > INT_MAX - b. That test itself can never overflow.

Choosing the right type

Kind of valueGood type
Counts, marks, ages, loop countersint
Array lengths, memory sizes, results of sizeofsize_t
Large totals, money in paiselong long or int64_t
Raw bytes, pixel valuesuint8_t or unsigned char
Exact sizes in files and network dataint32_t, uint16_t and friends
Measurements with decimalsdouble

Common mistakes

  • Printing sizeof with %d. Its type is size_t; use %zu.
  • Assuming long is 8 bytes. It is 4 bytes on Windows. Use long long or int64_t when you need 64 bits.
  • Mixing signed and unsigned in a comparison. In -1 < 10u, the -1 is converted to unsigned and becomes 4294967295, so the result is false. -Wextra warns about such comparisons.
  • Counting down with an unsigned variable. for (unsigned i = 5; i >= 0; i--) never ends, because an unsigned value is always 0 or more.

Key points

  • sizeof gives sizes in bytes as a size_t; print it with %zu.
  • Sizes vary by system: long is 8 bytes on 64-bit Linux and macOS but 4 bytes on Windows.
  • limits.h and float.h give exact limits such as INT_MAX and DBL_DIG.
  • stdint.h types like int32_t and uint8_t have guaranteed sizes; print them with the inttypes.h macros.
  • Unsigned overflow wraps around; signed overflow is undefined behaviour and must be prevented.
  • C23 ckd_add, ckd_sub and ckd_mul detect overflow safely.

Exercise

A cinema chain sold 2,000,000 tickets at Rs 1,150 each and stores money in paise. Write a program that multiplies the tickets by the price in paise (115000) using ckd_mul with an int result, and prints a warning if it overflows. Then do the same calculation with an int64_t result and print the total.

Show solution
C
#include <inttypes.h>
#include <stdckdint.h>
#include <stdio.h>

int main(void)
{
    int tickets = 2000000;
    int price_paise = 115000;

    int small_total;
    if (ckd_mul(&small_total, tickets, price_paise)) {
        printf("Too big for an int!\n");
    }

    int64_t total;
    if (ckd_mul(&total, tickets, price_paise)) {
        printf("Too big even for int64_t!\n");
    } else {
        printf("Total: %" PRId64 " paise\n", total);
    }
    return 0;
}
Output
Too big for an int!
Total: 230000000000 paise

The product is 230 billion paise (Rs 230 crore), far above INT_MAX. ckd_mul computes the exact result and checks it against the type of the result variable, so the second call succeeds with int64_t.

Quick Quiz

1. What does sizeof(int) usually give on a 64-bit computer?

2. What happens when an unsigned int holding 0 has 1 subtracted from it?

3. Which statement about signed integer overflow is correct?

4. Which format specifier prints a size_t?

Learn C with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call