C Variables: Declaring, Typing, Scoping, and Storing Data
Declare and initialize variables of every core C type (int, float, double, char, the signed/unsigned and long variants, and _Bool), understand the difference between declaration/initialization and plain assignment, and reason correctly about variable scope and storage duration.
A program that can't store and manipulate data isn't much of a program — variables are how C (and later, Objective-C) hold onto values while a program runs: a user's age, an account balance, a single character typed on a keyboard. Unlike some languages you may have heard of that let a variable hold 'anything' and figure out the type later, C requires you to state, up front, exactly what kind of value a variable will hold and exactly how much memory to set aside for it. This lesson works through every core C type, how to declare and initialize variables correctly, and two ideas — scope and storage duration — that determine where a variable can be used and how long its value survives.
Declaring a Variable: Type, Name, and Memory
In C, declaring a variable means telling the compiler three things at once: what type of value it will hold, what name you'll use to refer to it, and (implicitly) how much memory to reserve for it. The general form is type name; — for example, int age; declares a variable named age that will hold a whole number, and reserves enough memory (typically 4 bytes on modern systems) to store an int-sized value. At this point, age exists and has memory allocated for it, but that memory holds whatever bits happened to already be there — an unpredictable 'garbage' value — until you explicitly give it one.
This is why C is called a statically typed language: a variable's type is fixed at the moment it's declared and never changes for the lifetime of that variable. If age is declared as an int, it can only ever hold whole numbers for as long as that variable exists — you cannot later store text in it, the way you could reassign a variable in a dynamically typed language. This might feel restrictive coming from other contexts, but it buys you something valuable: the compiler can catch a huge class of mistakes (like trying to do math on text) before the program ever runs, simply because it knows, precisely, what every variable is supposed to hold.
Because an uninitialized variable holds unpredictable garbage, C lets you declare and initialize in a single line: type name = value;. int age = 25; both reserves memory for an int named age and immediately stores the value 25 into it — this is initialization, and it should be your default habit; declaring a variable without giving it a starting value is a common source of subtle, hard-to-reproduce bugs, because the program might appear to work by luck (if the garbage bits happen to look reasonable) and then fail unpredictably somewhere else.
• Declaration form: type name; — states the type and reserves memory; the value is initially garbage. • C is statically typed: a variable's type is fixed at declaration and cannot change afterward. • Initialization form: type name = value; — declares AND gives a starting value in one step; prefer this over bare declaration. • The compiler uses declared types to catch type-mismatch mistakes before the program ever runs.
The Core Numeric Types: int, float, double
int is C's default type for whole numbers (no fractional/decimal part) — things like a count, an age, an array index, or an ID. On essentially all modern desktop and mobile platforms, an int occupies 4 bytes (32 bits) of memory, which lets it represent a signed range of roughly -2,147,483,648 to 2,147,483,647. int age = 25; declares and initializes an integer variable in one step. If a calculation would produce a value outside that range, the result 'overflows' — wraps around unpredictably — rather than raising an error, which is a real hazard to keep in mind when doing arithmetic on large numbers.
float and double both represent numbers with a fractional part — like a temperature, a price, or a measurement — using the IEEE 754 floating-point format. The difference between them is precision and size: float typically occupies 4 bytes and offers roughly 6-7 significant decimal digits of precision, while double ('double-precision') typically occupies 8 bytes and offers roughly 15-16 significant decimal digits. double temperature = 36.5; declares and initializes a double. Unless memory is extremely tight (as it sometimes is in embedded systems), double is the more common default choice in modern C and Objective-C code, because its extra precision avoids a whole category of small, cumulative rounding errors that show up more easily with float.
A crucial, easy-to-miss detail: floating-point numbers cannot represent every decimal value exactly, because they're stored in binary internally, and many decimal fractions (like 0.1) have no exact binary representation — similar to how 1/3 has no exact finite decimal representation. This means you should never compare two floating-point values for exact equality with ==; instead, check whether their difference is smaller than some small tolerance value. This is not a C-specific quirk — it's true of floating-point arithmetic in nearly every programming language — but it trips up beginners in every one of them.
• int — whole numbers, typically 4 bytes, roughly ±2.1 billion range; e.g. int age = 25;. • float — fractional numbers, typically 4 bytes, ~6-7 significant digits of precision. • double — fractional numbers, typically 8 bytes, ~15-16 significant digits of precision; the common default; e.g. double temperature = 36.5;. • Integer arithmetic that exceeds int's range silently overflows/wraps rather than erroring. • Never compare floats/doubles for exact equality with ==; compare that their difference is within a small tolerance.
char, Signedness, and the long Family
char stores a single character, occupying exactly 1 byte of memory — internally, it's actually a small integer type, and a character literal like 'A' is really just the numeric code (65, in ASCII) for that character stored in that byte. char grade = 'A'; declares and initializes a char. Note the single quotes: a single character uses single quotes ('A'), while a sequence of characters (a string) uses double quotes ("A") and is an entirely different kind of thing in C (a string is actually an array of chars, covered in a later module) — mixing up single and double quotes for a single character is a very common beginner mistake.
Every integer type in C (int, char, and the long variants below) can be explicitly marked signed or unsigned. signed (the default if you don't specify) allows negative values by dedicating part of the range to represent them; unsigned gives up the ability to represent negative numbers entirely, in exchange for doubling the maximum positive value the same number of bits can represent. unsigned int score = 100; can never legally hold a negative number — if a calculation would produce a negative result and you store it in an unsigned variable, it wraps around to a very large positive number instead, which is a classic, nasty source of bugs, especially in loop counters that accidentally go negative.
When int's roughly 2-billion range isn't enough, C offers long and long long for progressively larger whole numbers. long is at least as large as int and is commonly 8 bytes on 64-bit systems (though this varies by platform — it's 4 bytes on 64-bit Windows, which is a genuine portability gotcha). long long, guaranteed by the C standard to be at least 8 bytes everywhere, is the more portable choice when you specifically need a very large integer range regardless of platform, such as counting something that could plausibly exceed 2 billion. Both can also be combined with unsigned: unsigned long long is the largest common whole-number type.
• char — a single character, 1 byte, internally a small integer; use single quotes: char grade = 'A';. • signed (default) allows negative values; unsigned cannot hold negatives but doubles the usable positive range. • unsigned arithmetic that goes negative wraps around to a huge positive number instead of erroring — a common bug source. • long — bigger than int, but its exact size varies by platform (a portability gotcha). • long long — guaranteed at least 8 bytes on every standard-conforming platform; the portable choice for very large integers.
_Bool and the Boolean Convention
Older C did not have a true dedicated boolean type at all — programmers conventionally used int, treating 0 as false and any nonzero value as true, a convention that is still extremely common in real C code (and inherited by Objective-C's BOOL type, which you'll meet later in the course). The C99 standard introduced a real boolean type named _Bool, which can hold only the values 0 or 1; assigning any nonzero value to a _Bool variable automatically converts it to 1. Including the standard header <stdbool.h> gives you the friendlier aliases bool, true, and false in place of _Bool, 1, and 0, which is what virtually all modern C code actually uses in practice: #include <stdbool.h> then bool isActive = true;.
It's worth internalizing C's truthiness rule now, because it resurfaces constantly: in an if condition (or any place a boolean is expected), the value 0 (for any numeric type) is treated as false, and every other value is treated as true — there is no separate 'falsy string' or 'falsy empty list' concept the way there is in some other languages; it is purely about whether the numeric value is zero or not. This is exactly the convention Objective-C's BOOL type continues, and exactly why if (someInt) is common, idiomatic C for 'if someInt is not zero'.
Regardless of which numeric type you choose, remember that C fixes a variable's size and interpretation at declaration time based on its type, and every one of these types occupies a specific, predictable number of bytes on a given platform — you can check any type's size on your own system with the sizeof operator, e.g. sizeof(int), which is itself a useful habit whenever you're unsure or need to reason precisely about memory.
• Classic C convention: use int, 0 = false, nonzero = true (still very common in real code). • _Bool (C99): a true boolean type holding only 0 or 1. • #include <stdbool.h> gives the friendlier bool, true, false aliases used in modern code. • Truthiness rule: 0 is false, any nonzero value is true — no separate concept of a 'falsy' string or list. • sizeof(type) reports exactly how many bytes a type occupies on the current platform.
Assignment, Scope, and Storage Duration
Once a variable is declared, = performs assignment: it stores a new value into that variable's existing memory, overwriting whatever was there. age = 26; (no type name this time) reassigns the already-declared age variable — writing int age = 26; a second time in the same scope for the same variable would actually be a compiler error, because you'd be trying to declare age twice. This distinction — declaration/initialization happens once, assignment can happen repeatedly afterward — is one beginners often blur together, so it's worth being deliberate about which one you're doing.
Scope determines where in your source code a variable's name is visible and usable. A variable declared inside a pair of curly braces { } — such as inside main, or inside an if block, or inside a loop — has block scope: it exists only from the point it's declared until the closing } of that block, and referencing it outside that block is a compiler error ('use of undeclared identifier'). A variable declared outside any function, at the top level of a file, has file scope (often called global scope) and is visible to every function later in that file, which is powerful but should be used sparingly — code that leans heavily on global variables becomes hard to reason about, because any function anywhere might be changing that variable's value.
Storage duration is a related but distinct idea: it's about how long a variable's memory actually persists, not just where its name is visible. A variable declared normally inside a function has automatic storage duration — its memory is allocated fresh each time the function is called and destroyed when the function returns, meaning its value does not survive between separate calls to the same function. Marking a variable static inside a function (static int counter = 0;) gives it static storage duration instead: it is allocated once, initialized only the first time that line is reached, and then keeps its value across every subsequent call to that function, for as long as the whole program runs — a distinction that will matter again when you meet Objective-C's own persistent-state patterns later in the course.
• Assignment (name = value;) stores a new value into an already-declared variable; it is not a new declaration. • Block scope: a variable declared inside { } only exists and is only visible within that block. • File/global scope: a variable declared outside every function is visible throughout the rest of that file. • Automatic storage duration (the default inside a function): memory is created fresh per call, destroyed on return. • Static storage duration (static inside a function): initialized once, retains its value across every subsequent call.
Code Examples
Each variable is declared with an explicit type and initialized in the same statement. printf uses a different format specifier per type — %d for int, %f (here %.1f for one decimal place) for double, %c for a single char, %u for unsigned int, and %lld for long long — the specifier must match the variable's type or printf will misinterpret the bytes it's given. Expected result: Age: 25 Temperature: 36.5 Grade: A Score: 100 Population: 8000000000 age is declared once and later reassigned with plain = (no type name). status is declared inside the if-block's braces and only exists between that opening and closing brace -- trying to print status after the block ends would fail to compile with 'use of undeclared identifier'. Expected result: Adult, status=1 Final age: 26 localCount is re-created and reset to 0 every time callCounter runs (automatic storage duration), so it always prints 1. staticCount is initialized to 0 only the very first time the function is called, then keeps its incremented value across every later call (static storage duration), so it counts up across calls. Expected result: local=1 static=1 local=1 static=2 local=1 static=3 int age; printf("%d\n", age);
int age = 0; // or a real starting value, e.g. 25 printf("%d\n", age);
Reading an uninitialized variable's value is reading garbage -- whatever bits happened to already be in that memory. Always initialize a variable when you declare it, rather than relying on a separate assignment later.
char letter = "A";
char letter = 'A';
Double quotes create a string (technically a different type, an array of characters); a single char must use single quotes. Assigning a double-quoted string literal to a char variable is a type mismatch the compiler will flag.
unsigned int remaining = 3 - 5; // intended: -2 int remaining = 3 - 5; // signed, correctly represents -2 unsigned integers cannot represent negative values; a calculation that would be negative wraps around to a very large positive number instead. Use a signed type whenever a value might legitimately go negative.
if (temperature == 36.5) { printf("Normal\n"); } if (temperature > 36.4 && temperature < 36.6) { printf("Normal\n"); }
Floating-point values cannot exactly represent every decimal number due to their binary internal storage, so exact equality comparisons with == are unreliable; compare within a small tolerance range instead.
What happens if you declare int count; and then immediately print it without assigning a value? A. It automatically prints 0 B. It's a compiler error C. It prints whatever unpredictable garbage bits were already in that memory D. It prints -1 by convention
C. It prints whatever unpredictable garbage bits were already in that memory — An uninitialized variable's memory holds whatever bits were already there; C does not automatically zero it out, so its value is unpredictable garbage until you explicitly assign one.
Which type is the most portable choice when you need to guarantee at least 8 bytes of range for a whole number on every standard-conforming platform? A. int B. long C. long long D. char
C. long long — long's exact size varies by platform (notably 4 bytes on 64-bit Windows), while long long is guaranteed by the C standard to be at least 8 bytes everywhere, making it the portable choice for very large integers.
Why should you avoid comparing two double values with == for exact equality? A. == is not a valid operator for doubles in C B. Floating-point numbers can't represent every decimal value exactly in binary, so tiny rounding differences make exact equality unreliable C. Doubles can only be compared using printf D. == always returns false for doubles regardless of their values
B. Floating-point numbers can't represent every decimal value exactly in binary, so tiny rounding differences make exact equality unreliable — Because floating-point values are stored in binary and many decimal fractions have no exact binary representation, small rounding errors accumulate; comparing within a small tolerance is the reliable approach instead of ==.
A variable declared with static int counter = 0; inside a function will: A. Reset to 0 every time the function is called B. Be initialized once and retain its updated value across every subsequent call to that function C. Cause a compiler error because static cannot be used on local variables D. Only be visible from other files
B. Be initialized once and retain its updated value across every subsequent call to that function — static gives a local variable static storage duration: it is initialized only the first time that declaration is reached, and its value persists across every later call to the same function, unlike an ordinary (automatic) local variable.
#include <stdio.h>
int main(void) {
int age = 25;
double temperature = 36.5;
char grade = 'A';
unsigned int score = 100;
long long population = 8000000000LL;
printf("Age: %d\n", age);
printf("Temperature: %.1f\n", temperature);
printf("Grade: %c\n", grade);
printf("Score: %u\n", score);
printf("Population: %lld\n", population);
return 0;
}#include <stdio.h>
int main(void) {
int age = 25; // declaration + initialization
age = 26; // plain assignment: reuses existing age
if (age > 18) {
int status = 1; // block-scoped: only exists inside this if-block
printf("Adult, status=%d\n", status);
}
// status is NOT visible here -- referencing it now is a compiler error
printf("Final age: %d\n", age);
return 0;
}#include <stdio.h>
void callCounter(void) {
int localCount = 0; // automatic: resets to 0 every call
static int staticCount = 0; // static: initialized once, persists
localCount++;
staticCount++;
printf("local=%d static=%d\n", localCount, staticCount);
}
int main(void) {
callCounter();
callCounter();
callCounter();
return 0;
}