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:
| type | typical size on 64-bit Linux | range on 64-bit Linux | printf |
|---|---|---|---|
signed char | 1 byte | -128 to 127 | %hhd |
short | 2 bytes | -32,768 to 32,767 | %hd |
int | 4 bytes | -2,147,483,648 to 2,147,483,647 | %d |
long | 8 bytes | about -9.2 * 10^18 to 9.2 * 10^18 | %ld |
long long | 8 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>:
| type | meaning |
|---|---|
int8_t, int16_t, int32_t, int64_t | signed, exactly that many bits |
uint8_t, uint16_t, uint32_t, uint64_t | unsigned, exactly that many bits |
intptr_t, uintptr_t | big 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:
| macro | value on 64-bit Linux |
|---|---|
CHAR_BIT | 8 (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_MAX | same 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:
| literal | type |
|---|---|
42 | int |
42U | unsigned int |
42L | long |
42UL | unsigned long |
42LL | long long |
42ULL | unsigned 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
- Wrong printf specifier for the type.
printf("%d\n", sizeof(int));givessize_tislong unsigned inton Linux; use%zu. Likewise%ldfor long,%lldfor long long,%ufor unsigned.warning: format '%d' expects argument of type 'int', but argument 2 has type 'long unsigned int' [-Wformat=] - Assuming
longis 64 bits. True on Linux, false on Windows. Useint64_torlong longwhen you need 64 bits. - Overflowing in the expression, not the variable, as in the seconds example. Watch for products of several ints.
- Using
intfor sizes and counts of things in memory.size_tis the right type and avoids both overflow on huge objects and warnings when comparing withsizeofresults.
Recap
- Integer types:
char,short,int,long,long long, each signed or unsigned, each with a fixed size and range. - The standard gives only minimums; 64-bit Linux is LP64 (int 4 bytes, long 8).
<stdint.h>gives exact widths (int32_t,uint8_t, ...);<limits.h>and<stdint.h>give the limits (INT_MAX,UINT64_MAX, ...).sizeofgives sizes in bytes at compile time, as asize_t(print with%zu).- Literals have types; suffixes
U,L,LLchange them, and the operand types decide the type an expression is computed in.
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.