LearnLearn CC Foundations

Types and sizes

sizeof - a hands-on Linux lab on a real virtual machine.

Every C variable is a box with a fixed size: on this 64-bit machine char is 1 byte, short 2, int 4, long 8, and a pointer 8, all measurable with sizeof (char is the only size C guarantees, because sizeof counts in units of char). A char is a small integer in costume, and the classic wrong turn, char letter = "A"; with double quotes, draws the int-conversion warning and stores one garbage byte of the string address. const locks a value at compile time, and INT_MAX from limits.h names the 4 byte ceiling of an int. Add 1 to INT_MAX and this machine wraps to -2147483648 with no message, a result the C standard calls undefined behavior. The fix is a wider type chosen up front, never trust in the wrap.

You can already compile and run a C program; that was the Hello, machine lesson. So here is a mystery worth your new skill. A tiny program stores the biggest value an int can hold, prints it, adds 1, and prints again. This is the real output from the lab machine:

INT_MAX     = 2147483647
INT_MAX + 1 = -2147483648

Add 1 to the biggest number and the result is not bigger. It is negative, and it is enormous. No error was printed. The program did not crash. It answered with the wrong sign, quietly, and kept going.

This is not a toy problem. In 2014 the view count on YouTube's most watched video raced toward 2,147,483,647, the ceiling of the type the counter was stored in, and YouTube had to move the count into a bigger type. That is the same fix you will make yourself before this lesson ends.

By the end you will know what that ceiling is and why crossing it flips the sign. You will also measure the size of any type with sizeof, and know which type to reach for when int is too small.

The black panels below are a practice terminal: a safe sandbox that checks the one command each step teaches. Every output line was captured from the real Learn C lab machine, Debian 12 with gcc 12.2.0. Type sizes are for this 64-bit machine, and the lesson flags anything that can differ elsewhere.

Your machine's memory is one long row of bytes. A byte is 8 bits, and a bit is a single on-or-off switch. Everything a program remembers lives somewhere in that row.

A variable is a named box in that row. In C, every variable has a type, and the type answers two questions the machine cannot guess: how many bytes does the box take, and how should those bytes be read.

Think of renting a storage unit. You pick the size up front, the size never changes while your things are inside, and nothing bigger than the unit will ever fit. C types work the same way: you pick the box when you declare the variable, and the box never grows.

The basic boxes:

Declaring a variable is the type, then the name, then the starting value:

int count = 0;

Two more players join in this lesson. const is a keyword that locks a box after you fill it. sizeof is the measuring tape: it tells you how many bytes any type or variable takes.

Some languages hide all of this. In them, a number is just a number, and it silently grows as needed. C hides nothing, because C's whole job is to hand you the machine as it really is. That honesty is why Linux, gcc, and most of the tools you met in Linux Foundations are themselves written in C.

A fixed box buys speed. The processor adds two 4 byte integers in a single hardware step, no size checks, no growing. A fixed box also has a fixed ceiling, and the cold open showed you what crossing that ceiling looks like.

In real work the choice matters daily. Loop counters and exit codes fit comfortably in int. Byte counts of large files, global view counters, and nanosecond timestamps outgrow int and need long. Text moves through char. Measurements with fractions ride in float or double.

One box gets only a glance today: the pointer, a variable that holds an address in memory. Pointers get their own lesson soon. One appears here for a single reason: it has a size too, and you are about to measure it.

You never have to memorize sizes, because C will tell you. sizeof takes a type or a variable and answers in bytes. Here is a complete program that measures five types:

#include <stdio.h>

int main(void) { printf("char=%zu short=%zu int=%zu long=%zu ptr=%zu\n", sizeof(char), sizeof(short), sizeof(int), sizeof(long), sizeof(int *)); return 0; }

You know printf from Hello, machine: it prints the text you hand it. The new part is the placeholders. Each %zu is a slot, and each sizeof result drops into the next slot in order. %zu is the placeholder for the kind of unsigned number sizeof produces.

The last measurement, sizeof(int *), measures a pointer to an int: not the int itself, but the address of one. Predict before you compile.

The && in the command below is the chaining you learned in Linux Foundations: run the second command only if the first one succeeded. So ./sizes runs only when the compile is clean.

prompt: operator@camp:~$ answer: gcc -Wall sizes.c -o sizes && ./sizes output: char=1 short=2 int=4 long=8 ptr=8 hint: Compile with warnings on, then run the binary, in one line: gcc -Wall sizes.c -o sizes && ./sizes

