Types and how data lives in memory · about 45 min

Integer types, sizes and limits

Why this matters

In many languages, an integer is just "a number" that grows as large as needed. In C, every integer type has a fixed size in bytes, chosen when the program is compiled, and therefore a fixed range. Store a value outside that range and you do not get an error: you get a wrong number. Choosing the right type, and knowing its limits, is part of writing correct C. It also matters for memory: an array of a million char is 1 MB, of a million long long is 8 MB.

The integer types

C has five standard signed integer types, in increasing (or equal) size:

typetypical size on 64-bit Linuxrange on 64-bit Linuxprintf
signed char1 byte-128 to 127%hhd
short2 bytes-32,768 to 32,767%hd
int4 bytes-2,147,483,648 to 2,147,483,647%d
long8 bytesabout -9.2 * 10^18 to 9.2 * 10^18%ld
long long8 bytes-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807%lld

Each has an unsigned counterpart (unsigned char, unsigned short, unsigned int or just unsigned, unsigned long, unsigned long long) that uses the same number of bytes but holds only non-negative values, so its maximum is about twice as large: unsigned int goes from 0 to 4,294,967,295. Unsigned types print with %u, %lu, %llu (or in hex with %x, %lx). How signed values are stored is the subject of the next lesson.

int is the "natural" integer for the machine and the default choice for ordinary counting. Arithmetic on smaller types is actually done in int anyway, as you will see in the next module.

What the standard guarantees, and what it does not

The C standard does not fix the sizes. It only guarantees minimums and an ordering:

  char  >= 8 bits
  short >= 16 bits,  int >= 16 bits,  long >= 32 bits,  long long >= 64 bits
  sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)

Real platforms pick a data model. Every 64-bit Unix (Linux, macOS, BSD) uses LP64: int 32 bits, long and pointers 64 bits. 64-bit Windows uses LLP64: long stays 32 bits. Tiny microcontrollers may have a 16-bit int. So int and long are fine for values whose range you know is small, but not when you need an exact width.

Exact-width types: stdint.h

For exact sizes, include <stdint.h>:

typemeaning
int8_t, int16_t, int32_t, int64_tsigned, exactly that many bits
uint8_t, uint16_t, uint32_t, uint64_tunsigned, exactly that many bits
intptr_t, uintptr_tbig enough to hold a pointer as an integer

Use these for file formats, network protocols, hardware registers, hashing, and anything else where the bit count is part of the meaning. Printing them portably uses macros from <inttypes.h>, which look odd but expand to the right specifier string:

#include <inttypes.h>
int64_t population = 8100000000;
printf("%" PRId64 "\n", population);   /* PRId64 is "ld" on Linux */

sizeof: asking the compiler

The sizeof operator gives the size, in bytes, of a type or of an expression's type. It is evaluated at compile time; nothing is computed at run time.

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

