Learn C Programming

Lesson 5 of 7 · Variables and Data Types

Module 2 · Variables and Data Types

sizeof and limits.h: Asking the Machine Instead of Guessing

FreeReading

In this lesson

  • Ask the machine for a type's size with sizeof, instead of remembering a table.
  • Print every integer limit from <limits.h> and every decimal limit from <float.h>.
  • Name the fixed-width type to use when a size has to be the same on every machine.

Kenji's program works for a year on his laptop. Then it is loaded onto a small chip in a card reader, and every number above 32,767 comes out wrong.

He had not written a bug. He had written down a size, in his head, and taken it with him to a machine that disagreed. This lesson is one habit: ask, do not guess.

sizeof: the operator that answers in bytes

sizeof looks like a function and is not one. It is an operator, built into the language, and the compiler works out its answer before the program ever runs.

The two forms of sizeof

sizeof(int)         a TYPE needs the parentheses
sizeof marks        an EXPRESSION does not
sizeof(marks)       also fine, and what most people write

sizeof marks + 1    is (sizeof marks) + 1, which is 5
sizeof (marks + 1)  is the size of the expression, which is 4
  • The answer is a count of bytes, and a byte is CHAR_BIT bits, which is 8 everywhere you will go.
  • sizeof(char) is 1 by definition, in every C implementation that has ever existed.
  • The expression inside is never run. sizeof(x / 0) is a size, not a crash.
  • Its type is size_t, and the specifier for that is %zu.

So parentheses are required for a type and optional for a variable. The last two lines of that card are a trap worth reading twice.

size_t, and the %zu that goes with it

sizeof does not give back an int. It gives back a size_t, an unsigned type that is wide enough to hold the size of the largest thing the machine can store.

It is 8 bytes on the Playground and on any 64-bit machine, and 2 bytes on a small chip. That is the point of it: it is defined by the machine, not by a number somebody chose.

Because it is unsigned, size_t can never be negative, and subtracting past zero wraps exactly as lesson 2 described.

So print it with %zu, and when you want a plain int for arithmetic, write the cast yourself and mean it.

limits.h and float.h: printed, not copied

Two headers hold every boundary value, already correct for the machine you are on.

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

int main(void)
{
    printf("bits in a byte : %d\n", CHAR_BIT);
    printf("int range      : %d to %d\n", INT_MIN, INT_MAX);
    printf("long long top  : %lld\n", LLONG_MAX);
    printf("float digits   : %d\n", FLT_DIG);
    printf("double digits  : %d\n", DBL_DIG);
    printf("double largest : %e\n", DBL_MAX);
    return 0;
}
bits in a byte : 8
int range      : -2147483648 to 2147483647
long long top  : 9223372036854775807
float digits   : 6
double digits  : 15
double largest : 1.797693e+308

FLT_DIG is 6 and DBL_DIG is 15. Lesson 3 said "about 7" and "about 15 or 16"; these are the numbers the machine will actually stand behind.

So a range you printed is a fact about today's machine. A range you typed from memory is a hope.

long is the one to distrust

Here is a fact you can check for yourself, and it is the reason this whole lesson exists.

On the Playground, which runs 64-bit Linux, sizeof(long) is 8. On a Windows machine with the same processor and the same compiler, it is 4.

Same code, same chip, same GCC. Two answers. Nothing is wrong with either one: the standard promises only that long reaches about plus or minus 2.1 billion, and both machines keep that promise.

Every other size in this module agrees across both: char 1, short 2, int 4, long long 8, float 4, double 8.

So this track writes long long and never long, and the exercise below asks you to print a list of sizes with long deliberately left out.

Type sizes in bytes, and the one that changes What sizeof answers on the Playground, in bytes char 1 short 2 int 4 float 4 long long 8 double 8 size_t 8 long 4 on Windows, 8 on Linux Seven sizes you can rely on, and one you cannot. Ask the machine before you assume.
Figure 1. Every bar here came out of a real sizeof. The dashed half of the last bar is the reason this track writes long long.

stdint.h: when the size has to be the same everywhere

Sometimes "at least 4 bytes" is not good enough. A network packet, a file format and a hash all need a value that is exactly 32 bits wide, on every machine, for ever.

<stdint.h> gives you those types, and their names say the width out loud.

TypeWidthRangeUse it for
int8_t8 bits-128 to 127a small signed value in a packed structure
uint8_t8 bits0 to 255a raw byte, a colour channel
int16_t16 bits-32,768 to 32,767an audio sample
uint16_t16 bits0 to 65,535a port number
int32_t32 bitsabout plus or minus 2.1 billionan ordinary counter that must not change size
uint32_t32 bits0 to 4,294,967,295an IPv4 address, a checksum
int64_t64 bitsabout plus or minus 9.2 quintilliona timestamp, a file size

