C

Variables, Data Types & Operators

A variable is C's basic unit of storage: a named, fixed-size chunk of memory holding one value of one specific type. This chapter covers how to declare one, what types are actually available, how big each one is, and the operators you use to combine and compare them — the vocabulary every single line of C code after this chapter depends on.

Quick reference
What it is
Fixed-size, typed storage locations, plus the operators (arithmetic, relational, logical, bitwise) used to compute with them.
Mental model
A variable is a labeled box of a fixed size; its type is what's printed on the box telling you how to interpret the bits inside — the same 4 bytes mean something completely different labeled int vs. float.
Key numbers
char = 1 byte, int commonly 4 bytes, float = 4 bytes, double = 8 bytes — always platform-dependent; sizeof is the only source of truth.
Reach for it when
Any time a value needs to be stored or computed — the very first design decision inside any function.

Declaring a variable

Declaring a variable in C always states its type first, then its name, optionally followed by an initial value:

int age = 25;       // type: int,    name: age,    value: 25
char grade = 'B';    // type: char,   name: grade,  value: 'B'
float price = 9.99f; // type: float,  name: price,  value: 9.99
double pi = 3.14159265358979; // type: double, name: pi
The declaration int age = 25 reserves 4 bytes of memory, labels that location "age", and stores the bit pattern for 25 in it int age = 25; 25 4 bytes, address 0x1000 name: age — type: int
The compiler reserves the box's size based on the type, and from that point on "age" is just a convenient name for that specific memory address.

Once declared, a variable's type never changes — this is what makes C statically typed. You can change the value stored inside the box, but not what kind of box it is.

The basic data types

TypeStoresTypical sizeExample
charA single character (or small integer)1 bytechar c = 'A';
intA whole number4 bytesint count = 42;
floatA decimal number, single precision4 bytesfloat f = 3.14f;
doubleA decimal number, double precision8 bytesdouble d = 3.14159;
shortA smaller whole number2 bytesshort s = 100;
longA larger whole number8 bytes (platform-dependent)long l = 100000L;

Any of the integer types (char, short, int, long) can be prefixed with unsigned to store only non-negative numbers, doubling the positive range in exchange for losing negative values entirely — unsigned int instead of int. The exact bit-level consequences of that trade-off, and the fixed-width types (uint8_t, int32_t, …) that remove the "typical size" guesswork entirely, are covered properly in Embedded C's Data Types & Integer Overflow — worth reading once you're comfortable here, since exact sizes matter far more on a microcontroller than on a desktop.

sizeof — and why it matters

"Typical size" above is deliberately vague, because C never actually guarantees exact sizes — only minimums. The one way to know for certain, on your specific compiler and target, is the built-in sizeof operator:

#include <stdio.h>

int main(void) {
    printf("int is %zu bytes\n", sizeof(int));
    printf("double is %zu bytes\n", sizeof(double));
    return 0;
}
// int is 4 bytes
// double is 8 bytes

This isn't a pedantic detail — code that assumes int is always 4 bytes has genuinely broken on real, shipped hardware where it wasn't. sizeof is how you stop guessing.

Type conversion & casting

C will sometimes convert one type to another automatically (implicit conversion), and you can always force one explicitly (casting):

int a = 7;
int b = 2;
float result = a / b;          // 3.0 — integer division happened BEFORE the assignment
float fixed  = (float)a / b;   // 3.5 — casting a to float first forces floating-point division

(float)a is an explicit cast: "treat this int as a float from this point forward." Without it, C evaluates a / b using integer division first — both operands are integers, so the result is truncated to an integer before it's ever assigned to result.

Operators

CategoryOperatorsExampleMeaning
Arithmetic+ - * / %a % bRemainder after dividing a by b
Relational== != > < >= <=a == bTrue if a and b are equal
Logical&& || !a > 0 && b > 0True only if both conditions are true
Bitwise& | ^ ~ << >>flags & 0x04Tests bit 2 of flags directly
Assignment= += -= *= /=total += 5Shorthand for total = total + 5

Bitwise operators work on the individual bits of a value rather than its numeric meaning — they look unfamiliar at first, but they're the exact tool used to set, clear, and test individual bits in a hardware register, which is why Embedded C's Registers & volatile chapter leans on them constantly.

flags & 0x04 masks out every bit except bit 2, leaving a nonzero result only if that bit was set flags & 0x04 — testing whether bit 2 is set 0 0 0 0 1 1 0 1 flags 0 0 0 0 0 1 0 0 0x04 (the mask) bit 2 is 1 → result is nonzero (true)
The AND operator keeps a bit only where both inputs have a 1 — everywhere the mask is 0, the result is forced to 0 regardless of flags.

Operator precedence

When an expression mixes several operators, C evaluates them in a fixed order — not strictly left to right. The rules that matter most day to day:

  1. *, /, % bind tighter than +, - — exactly like ordinary arithmetic (2 + 3 * 4 is 14, not 20).
  2. Relational (>, ==) bind looser than arithmetic, so a + b > c means (a + b) > c.
  3. Logical && binds tighter than logical ||.
  4. Bitwise &, ^, | bind looser than relational operators — the single most common precedence mistake in real C code (see Gotchas).
  5. When genuinely unsure, add parentheses — they cost nothing at runtime and remove all ambiguity for the next reader, including future you.

Real-world applications

Gotchas

Field notes