One line, five measurements. char=1 is the only number the C language guarantees, and the guarantee is by definition, not by luck: sizeof answers in units of char, so asking the size of a char is asking how many chars fit inside a char. One, on every machine ever made, the way a ruler measures itself as exactly one ruler long. The other types get no such promise. The C standard only demands that each type can hold a minimum range of values, and every machine picks the byte counts its processor is fastest at. This machine chose short 2 bytes, int 4, long 8, and 8 byte pointers. A small embedded controller can choose a 2 byte int and a 2 byte pointer and still be perfectly legal C. That is why working engineers measure with sizeof instead of assuming, and why this lesson keeps saying: on this machine.

Here is a secret that unlocks a lot of C: the machine cannot store letters. It stores numbers, and everyone agrees on a table that maps numbers to characters. The table is called ASCII, and in it the number 65 means A, 66 means B, and so on.

A char is therefore just a 1 byte integer. Write the character in single quotes, like 'A', and C stores the number 65. Single quotes mean exactly one character. Double quotes are for whole strings, which get their own lesson later.

#include <stdio.h>

int main(void) { char letter = 'A'; printf("letter = %c, as a number = %d\n", letter, letter); printf("letter + 1 = %c\n", letter + 1); return 0; }

Line 5 prints the same variable twice with two different placeholders: %c shows it as a character, %d shows it as a plain whole number. Line 6 does arithmetic on it.

prompt: operator@camp:~$ answer: gcc -Wall letter.c -o letter && ./letter output: letter = A, as a number = 65 letter + 1 = B hint: Same shape as before: gcc -Wall letter.c -o letter && ./letter

The first line shows both costumes at once: the byte holds 65, %c renders it as A, %d shows the raw number. The second line proves that math on characters is ordinary C. This one idea, characters are numbers, powers real tools: it is how programs convert lowercase to uppercase, and how sorting puts names in order.

Now the classic wrong turn, the one almost everyone takes once. If single quotes work, surely double quotes do too. Here is letter.c shrunk to its core, with exactly one change: the A is in double quotes.

#include <stdio.h>

int main(void) { char letter = "A"; printf("as a number = %d\n", letter); return 0; }

prompt: operator@camp:~$ answer: gcc -Wall quotes.c -o quotes && ./quotes output: quotes.c: In function 'main': quotes.c:4:19: warning: initialization of 'char' from 'char *' makes integer from pointer without a cast [-Wint-conversion] 4 | char letter = "A"; | ^~~ as a number = 4 hint: Same shape as always: gcc -Wall quotes.c -o quotes && ./quotes

Read the warning: makes integer from pointer without a cast. A pointer is an address, and gcc is telling you that "A" produced one and you stuffed it into a number box. Then look at the run: the 4 is not A, not 65, not anything. It is the lowest byte of that address, chopped off to fit the 1 byte char. Edit the file in any way and rebuild, and a different byte can land there; this build happened to print 4. The one sentence that resolves it: single quotes make a small number, double quotes make a string and hand you its address, and a char is a box for the number. When a char prints as a tiny wrong value and gcc mentions int-conversion, check the quote marks first.

Back to the mystery from the cold open. An int is 4 bytes, which is 32 bits, which is 32 on-or-off switches. That allows 4,294,967,296 different patterns. An int must also represent negative numbers, so half the patterns are spent below zero. The result is a hard range: -2,147,483,648 up to 2,147,483,647.

C publishes these limits in a header file named limits.h. Include it and you get INT_MAX, the biggest possible int, as a ready-made name. Here is the cold open program in full:

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

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

prompt: operator@camp:~$ answer: gcc -Wall overflow.c -o overflow && ./overflow output: INT_MAX = 2147483647 INT_MAX + 1 = -2147483648 hint: One line, compile then run: gcc -Wall overflow.c -o overflow && ./overflow

Think of an old car odometer showing 999999. Drive one more mile and it reads 000000, because there is no seventh wheel. The int did the same thing. All the value bits were as high as they could go, and the +1 carried into the very last bit, the one that marks the sign. The pattern left behind means -2147483648 in the signed number scheme processors use, called two's complement. You do not need the scheme's details yet. You need the shape of the event: cross the ceiling, land at the floor.

Here is the variable's actual memory on the lab machine, byte by byte, before and after the addition:

{ "kind": "memory", "cells": [ { "addr": "0x7ffd5e189ee4", "bytes": "ff ff ff 7f", "label": "big = INT_MAX (2147483647)" }, { "addr": "0x7ffd5e189ee4", "bytes": "00 00 00 80", "label": "big after + 1 (-2147483648)" } ] }

