Module 2 · Variables and Data Types
sizeof and limits.h: Asking the Machine Instead of Guessing
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_BITbits, 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.
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.
| Type | Width | Range | Use it for |
|---|---|---|---|
int8_t | 8 bits | -128 to 127 | a small signed value in a packed structure |
uint8_t | 8 bits | 0 to 255 | a raw byte, a colour channel |
int16_t | 16 bits | -32,768 to 32,767 | an audio sample |
uint16_t | 16 bits | 0 to 65,535 | a port number |
int32_t | 32 bits | about plus or minus 2.1 billion | an ordinary counter that must not change size |
uint32_t | 32 bits | 0 to 4,294,967,295 | an IPv4 address, a checksum |
int64_t | 64 bits | about plus or minus 9.2 quintillion | a 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.
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.
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.
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>.
Where this is used
- The TCP header. Source port and destination port are
uint16_t, the sequence number isuint32_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
libpnguses the<stdint.h>names to do it. - SHA-256. The hash is defined entirely in 32-bit unsigned arithmetic, wrap included. Written with plain
intit would give a different answer on a 16-bit chip, so every implementation usesuint32_t. - Embedded firmware. On an 8-bit microcontroller an
intis 2 bytes, so the same source that ran on a laptop overflows at 32,767. Firmware code usesint16_tandint32_tprecisely 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.
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.
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.
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.
Common doubts
Does
sizeofcost 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
%zuso ugly?zmeans "the length of asize_t" andumeans unsigned. It was added in C99 precisely becausesize_thas no fixed width, so no existing specifier could be correct everywhere.Should I use
int32_tinstead ofinteverywhere?No. Use
intfor ordinary counting, and a fixed-width type when the width is part of a file format, a protocol or a hardware register.Is
sizeofthe same as the number of useful characters in a name?No, and that confusion arrives in Module 10. For an array of characters,
sizeofcounts the boxes;strlencounts the letters before the end marker.Can a byte ever not be 8 bits?
Yes, on some digital signal processors, where
CHAR_BITis 16 or 32. You will probably never meet one, and writingCHAR_BITinstead of 8 costs nothing.
Key takeaways
sizeofis 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 + 1is 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.longis 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