Variables, Types, and Constants
Last week you saw what a byte holds. This week you give those bytes names and types. A type in C is a promise about two things: how many bytes, and how to interpret them — and the C standard is far looser about the first than most people expect.
- Declare and initialize variables of every basic type, and explain what an uninitialized variable contains.
- Choose between
int,long,unsigned,float,double, andbooldeliberately. - Use
sizeofcorrectly and explain why its result has typesize_t. - Report your machine's actual type limits using
<limits.h>and<float.h>. - Say why
constand#defineare not the same thing, and when to use each.
1Declarations and initialization
A declaration introduces a name and gives it a type:
int count; /* declared, not initialized */
int total = 0; /* declared and initialized */
double average = 0.0;
char grade = 'B';The type comes first, then the name. You can declare several names of the same type at once, though it is usually clearer not to:
int width = 10, height = 20; /* legal */Identifiers
A name may contain letters, digits, and underscores, and may not begin with a digit. C is case-sensitive: total, Total, and TOTAL are three different names. Keywords such as int, return, and while are reserved.
Two conventions worth respecting. Names beginning with an underscore followed by a capital letter, and names containing a double underscore, are reserved for the implementation — do not invent your own. And the standard library has already taken a great many ordinary words, so a global variable called index or time can collide in surprising ways.
The uninitialized variable
This is the first genuinely dangerous thing in C:
int count;
printf("%d\n", count); /* undefined behavior */C does not zero your variables. A local variable that you do not initialize contains whatever bytes happened to be at that memory location — leftovers from a previous function call. The value is not random in any useful sense; it is arbitrary, it may be different on each run, and reading it is undefined behavior.
Why this bites so hard. Uninitialized memory is often zero by accident during early development, so the bug hides. It surfaces later, on a different machine or after an unrelated change, as behavior nobody can reproduce. -Wall catches many cases (-Wmaybe-uninitialized), and week 20 shows how a sanitizer catches the rest. The cheap defence is to initialize every variable at the point of declaration.
2The basic types
Integers
C has one integer type, int, plus modifiers that adjust its size and signedness.
| Declaration | Meaning | Guaranteed minimum range |
|---|---|---|
signed char | smallest integer | −127 … 127 |
unsigned char | non-negative byte | 0 … 255 |
short | at least 16 bits | −32 767 … 32 767 |
int | the natural size | −32 767 … 32 767 |
long | at least 32 bits | −2 147 483 647 … 2 147 483 647 |
long long | at least 64 bits | about ±9.2 × 10¹⁸ |
unsigned int | no negatives, one extra bit of range | 0 … 65 535 |
Read that table carefully. The standard guarantees int can hold only ±32 767. On your laptop it will be 32 bits, but the guarantee is what portable code may rely on — a distinction week 37 turns into a working discipline with <stdint.h>.
signed is the default for int, short, long, so signed int and int are the same type. Plain char is the exception: whether it is signed is implementation-defined, as week 4 noted.
Floating point
| Type | Typical size | Decimal digits of precision |
|---|---|---|
float | 4 bytes | about 7 |
double | 8 bytes | about 15 |
long double | 8, 12, or 16 bytes | platform dependent |
Default to double. A floating-point literal such as 3.14 is already a double; writing 3.14f makes it a float. Modern processors compute in double precision anyway, so float buys memory, not speed, and costs you half your significant digits. Use float when you have millions of values to store or you are on a small embedded target — week 53.
Boolean
C had no boolean type until C99. It has one now:
#include <stdbool.h>
bool ready = true;
bool done = false;Under the hood this is _Bool, and <stdbool.h> provides the friendlier spellings. In C23 bool, true, and false became real keywords and the header is no longer needed — but including it is harmless and keeps the code compiling under C17.
Before C99, and still in a great deal of existing code, truth is expressed with int: zero is false, every other value is true. That rule still governs every if and while you will write, so it is worth internalizing rather than hiding behind bool.
3sizeof and size_t
sizeof reports how many bytes an object or type occupies. It is an operator, not a function, and it is evaluated at compile time.
printf("%zu\n", sizeof(int)); /* parentheses required for a type */
printf("%zu\n", sizeof count); /* optional for an expression */sizeof(char) is 1 by definition, and every other size is a multiple of it. So sizeof answers "how many char-sized units", which on all mainstream machines means bytes.
Why %zu and not %d
sizeof yields a value of type size_t — an unsigned integer type large enough to hold the size of any object. On a 64-bit machine it is typically 64 bits wide, whereas int is 32. Printing it with %d is a mismatch, which is undefined behavior; %zu is the correct specifier.
size_t is worth taking seriously now, because it is everywhere from here on: it is the type of every array index you should use (week 12), the parameter type of malloc (week 19), and the return type of strlen (week 14).
size_t is unsigned. A countdown loop written as for (size_t i = n - 1; i >= 0; i--) never terminates: an unsigned value is always greater than or equal to zero, so when i is 0 and decrements, it wraps to an enormous number. You will meet this again in week 7 and again, painfully, in week 12.
4Knowing your machine: <limits.h> and <float.h>
Rather than guessing what your types can hold, ask. Two headers define the answers as compile-time constants.
| Constant | Meaning |
|---|---|
CHAR_BIT | bits in a char — 8 everywhere you will meet |
INT_MIN, INT_MAX | range of int |
UINT_MAX | largest unsigned int |
LONG_MIN, LONG_MAX | range of long |
LLONG_MAX | largest long long |
FLT_DIG, DBL_DIG | decimal digits of precision |
FLT_EPSILON, DBL_EPSILON | smallest difference from 1.0 that is representable |
DBL_EPSILON deserves a note now and a full treatment in week 7: it is the standard starting point for choosing the tolerance in a floating-point comparison, because it quantifies exactly how much precision you have.
5Literals and escape sequences
A literal is a value written directly in the source. Its type is determined by how you write it, which matters more than beginners expect.
| Literal | Type | Note |
|---|---|---|
42 | int | |
42U | unsigned int | |
42L | long | |
42LL | long long | |
3.14 | double | the default for decimals |
3.14f | float | |
0x2A | int | hexadecimal 42 |
052 | int | octal 42 — leading zero |
0b101010 | int | binary, C23 |
'A' | int in C, not char | a character constant |
"text" | char[5] | four characters plus '\0' |
The type of a literal decides the type of the expression it sits in. 1 / 2 is integer division and gives 0; 1.0 / 2 gives 0.5. This is the most common arithmetic surprise in the language, and week 7 explains the rule behind it.
Escape sequences
| Escape | Character |
|---|---|
\n | newline |
\t | tab |
\\ | backslash |
\' \" | quote characters |
\0 | the null character, value 0 |
\x41 | the character with hex value 41 — 'A' |
6Three ways to say "this never changes"
#define — textual substitution
#define MAX_STUDENTS 100The preprocessor replaces every occurrence of MAX_STUDENTS with the text 100 before the compiler runs. There is no variable, no type, and no memory. It works anywhere, including array sizes and #if conditions.
const — a typed, read-only object
const int max_students = 100;This is a real object with a real type that the compiler refuses to let you assign to. It appears in the debugger, participates in type checking, and obeys scope.
enum — named integer constants
enum { MAX_STUDENTS = 100, MAX_COURSES = 20 };Integer constants with no memory cost, usable where a compile-time constant is required. Week 22 covers enumerations properly.
#define | const | enum | |
|---|---|---|---|
| Type-checked | no | yes | integers only |
| Respects scope | no | yes | yes |
| Visible in a debugger | no | yes | usually |
| Usable as an array size | yes | C99 VLA only | yes |
| Works for non-integers | yes | yes | no |
Prefer const for values with a type, and enum for a family of related integers. Reserve #define for things the other two cannot do — which, before week 27, is mostly array sizes in C89-style code.
A #define is text, and text has no arithmetic. #define HALF 1/2 followed by double x = HALF; gives 0, not 0.5, because the substitution produces integer division. Worse, #define SQUARE(x) x*x makes SQUARE(2+3) expand to 2+3*2+3 = 11. Week 27 explains the parentheses that fix this and why a function is usually better.
7Worked example: a program that describes its own machine
Save as types.c. It prints what your compiler actually does, next to what the standard merely promises.
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <float.h>
#include <stdbool.h>
int main(void)
{
printf("CHAR_BIT = %d (bits per byte on this machine)\n\n", CHAR_BIT);
printf("%-20s %6s %s\n", "type", "bytes", "range on this machine");
printf("%-20s %6zu %d .. %d\n",
"char", sizeof(char), CHAR_MIN, CHAR_MAX);
printf("%-20s %6zu %d .. %d\n",
"short", sizeof(short), SHRT_MIN, SHRT_MAX);
printf("%-20s %6zu %d .. %d\n",
"int", sizeof(int), INT_MIN, INT_MAX);
printf("%-20s %6zu %ld .. %ld\n",
"long", sizeof(long), LONG_MIN, LONG_MAX);
printf("%-20s %6zu %lld .. %lld\n",
"long long", sizeof(long long), LLONG_MIN, LLONG_MAX);
printf("%-20s %6zu 0 .. %u\n",
"unsigned int", sizeof(unsigned), UINT_MAX);
printf("%-20s %6zu\n", "float", sizeof(float));
printf("%-20s %6zu\n", "double", sizeof(double));
printf("%-20s %6zu\n", "long double", sizeof(long double));
printf("%-20s %6zu\n", "bool", sizeof(bool));
printf("%-20s %6zu\n", "size_t", sizeof(size_t));
printf("%-20s %6zu\n", "void *", sizeof(void *));
printf("\nfloat : %d significant decimal digits, epsilon = %g\n",
FLT_DIG, FLT_EPSILON);
printf("double : %d significant decimal digits, epsilon = %g\n",
DBL_DIG, DBL_EPSILON);
puts("\n-- what the standard actually guarantees --");
puts("char at least -127 .. 127");
puts("short at least -32767 .. 32767");
puts("int at least -32767 .. 32767");
puts("long at least -2147483647 .. 2147483647");
puts("long long at least about +/- 9.2e18");
puts("\n-- the classic surprise --");
printf("1 / 2 = %d\n", 1 / 2);
printf("1.0 / 2 = %g\n", 1.0 / 2);
printf("7 / 2 = %d\n", 7 / 2);
printf("7 %% 2 = %d\n", 7 % 2);
return EXIT_SUCCESS;
}gcc -std=c17 -Wall -Wextra -g -o types types.c
./typesWhat to notice
int is 4 bytes but only 2 are guaranteed. The gap between the two halves of the output is the entire subject of week 37. Code that assumes the left column instead of the right is code that breaks when it moves.
long is where portability goes wrong. On 64-bit Linux and macOS it is 8 bytes; on 64-bit Windows it is 4. A great deal of otherwise-correct code has failed on exactly this.
sizeof(void *) tells you the word size. 8 on a 64-bit machine, 4 on a 32-bit one. This is the number that decides how much memory a process can address.
1 / 2 is 0. Both operands are int, so C performs integer division and discards the remainder. Writing 1.0 / 2 makes one operand a double and the whole expression follows. Remember this the next time an average comes out wrong.
An experiment worth running
Add these two lines and rebuild:
int uninitialized;
printf("uninitialized = %d\n", uninitialized);GCC will warn:
types.c:52:5: warning: 'uninitialized' is used uninitialized
[-Wuninitialized]Run it several times, and at different optimization levels. The value may be 0, may be garbage, and may change. That instability is exactly why the language calls it undefined rather than unspecified.
8Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
| Reading an uninitialized variable | Undefined behavior; unreproducible bugs | Initialize at the point of declaration, always. |
Printing sizeof with %d | Undefined behavior; wrong number on 64-bit | Use %zu. |
| Counting down with an unsigned index | Infinite loop | Loop upward, or use a signed type, or restructure the condition. |
Expecting 1 / 2 to be 0.5 | Silent integer division | Make one operand floating point: 1.0 / 2. |
Using float by habit | Seven digits of precision where you needed fifteen | Default to double. |
#define HALF 1/2 | Textual substitution yields integer division | Parenthesize, or use const double half = 0.5;. |
Assuming long is 64 bits | Works on Linux, breaks on Windows | Use long long, or int64_t from week 37. |
9Check yourself
What does an uninitialized local variable contain?
Whatever bytes were already at that memory location — typically leftovers from earlier function calls. It is not zero, not random in any dependable sense, and reading it is undefined behavior. Initialize every variable where you declare it.
Why does sizeof return size_t rather than int?
Because a size cannot be negative and can exceed the range of int on a 64-bit machine. size_t is an unsigned type guaranteed to hold the size of any object. Print it with %zu; using %d is a format mismatch and undefined behavior.
Why does for (size_t i = n - 1; i >= 0; i--) never end?
size_t is unsigned, so i >= 0 is always true. When i reaches 0 and is decremented it wraps to the largest representable value rather than becoming −1. Either loop upward, or write the condition as i-- > 0 using the value before the decrement.
Give one thing const does that #define cannot, and one thing #define does that const cannot.
const has a type, so the compiler type-checks its use, it obeys scope, and a debugger can show it. #define produces a compile-time constant expression usable as a fixed array size in standard C89, where a const int cannot be. That is essentially the only remaining reason to prefer it for plain values.
Your program computes an average as int total / int count and gets 3 instead of 3.5. Why, and what is the minimal fix?
Both operands are integers, so C performs integer division and truncates. The minimal fix is to make one operand floating point — (double)total / count. Casting the result instead, as in (double)(total / count), is too late: the truncation has already happened.
10Where this leads
You can now name and size every basic value. Week 6 puts those values into expressions: what the operators do, how precedence decides the shape of an expression, and why the order in which C evaluates operands is less fixed than it looks. Week 7 then combines weeks 4, 5, and 6 into the single topic that produces more quiet bugs than any other in the language — what happens when types mix.