int main(void)
{
    printf("char:      %zu byte\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));

    int x = 42;
    printf("sizeof x = %zu\n", sizeof x);       /* parentheses optional for expressions */

    printf("int range: %d to %d\n", INT_MIN, INT_MAX);
    printf("unsigned max: %u\n", UINT_MAX);
    printf("long long max: %lld\n", LLONG_MAX);
    return 0;
}
char:      1 byte
short:     2 bytes
int:       4 bytes
long:      8 bytes
long long: 8 bytes
sizeof x = 4
int range: -2147483648 to 2147483647
unsigned max: 4294967295
long long max: 9223372036854775807

sizeof(char) is 1 by definition; all other sizes are measured in chars (bytes). The result of sizeof has the type size_t, an unsigned integer type big enough for the size of any object (64 bits on 64-bit systems). Print it with %zu. You will use size_t constantly for sizes, lengths and array indices.

limits.h

<limits.h> defines the actual ranges on the platform you compile for, so you never have to memorize or hard-code them:

macrovalue on 64-bit Linux
CHAR_BIT8 (bits per byte)
SCHAR_MIN, SCHAR_MAX, UCHAR_MAX-128, 127, 255
SHRT_MIN, SHRT_MAX, USHRT_MAX-32768, 32767, 65535
INT_MIN, INT_MAX, UINT_MAX-2147483648, 2147483647, 4294967295
LONG_MIN, LONG_MAX, ULONG_MAXsame as long long on LP64
LLONG_MIN, LLONG_MAX, ULLONG_MAX-9223372036854775808, 9223372036854775807, 18446744073709551615

stdint.h has the same for its types: INT32_MAX, UINT64_MAX, SIZE_MAX and so on.

Integer literals have types too

A literal like 42 is not "just a number"; it has a type. A plain decimal literal is an int if it fits, otherwise long, otherwise long long. You can force a type with a suffix:

literaltype
42int
42Uunsigned int
42Llong
42ULunsigned long
42LLlong long
42ULLunsigned long long

Suffixes may be upper or lower case, but use uppercase L: 42l looks like 421.

This matters because of a rule you will study properly in the next module: an expression is computed in the type of its operands, before the result is stored anywhere. Here is the classic trap:

int seconds = 100 * 365 * 24 * 60 * 60;
printf("%d\n", seconds);
main.c:5:39: warning: integer overflow in expression of type 'int' results in '-1141367296' [-Woverflow]
-1141367296

All the literals are int, so the multiplication happens in int and overflows long before anything is stored. Changing only the variable to long long does not help; the damage is done on the right side. Making one operand long long (100LL * 365 * ...) makes the whole calculation happen in long long.

Common mistakes

Recap

Going further: read /usr/include/limits.h in the sandbox terminal (less /usr/include/limits.h) to see how the macros are defined, and look up "Integer types" and "Fixed width integer types" on cppreference.com.

Exercises

Exercise 1 · quiz

How big is a long?

On 64-bit Linux (x86-64, the sandbox), what is sizeof(long)?

Answer
  • 8

64-bit Linux and macOS use the LP64 model: int is 4 bytes, long and pointers are 8. 64-bit Windows uses LLP64, where long is only 4 bytes and you need long long for 8. This is exactly why code that must have a specific width should use int64_t and friends from <stdint.h>. A variable's size is fixed by its type at compile time and never depends on the value.

Exercise 2 · write code

Measure the types

Use the sizeof operator to print the size of each integer type, exactly in this format:

char: 1
short: 2
int: 4
long: 8
long long: 8

sizeof produces a value of type size_t, which prints with %zu. The numbers must come from sizeof, not be typed in.

File: main.c

Starting point

#include <stdio.h>

int main(void)
{
    printf("char: %zu\n", sizeof(char));
    /* the other four types */
    return 0;
}

Example

expected output

char: 1
short: 2
int: 4
long: 8
long long: 8
Hint 1

Copy the char line and change the type name in both places.

Hint 2

sizeof(long long) works: a type name in parentheses.

Reference solution
#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));
    return 0;
}

Exercise 3 · write code

Ask limits.h

Print the range of int, the maximum unsigned int, and the maximum long long, using the macros from <limits.h> (not typed-in digits). Output exactly:

int: -2147483648 to 2147483647
unsigned int max: 4294967295
long long max: 9223372036854775807

Pay attention to the format specifier for each type: %d for int, %u for unsigned int, %lld for long long.

File: main.c

Starting point

#include <stdio.h>

int main(void)
{
    /* include <limits.h> above, then use INT_MIN, INT_MAX, UINT_MAX, LLONG_MAX */
    return 0;
}

Example

expected output

int: -2147483648 to 2147483647
unsigned int max: 4294967295
long long max: 9223372036854775807
Hint 1

Add #include <limits.h> under the stdio include.

Hint 2

printf("int: %d to %d\n", INT_MIN, INT_MAX);

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

int main(void)
{
    printf("int: %d to %d\n", INT_MIN, INT_MAX);
    printf("unsigned int max: %u\n", UINT_MAX);
    printf("long long max: %lld\n", LLONG_MAX);
    return 0;
}

Exercise 4 · quiz

Pick a type

You need to store the world population (about 8,100,000,000) in a variable. Which types can hold it on every platform? Select all that apply.

Answer
  • long long

  • int64_t

8.1 billion is more than UINT_MAX (about 4.29 billion), so no 32-bit type can hold it. long is 64 bits on Linux and macOS but only 32 on Windows, so it is not guaranteed. long long is at least 64 bits everywhere, and int64_t is exactly 64 bits wherever it exists (which is everywhere you are likely to work).

Exercise 5 · write code

Seconds in a century

The starter computes the number of seconds in 100 years of 365 days, but prints a negative number, and gcc warns about it. Fix it so the program prints exactly:

3153600000

Keep the multiplication 100 * 365 * 24 * 60 * 60 in the source (the check looks for it), but make sure the arithmetic happens in a type big enough for the result.

File: main.c

Starting point

#include <stdio.h>

int main(void)
{
    int seconds = 100 * 365 * 24 * 60 * 60;
    printf("%d\n", seconds);
    return 0;
}

Example

expected output

3153600000
Hint 1

Read the warning. The result does not fit in an int, whose maximum is about 2.1 billion.

Hint 2

Changing only the variable's type is not enough: the right side is computed first, as int, because every literal in it is an int.

Hint 3

Make the first literal a long long with the LL suffix (100LL), store into a long long, and print with %lld.

Reference solution
#include <stdio.h>

int main(void)
{
    long long seconds = 100LL * 365 * 24 * 60 * 60;
    printf("%lld\n", seconds);
    return 0;
}