Read each row right to left. This machine stores the least important byte first, a layout called little-endian. Right to left, the before row reads 7f ff ff ff, which is 2147483647 written in hexadecimal. The after row reads 80 00 00 00. The +1 rippled a carry through every byte and landed in the sign bit.

The address 0x7ffd5e189ee4 is real, captured on the lab machine. Yours will differ on every single run: Linux deliberately randomizes where a program's memory sits each time it starts, a defense called ASLR. The bytes are the lesson here, not the address.

Now the fine print that separates C from every other language you will meet. The C standard does not define what happens when signed integer math overflows. The behavior is undefined: the compiler is allowed to assume it never happens, and to optimize your code based on that assumption. On this machine, with gcc 12 and these flags, the value wrapped, and you watched it. Another compiler, another version, or another optimization level can make the same line behave differently. Treat the wrap as an observation, never a promise. The real fix is to never overflow at all: give the value a bigger box.

Here is overflow.c again with exactly three lines changed. Line 5 gives big an 8 byte box, and lines 6 and 8 swap the placeholder to %ld, the one that prints a long. The highlighted lines are the whole diff:

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

int main(void) { long big = INT_MAX; printf("INT_MAX = %ld\n", big); big = big + 1; printf("INT_MAX + 1 = %ld\n", big); return 0; }

prompt: operator@camp:~$ answer: gcc -Wall bigger.c -o bigger && ./bigger output: INT_MAX = 2147483647 INT_MAX + 1 = 2147483648 hint: The fixed program compiles and runs the same way: gcc -Wall bigger.c -o bigger && ./bigger

Same numbers, same addition, correct answer. INT_MAX is nowhere near the ceiling of an 8 byte long, which on this machine tops out above 9.2 quintillion. The working rule: if a value could ever pass 2 billion, think byte counts of big files, global counters, nanosecond timestamps, reach for long from the start. And keep the placeholder honest. Printing a long with %d is a bug, and gcc -Wall calls out the mismatch at compile time. That is one more reason -Wall rides along on every compile in this camp.

Some values should be set once and never move: a speed limit, a buffer size, a retry cap. Put const in front of the type and C locks the box after the first fill.

#include <stdio.h>

int main(void) { const int speed_limit = 55; speed_limit = 70; printf("limit = %d\n", speed_limit); return 0; }

Delete line 5 and the program compiles clean and prints limit = 55. Make const your default for any value that should never drift. It costs nothing when the program runs, and it turns a whole class of bugs into compile errors you cannot miss.

Three outputs you will meet while working with types. Two are loud. The third is the dangerous one, because it says nothing.

1. The missing semicolon. You will hit this constantly while typing the programs in this track. Captured from this exact machine:

broken.c: In function 'main':
broken.c:5:13: error: expected ';' before '}' token
    5 |     return 7
      |             ^

It means a statement was never finished. gcc points the caret at the spot where it expected the semicolon, usually the end of the line it shows. Check the line under the caret first, then the line above it. No binary is produced until this is fixed.

2. Segmentation fault. Some members predicted the overflow program would crash. It did not, but real crashes exist, and this is their signature:

Segmentation fault
exit code: 139

It means the program touched memory it does not own, so the kernel killed it. The exit code is 139 because the kernel ended it with signal 11, and 128 + 11 = 139. Your shell may print job details in front of the words. You will learn to cause and fix these in the pointers lesson. For today, know one thing: integer overflow does NOT print this. Overflow is quieter, and meaner.

3. The silent wrong number. The worst error message in C is no message at all:

INT_MAX + 1 = -2147483648

No warning while the program runs, no crash, and a wrong value flowing into everything downstream. When a number that should be large turns up negative, or a counter suddenly jumps to a huge negative value, check the type first. If the value can pass 2,147,483,647, an int cannot hold it. Widen it to long.

Scaffolding off. No code panel this time. You write the whole file.

Create width.c: a complete C program that prints exactly one line, long=8, by measuring a long with sizeof, not by typing the number 8. You know every piece: the include line for printf, a main, one printf with a %zu slot, and sizeof(long) filling it. Then compile it with warnings on into a binary named width and run it, all in one line.

prompt: operator@camp:~$ answer: gcc -Wall width.c -o width && ./width output: long=8 hint: The compile-and-run shape is gcc -Wall width.c -o width && ./width

The heart of your file is one line: printf("long=%zu\n", sizeof(long)); and the rest is the main scaffold you have seen in every program today. If your output said long=8, you measured the machine instead of trusting a table, and that habit is the real skill. Any time you wonder how big a type is, on any machine, you now have a program a few lines long that answers in seconds.

You earned the cheat sheet. Every row is something you measured, broke, or fixed yourself:

Practice Types and sizes in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.