These are not new types. They are other names for the ordinary types, chosen per machine so that the width is right. int32_t is int on the Playground.

Printing them takes care, because the underlying type differs by machine. Inside this module, read and print a fixed-width value through a long long and %lld, which is wide enough for all of them.

So use a plain int for ordinary counting, and a fixed-width type when the number of bits is part of the specification.

Example 1: the smallest question you can ask

Two forms of sizeof, on a type and on a variable, and the precedence trap on the last line.

#include <stdio.h>

int main(void)
{
    int marks = 90;

    printf("sizeof(int)        : %zu\n", sizeof(int));
    printf("sizeof marks       : %zu\n", sizeof marks);
    printf("sizeof marks + 1   : %zu\n", sizeof marks + 1);
    printf("sizeof (marks + 1) : %zu\n", sizeof (marks + 1));
    return 0;
}
sizeof(int)        : 4
sizeof marks       : 4
sizeof marks + 1   : 5
sizeof (marks + 1) : 4

The third line is 5 because sizeof binds tighter than +. It asked for the size, got 4, then added 1 to it. Parentheses remove the question.

Run in Compiler
Example 2: the sizes, asked rather than remembered

Every size in this module, printed by the machine. long is left out on purpose, and the last line says why.

#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 long : %zu\n", sizeof(long long));
    printf("float     : %zu\n", sizeof(float));
    printf("double    : %zu\n", sizeof(double));
    printf("size_t    : %zu\n", sizeof(size_t));
    printf("long      : ask your own machine\n");
    return 0;
}
char      : 1
short     : 2
int       : 4
long long : 8
float     : 4
double    : 8
size_t    : 8
long      : ask your own machine

Those seven numbers are the same on the Playground and on a Windows laptop. Add a line for long and the two machines stop agreeing.

Run in Compiler
Example 3: the fixed-width types, the one a beginner actually writes

A small packet header: a version byte, a port and a timestamp. Every field's width is part of the design.

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

int main(void)
{
    uint8_t  version = 4;
    uint16_t port = 443;
    int64_t  timestamp = 1758240000;

    printf("version   : %lld in %zu byte\n", (long long)version, sizeof version);
    printf("port      : %lld in %zu bytes\n", (long long)port, sizeof port);
    printf("timestamp : %lld in %zu bytes\n", (long long)timestamp, sizeof timestamp);
    return 0;
}
version   : 4 in 1 byte
port      : 443 in 2 bytes
timestamp : 1758240000 in 8 bytes

Each value is widened to a long long for printing, which always works and never surprises. Module 15 covers the proper specifiers from <inttypes.h>.

Run in Compiler

Where this is used

  • The TCP header. Source port and destination port are uint16_t, the sequence number is uint32_t. Those widths are in the standard that the whole internet implements, so a C type that "might be wider" is useless there.
  • The PNG file format. Every chunk starts with a 32-bit big-endian length. Every PNG reader in every language declares that field as exactly 32 bits, and libpng uses the <stdint.h> names to do it.
  • SHA-256. The hash is defined entirely in 32-bit unsigned arithmetic, wrap included. Written with plain int it would give a different answer on a 16-bit chip, so every implementation uses uint32_t.
  • Embedded firmware. On an 8-bit microcontroller an int is 2 bytes, so the same source that ran on a laptop overflows at 32,767. Firmware code uses int16_t and int32_t precisely to stop that from being a surprise.

Common mistakes

1. Printing a sizeof result with %d.

printf("%d\n", sizeof(int));

Silent on the Playground, because the format check needs -Wall. It usually prints 4 and is still wrong. A local gcc -Wall says warning: format '%d' expects argument of type 'int', but argument 2 has type ..., and the tail of that message names a different type on Linux and on Windows, which is the lesson in one line. Write %zu.

2. Reaching for a limit without its header.

#include <stdio.h>

int main(void)
{
    printf("%d\n", INT_MAX);
    return 0;
}

GCC 12 says error: 'INT_MAX' undeclared (first use in this function) and then note: 'INT_MAX' is defined in header '<limits.h>'. The note is doing you a favour. Read every note, not only the error above it.

3. Adding to a sizeof without parentheses.

int marks = 90;
printf("%zu\n", sizeof marks + 1);

No message at all, and it prints 5. You asked for the size of the expression and got the size of the variable, plus one. Write sizeof(marks + 1) when that is what you mean.

