02 · Variables, Data Types & Operators¶
🎥 Video walkthrough¶
Declaring and initializing variables¶
Unlike dynamically typed languages, C requires you to state a variable's type up front — the compiler uses that type to decide how many bytes to allocate and how to interpret them.
#include <stdio.h>
int main(void) {
int age = 30; // whole numbers
float price = 19.99f; // single-precision floating point
double pi = 3.14159265; // double-precision floating point
char grade = 'B'; // a single character, stored as a small integer
int score; // declared, not yet initialized -- holds garbage until assigned
score = 100;
printf("%d %.2f %f %c %d\n", age, price, pi, grade, score);
return 0;
}
// Output:
// 30 19.99 3.141593 B 100
Reading an uninitialized variable's value before assigning it is undefined behavior in C — the compiler doesn't zero it out for you. Always initialize before use.
Primitive types¶
C's built-in types map fairly directly onto the machine's memory:
int whole = 42;
short small = 100;
long big = 1000000L;
long long huge = 10000000000LL;
unsigned int positive_only = 4000000000u;
float f = 1.5f;
double d = 1.5;
char c = 'x';
unsigned variants store only non-negative numbers, which doubles the top end
of the positive range in exchange for giving up negative values entirely.
sizeof — how big is this type, really?¶
Exact sizes aren't guaranteed by the C standard — they depend on the platform
and compiler — so sizeof lets you ask at compile time instead of assuming:
#include <stdio.h>
int main(void) {
printf("int: %zu bytes\n", sizeof(int));
printf("float: %zu bytes\n", sizeof(float));
printf("double: %zu bytes\n", sizeof(double));
printf("char: %zu bytes\n", sizeof(char));
printf("long: %zu bytes\n", sizeof(long));
return 0;
}
// Output (typical on a 64-bit machine):
// int: 4 bytes
// float: 4 bytes
// double: 8 bytes
// char: 1 bytes
// long: 8 bytes
%zu is the format specifier for size_t, the unsigned integer type
sizeof returns.
Cheat sheet: common types¶
| Type | Typical size | Typical range |
|---|---|---|
char |
1 byte | -128 to 127 (or 0 to 255 if unsigned) |
short |
2 bytes | -32,768 to 32,767 |
int |
4 bytes | -2,147,483,648 to 2,147,483,647 |
unsigned int |
4 bytes | 0 to 4,294,967,295 |
long |
8 bytes (4 on some platforms) | roughly ±9.2 × 10^18 |
long long |
8 bytes | roughly ±9.2 × 10^18 |
float |
4 bytes | ~7 significant decimal digits |
double |
8 bytes | ~15 significant decimal digits |
Treat these sizes as "typical, not guaranteed" — if exact width matters (for
file formats or network protocols), Level 2 covers the fixed-width types like
int32_t from <stdint.h>.
Type casting¶
C sometimes converts types for you (implicit conversion), and sometimes you have to ask for it explicitly.
#include <stdio.h>
int main(void) {
int a = 7;
int b = 2;
// Implicit: both operands are int, so this is integer division
printf("%d\n", a / b); // 3 -- fraction is discarded, not rounded
// Explicit cast: force one operand to double before dividing
printf("%f\n", (double)a / b); // 3.500000
double price = 9.75;
int whole_dollars = (int)price; // explicit cast, truncates toward zero
printf("%d\n", whole_dollars); // 9
return 0;
}
Implicit conversions happen automatically when types mix in an expression
(e.g. int combined with double promotes the int to double first).
Explicit casts, written as (type)value, are how you override the default and
tell the compiler exactly what conversion you want — useful for avoiding
surprises like integer division when you meant real division.
Operators overview¶
#include <stdio.h>
int main(void) {
int a = 10, b = 3;
// Arithmetic
printf("%d %d %d %d %d\n", a + b, a - b, a * b, a / b, a % b);
// 13 7 30 3 1 -- % is remainder, not "percent"
// Relational -- produce 0 (false) or 1 (true)
printf("%d %d %d\n", a > b, a == b, a != b);
// 1 0 1
// Logical
int x = 1, y = 0;
printf("%d %d %d\n", x && y, x || y, !x);
// 0 1 0
return 0;
}
Bitwise operators (brief preview)¶
C also has operators that act directly on the binary representation of
integers: & (AND), | (OR), ^ (XOR), ~ (NOT), << (left shift), and
>> (right shift). These are used heavily for flags, masks, and
low-level tricks:
int flags = 0b0101; // binary literal: 5
int mask = 0b0011; // 3
printf("%d\n", flags & mask); // 1 -- bits set in both
printf("%d\n", flags | mask); // 7 -- bits set in either
printf("%d\n", flags << 1); // 10 -- shift left, multiply by 2
That's just enough to recognize them when you see them — a full treatment, including practical bit-manipulation patterns, is in Level 4.
Integer overflow¶
Integers have a fixed number of bits, so arithmetic that exceeds the type's range wraps around silently instead of raising an error:
#include <stdio.h>
#include <limits.h>
int main(void) {
int max = INT_MAX; // largest value an int can hold
printf("%d\n", max); // 2147483647
printf("%d\n", max + 1); // -2147483648 -- wraps around to the minimum!
return 0;
}
This is a common source of subtle bugs — C will not warn you at runtime.
Choosing a wider type (long, long long) or an unsigned type buys more
headroom but doesn't eliminate the problem, it just moves the boundary.
How It Actually Works¶
Every variable in a C function lives at a fixed offset from the CPU's stack
pointer for the duration of that function's call — the compiler decides these
offsets at compile time, not runtime. Declaring int age = 30; inside main
generates roughly one instruction to reserve 4 bytes below the current stack
frame and another to write the bit pattern for 30 into it (movl $30,
-4(%rbp) in x86-64 assembly). There's no hidden allocator call, no tag
tracking "this is an int" at runtime — the type only exists at compile time,
to tell the compiler how many bytes to reserve and how to interpret them.
After compilation, a float and an int of the same byte count are
indistinguishable bit patterns in memory; only the instructions the compiler
chose to operate on them differ.
That distinction is exactly what makes float/double different from int
at the same size. Both float (4 bytes) and int (4 bytes) occupy identical
storage, but float uses the IEEE-754 encoding — 1 sign bit, 8 exponent
bits, 23 mantissa bits — while int uses two's-complement. Casting
(int)price from 9.75 doesn't just chop off decimal digits: the CPU's
floating-point unit runs a dedicated instruction (cvttsd2si on x86-64) that
decodes the IEEE-754 bit pattern back into an integer bit pattern, truncating
toward zero as it goes — a genuinely different bit-level operation from
integer arithmetic, not the same bits reinterpreted.
Integer overflow is a direct consequence of two's-complement
representation and fixed-width registers. INT_MAX is 0111...1 (31 ones)
in binary. Adding 1 performs ordinary binary addition, which carries
through every bit and flips the sign bit, producing 1000...0 — which
two's-complement interprets as the most negative int. The CPU's adder
doesn't know or care that this is "wrong"; it just did binary addition on
fixed-width registers and let the carry fall off the end. Signed overflow is
undefined behavior in the C standard specifically because different CPU
architectures could technically handle that carry differently, even though
in practice x86/ARM both wrap the same way.
sizeof is resolved entirely at compile time (except for variable-length
arrays) — it's not a function call, it's an operator the compiler evaluates
while generating code, which is why sizeof(int) costs zero CPU cycles at
runtime; the compiler simply substitutes the constant 4 wherever you wrote
it.
🔀 See this in another language¶
Exercise¶
Write a program that declares an int, a float, a double, and a char,
prints the sizeof each one, then performs an explicit cast dividing two
integer variables to get a precise decimal result (not truncated integer
division). Finally, declare an int set to INT_MAX (from <limits.h>) and
print what happens when you add 1 to it.