4. Writing down a size instead of asking for it.

long total_bytes = 8;      /* "a long is 8 bytes" */

No message, and it is right on the Playground and wrong on Windows. The comment is the bug: it records a guess as a fact. Write sizeof(long) and the program stops needing the comment.

Brain teaser

Zara wants the number of bits in an int, so she writes this.

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

int main(void)
{
    printf("bits: %zu\n", sizeof(int) * 8);
    printf("bits: %zu\n", sizeof(int) * CHAR_BIT);
    return 0;
}

Both lines print 32 on the Playground. Say why the second line is better anyway, and name one machine where the two would disagree. Then answer the harder half: even the second line is not the number of value bits an int has. Say what else could be inside those bytes.

Look up what the C standard actually promises about a byte. Then ask whether every bit of a type has to carry part of the value.

Exercise 1Easy

Print the sizes of the six types this module has used, in a fixed shape, with nothing typed from memory.

Input. None.

Output. Six lines, in this order: char, short, int, long long, float, double. Each line is the type name, a colon, one space, and the size in bytes.

Constraints. Every number comes from sizeof. Do not write long, and be ready to say why.

Sample. The first line is char: 1.

#include <stdio.h>

int main(void)
{
    printf("char: %zu\n", sizeof(char));

    /* Five more lines, same shape. */

    return 0;
}

Not graded in this module. A judged version of this would break on a machine where long is 4 bytes. That is the reason the type is missing.

Run in Compiler
Exercise 2Medium

Amara is working out how much memory a table of readings will need before she allocates it.

Input. One line with one integer n, the number of readings.

Output. One line with the number of bytes n values of type double would take.

Constraints. 0 <= n <= 1000000000. Take the size of a double from sizeof, not from a number you typed.

Sample. Input 10 gives 80.

#include <stdio.h>

int main(void)
{
    long long n = 0;
    scanf("%lld", &n);

    /* Multiply by sizeof(double), print with %lld. */

    return 0;
}

Graded as bytes-for-doubles. The largest allowed n gives an answer above 2 billion, so the multiplication has to happen in a wide box.

Run in Compiler
Exercise 3Hard

Zara is designing a column in a file format and needs the narrowest signed fixed-width type that holds every value the column can carry.

Input. One line with two integers lo hi, the smallest and largest value the column may hold.

Output. One line with the width in bits of the smallest of int8_t, int16_t, int32_t and int64_t that holds the whole range: 8, 16, 32 or 64.

Constraints. -9000000000000000000 <= lo <= hi <= 9000000000000000000.

Sample. Input 0 100 gives 8. Input -200 300 gives 16. Input 0 5000000000 gives 64.

#include <stdio.h>

int main(void)
{
    long long lo = 0;
    long long hi = 0;
    scanf("%lld %lld", &lo, &hi);

    /* Lesson 4: a comparison is a 1 or a 0, and two of them multiplied is "both". */

    return 0;
}

Graded as type-fits. No condition and no loop are needed. Work out how many of the three narrow types fit, then turn that count into a width.

Run in Compiler

Common doubts

  • Does sizeof cost anything at run time?

    No. For an ordinary type the compiler replaces it with a number while compiling, so the running program never asks anybody.

  • Why is %zu so ugly?

    z means "the length of a size_t" and u means unsigned. It was added in C99 precisely because size_t has no fixed width, so no existing specifier could be correct everywhere.

  • Should I use int32_t instead of int everywhere?

    No. Use int for ordinary counting, and a fixed-width type when the width is part of a file format, a protocol or a hardware register.

  • Is sizeof the same as the number of useful characters in a name?

    No, and that confusion arrives in Module 10. For an array of characters, sizeof counts the boxes; strlen counts the letters before the end marker.

  • Can a byte ever not be 8 bits?

    Yes, on some digital signal processors, where CHAR_BIT is 16 or 32. You will probably never meet one, and writing CHAR_BIT instead of 8 costs nothing.

Key takeaways

  • sizeof is an operator answered at compile time, in bytes, and it never runs its argument.
  • A type needs parentheses; a variable does not, and sizeof x + 1 is not what it looks like.
  • Its type is size_t, printed with %zu, unsigned and machine sized.
  • <limits.h> and <float.h> hold every boundary value, correct for the machine you are on.
  • long is 8 bytes on the Playground and 4 on Windows; every other size in this module agrees.
  • <stdint.h> gives exact widths for protocols, file formats and hardware.

Next is the Problems lesson: ten graded problems over everything in this module, judged against hidden tests.

End of lesson 5

Mark it done, and your progress moves with you.

Next: Problems: Variables and Types