Variables & Data Types
The labeled boxes where C stores all information. Every piece of data you work with lives in a variable — but in C, you must tell the compiler exactly what kind of data it holds.
What is a variable?
Imagine you have a set of labeled boxes on a shelf. Each box has a sticker on the front (the variable name) and holds something inside (the value). In C, you must also write on the box exactly what type of thing it can hold — a whole number, a decimal number, a single letter, and so on. You cannot put a decimal number in a "whole numbers only" box.
In Python and Java, you have likely written things like x = 5 or int x = 5. In C, it works very similarly — but there are important differences you must understand.
C is statically typed. This means every variable must have its type declared before you use it — and that type cannot change. In Python, you can write x = 5 and then later x = "hello" — Python lets you switch types freely. C does not. Once you say a variable holds an integer, it will always hold an integer.
Why does C do this? Because C compiles directly to machine code that runs on the CPU. The CPU needs to know exactly how many bytes to set aside in memory for each variable. An integer takes 4 bytes, a character takes 1 byte, and a decimal number takes 8 bytes. Knowing the type up front means C can lay out memory perfectly efficiently.
You declare a variable with this pattern: type, then name, then semicolon. You can also give it an initial value immediately. That is called initialization.
# Python — no type needed
x = 42
name = "Alice"
height = 1.75
x = "changed" # totally fine
// Java — type declared
int x = 42;
String name = "Alice";
double height = 1.75;
/* C — type always required */
int x = 42;
char name[6] = "Alice";
double height = 1.75;
/* x = "changed"; would be a
COMPILE ERROR in C */
Java has String. Python has str. C has neither. Strings in C are arrays of characters ending with a special null character \0. You will learn all about this in Topic 09 — for now, just be aware there is no String keyword in C.
Where do variables live? Each variable you declare inside a function lives on the stack — a region of memory automatically managed by C. When your function returns, all its local variables are automatically erased. You will explore the stack in depth in Topic 12 (Memory Management).
Variables declared outside all functions are called global variables. They live for the entire life of the program and are accessible from anywhere in the code.
A variable is a named slot in memory. In C, you must declare the type so the compiler knows how big the slot needs to be. The type never changes. That is it — you just understood the foundation of C.
Declaring and initializing variables
Here is the annotated syntax for the two most common patterns — declaration only, and declaration with initialization:
int age; ^ ^ ^ | | └── semicolon: every C statement ends with this — don't forget it! | └────── the variable name — you choose this (must start with a letter or _) └──────────── the type — tells C this variable holds a whole number (4 bytes) int age = 25; ^ ^ ^ ^ ^ | | | | └── semicolon | | | └────── the initial value stored in the variable | | └─────────── assignment operator — stores the value into the variable | └──────────────── variable name └─────────────────────── type double price = 9.99; // double: 8-byte decimal number char grade = 'A'; // char: a single character, always in single quotes float pi = 3.14f; // float: 4-byte decimal (the 'f' suffix marks it as float)
;. (3) Characters use single quotes 'A', not double quotes. (4) If you declare without initializing, the value is garbage — whatever bytes happened to be in that memory location. Always initialize.
All C Data Types — Quick Reference
| Type | Size (typical) | What it stores | Range / Example | Usage |
|---|---|---|---|---|
char |
1 byte |
A single ASCII character or small integer | char c = 'A'; |
Characters, small counters |
unsigned char |
1 byte |
Positive-only small integer | 0 to 255 |
Raw bytes, pixel values |
short |
2 bytes |
Small whole number | −32,768 to 32,767 |
Memory-constrained situations |
int |
4 bytes |
Whole number (the go-to type) | −2,147,483,648 to 2,147,483,647 |
Counters, indices, most integers |
unsigned int |
4 bytes |
Positive-only whole number | 0 to 4,294,967,295 |
Sizes, counts (never negative) |
long |
8 bytes (64-bit) |
Large whole number | ±9.2 × 10¹⁸ |
File sizes, large counters |
float |
4 bytes |
Decimal number (less precise) | float x = 3.14f; |
When memory matters more than precision |
double |
8 bytes |
Decimal number (more precise) | double x = 3.14159; |
Default for floating-point (prefer this) |
The sizes above are typical for a 64-bit Linux/macOS system — which is what you use in this course. But strictly speaking, C's standard only guarantees minimum sizes. On some embedded platforms an int might be 2 bytes. Solution: include <stdint.h> and use int32_t (exactly 32 bits), int64_t, uint8_t etc. when you need a guaranteed size.
Signed vs Unsigned — what does it mean?
By default, all integer types in C are signed — they can hold both positive and negative values. The top bit of the byte is used as a "sign bit" (0 = positive, 1 = negative).
Adding unsigned removes the sign bit, doubling the positive range. Use unsigned when you know a value can never be negative (array sizes, counts, etc.). This matters because mixing signed and unsigned in comparisons causes subtle bugs — a topic covered in depth in Topic 30 (Security).
The sizeof operator
sizeof(type) tells you exactly how many bytes a type or variable takes on your machine. It returns a value of type size_t (which you print with %zu). Use it whenever you need to know the byte size at runtime — never hard-code sizes.
size_t s = sizeof(int); // s == 4 on 64-bit printf("%zu\n", sizeof(double)); // prints 8 int x = 42; printf("%zu\n", sizeof(x)); // also 4 — sizeof works on variables too // ^ always use %zu (size_t format) not %d for sizeof results
Complete programs you can compile and run
#include <stdio.h> /* Include standard I/O so we can use printf */
int main(void) { /* int main: the function that runs when the program starts */
/* void: this main takes no command-line arguments */
/* --- DECLARATION ONLY (value is garbage until assigned!) --- */
int age; /* declares a slot for a whole number called 'age' */
age = 20; /* now we store 20 into it */
/* --- DECLARATION WITH INITIALIZATION (preferred) --- */
double height = 1.75; /* decimal number, initialized immediately */
char grade = 'B'; /* a single character — note single quotes */
int score = 95;
/* --- PRINTING --- */
printf("Age: %d\n", age); /* %d = print an integer */
printf("Height: %f\n", height); /* %f = print a double */
printf("Grade: %c\n", grade); /* %c = print a character */
printf("Score: %d\n", score);
/* --- REASSIGNMENT --- */
score = 100; /* store a new value — old value (95) is gone forever */
printf("New score: %d\n", score);
/* --- sizeof --- */
printf("int is %zu bytes\n", sizeof(int)); /* 4 */
printf("double is %zu bytes\n", sizeof(double)); /* 8 */
printf("char is %zu bytes\n", sizeof(char)); /* 1 */
return 0; /* 0 means the program ran successfully */
}
Height: 1.750000
Grade: B
Score: 95
New score: 100
int is 4 bytes
double is 8 bytes
char is 1 bytes
#include <stdio.h>
#include <stdint.h> /* For portable exact-width types: int32_t, uint8_t, etc. */
int main(void) {
/* --- SIGNED vs UNSIGNED --- */
int signed_num = -42; /* can hold negative numbers */
unsigned int pos = 4294967295u; /* u suffix = unsigned literal */
printf("Signed: %d\n", signed_num);
printf("Unsigned: %u\n", pos); /* %u = unsigned int format */
/* --- OVERFLOW DEMO (signed vs unsigned wrap) --- */
/* What happens when unsigned goes below 0? It wraps around! */
unsigned int wrap = 0;
wrap = wrap - 1; /* subtracting 1 from 0 wraps to 4294967295 */
printf("0 - 1 (unsigned): %u\n", wrap); /* prints 4294967295 */
/* --- PORTABLE EXACT-WIDTH TYPES --- */
int32_t a = 2147483647; /* exactly 32 bits, signed — always! */
uint8_t b = 255; /* exactly 8 bits, unsigned (0..255) */
int64_t c = 9000000000LL;/* exactly 64 bits — note the LL suffix for large literals */
printf("int32_t max: %d\n", a);
printf("uint8_t max: %hhu\n", b); /* %hhu = unsigned char format */
printf("int64_t big: %lld\n", c); /* %lld = long long format */
return 0;
}
Unsigned: 4294967295
0 - 1 (unsigned): 4294967295
int32_t max: 2147483647
uint8_t max: 255
int64_t big: 9000000000
C99 allows int arr[n]; where n is a variable. Do not use this in COMP2017. The stack is small. If n depends on user input, a large value will crash the program. Use malloc() instead (Topic 12). You will lose marks for VLAs in assessments. Use the -Wvla compiler flag to catch them: gcc -Wvla yourfile.c -o yourfile
Practice problems from lectures, tutorials & exam
What does the following C program print? Explain each output line.
#include <stdio.h>
int main(void) {
char c = 'Z';
int n = c;
printf("%c\n", c);
printf("%d\n", c);
printf("%d\n", n);
return 0;
}
/* OUTPUT:
Z
90
90
*/
char is just a small integer. The character 'Z' is stored as its ASCII value, which is 90.Line 1:
printf("%c\n", c) — the %c format specifier tells printf to interpret the value as a character and print its symbol → prints Z.Line 2:
printf("%d\n", c) — the %d format specifier treats the same value as a decimal integer → prints 90 (the ASCII code of 'Z').Line 3:
int n = c copies the value 90 into an int. %d prints 90.Key takeaway:
char and small integers are interchangeable in C at the hardware level. The format specifier you use in printf controls how the value is displayed.
The following code has three errors. Identify each error and write the corrected version.
#include <stdio.h>
int main(void) {
int x = 3.14; /* line 1 */
char letter = "A"; /* line 2 */
int count /* line 3 */
printf("%d %c %d\n", x, letter, count);
return 0;
}
#include <stdio.h>
int main(void) {
double x = 3.14; /* Fix 1: use double, not int, for a decimal */
char letter = 'A'; /* Fix 2: single quotes for a char, not double */
int count = 0; /* Fix 3: missing semicolon + always initialize */
printf("%lf %c %d\n", x, letter, count);
return 0;
}
int x = 3.14 — an int cannot hold a decimal value. The .14 part is silently truncated, so x becomes 3. Fix: use double x = 3.14.Error 2:
char letter = "A" — double quotes create a string (an array of characters), not a single character. A char must be assigned with single quotes: 'A'.Error 3:
int count — missing the semicolon ; that must end every C statement. Also, count is uninitialized, meaning it holds a garbage value. Always initialize to a known value.
Write a C program that reads a Fahrenheit temperature as an integer from the user, converts it to Celsius using the formula C = (F − 32) × 5 / 9, and prints the result as an integer.
#include <stdio.h>
int main(int argc, char **argv) {
int ftemp; /* declare the variable BEFORE using it */
printf("Please enter a fahrenheit temperature: ");
/* scanf reads from the keyboard. &ftemp is the ADDRESS of ftemp —
scanf needs the address so it can write the value directly into
the variable. You will learn WHY this is needed in Topic 08 (Pointers). */
scanf("%d", &ftemp);
/* Integer arithmetic: multiply before dividing to reduce truncation error.
(ftemp - 32) * 5 / 9 — note: parentheses first, then * left-to-right */
printf("%d fahrenheit is %d centigrade\n",
ftemp, (ftemp - 32) * 5 / 9);
return 0;
}
1. We use
int ftemp because the problem says "integer". If we wanted decimal precision, we would use double.2.
scanf("%d", &ftemp) — the & gives scanf the address of the variable. This is a preview of pointers — scanf needs to know where to write the value in memory. Don't worry about the details yet; just remember the pattern: when reading with scanf, always put & before the variable name (except for strings/arrays).3. The formula multiplies by 5 first, then divides by 9. This is important because integer division discards the remainder — if we divided first we would lose precision.
The following loop is intended to count down from 5 to 0. It runs forever instead. Why, and how do you fix it?
#include <stdio.h>
int main(void) {
unsigned int i;
for (i = 5; i >= 0; i--) {
printf("%u\n", i);
}
return 0;
}
#include <stdio.h>
int main(void) {
/* FIX: use signed int so i can go negative and the loop can exit */
int i;
for (i = 5; i >= 0; i--) {
printf("%d\n", i);
}
return 0;
}
/* Output: 5, 4, 3, 2, 1, 0 — then stops correctly */
unsigned int can never be negative. When i reaches 0 and the loop does i--, instead of becoming -1 and failing the i >= 0 check, it wraps around to 4,294,967,295 (the largest unsigned int). This is called unsigned underflow or unsigned wraparound. The condition i >= 0 is always true for an unsigned int (0 is always ≥ 0), so the compiler may even warn you about this.Fix: Change
unsigned int i to int i. A signed int can go to -1, which fails the i >= 0 check correctly and stops the loop.Rule: Never use
unsigned for loop counters that count down to zero.
What is the expected output of the following code? This question is worth 1 mark.
void foo(int x) {
if (x < 5) {
static int y = 5; /* static local variable */
x = y + x;
printf("%d, ", x);
y += 1;
}
}
int main(void) {
for (int i = 7; i >= 0; i--)
foo(i);
return 0;
}
/* OUTPUT:
9, 9, 9, 9, 9,
*/
static local variables:A
static local variable is initialized once at program start (not re-created on each call) and its value persists between calls. Here, static int y = 5 means y starts at 5 and carries its updated value into every subsequent call to foo.Step 1 — Identify which calls enter the
if:The loop runs
i = 7, 6, 5, 4, 3, 2, 1, 0. The body of foo only executes when x < 5, so foo(7), foo(6), and foo(5) do nothing. The five calls that produce output are foo(4) through foo(0).Step 2 — Execution trace:
| Call # | x passed | y at entry | x = y + x | output | y after call |
|---|---|---|---|---|---|
| 1 | 4 | 5 | 5 + 4 = 9 | 9, | 6 |
| 2 | 3 | 6 | 6 + 3 = 9 | 9, | 7 |
| 3 | 2 | 7 | 7 + 2 = 9 | 9, | 8 |
| 4 | 1 | 8 | 8 + 1 = 9 | 9, | 9 |
| 5 | 0 | 9 | 9 + 0 = 9 | 9, | 10 |
x decreases by 1 while y increases by 1 — the two changes cancel exactly, so y + x is always 9.Output:
9, 9, 9, 9, 9,
Integer Literals & Memory Representation
C lets you write integer constants in four number bases. The base you choose is purely for human convenience — the compiler produces the same machine code either way. But the syntax matters: get it wrong and you silently get the wrong value.
Binary, Hex, Octal & Decimal C Literal Syntax
| Base | Prefix / Convention | Example literal | Decimal value | Notes |
|---|---|---|---|---|
Decimal |
none |
int x = 42; |
42 |
The default. No prefix. |
Hexadecimal |
0x or 0X |
int x = 0xFF; |
255 |
Digits 0–9, A–F. Common for addresses, bitmasks, colour values. |
Hexadecimal |
0x |
int x = 0xDEADBEEF; |
3735928559 |
Classic debug sentinel value. Fits in a 32-bit unsigned int. |
Octal |
leading 0 |
int x = 0755; |
493 |
Used for Unix file permissions (chmod). A lone leading zero makes it octal — not decimal! |
Binary |
0b or 0B |
int x = 0b1010; |
10 |
C99+ GCC extension. Digits 0–1 only. Useful for bitmasks. Not in the C standard before C23 — prefer hex in portable code. |
Writing int x = 010; looks like ten, but it is octal 10 = decimal 8. This is one of C's oldest traps. Never pad integers with leading zeros unless you actually mean octal. The compiler will not warn you — it silently does the wrong thing.
#include <stdio.h>
int main(void) {
int a = 255; /* decimal */
int b = 0xFF; /* hex */
int c = 0377; /* octal */
int d = 0b11111111; /* binary (GCC extension) */
/* All four variables hold the same value: 255 */
printf("%d %d %d %d\n", a, b, c, d); /* 255 255 255 255 */
/* printf can print in different bases too */
printf("hex: %x octal: %o decimal: %d\n", a, a, a);
/* hex: ff octal: 377 decimal: 255 */
/* The octal gotcha */
int x = 010; /* THIS IS 8, NOT 10 */
printf("010 in decimal is: %d\n", x); /* prints 8 */
return 0;
}
hex: ff octal: 377 decimal: 255
010 in decimal is: 8
Two's Complement — How Negative Integers Are Stored
Signed integers in C use two's complement encoding. The key insight is that -1 is stored as all bits set to 1. For a 32-bit int, that is 0xFFFFFFFF. Two's complement is why unsigned arithmetic wraps rather than going negative — there is no separate encoding for negatives; it is all in how the bits are interpreted.
The practical consequences you need to know right now:
int x = -1; /* stored in memory as: 1111 1111 1111 1111 1111 1111 1111 1111 */ /* = 0xFFFFFFFF */ /* Reinterpret those same bits as unsigned: */ printf("%u\n", (unsigned int)-1); /* prints: 4294967295 (2^32 - 1) */ int y = -128; /* for an int8_t: stored as 1000 0000 = 0x80 */ int z = 127; /* for an int8_t: stored as 0111 1111 = 0x7F */
Endianness — Byte Order in Memory
When you store a multi-byte integer like int x = 0x12345678;, the four bytes (0x12, 0x34, 0x56, 0x78) must go into four consecutive memory addresses. The question is: which byte goes first?
- Little-endian — lowest byte at the lowest address. Used by x86/x64 (your laptop, the COMP2017 servers).
- Big-endian — highest byte at the lowest address. Used by network protocols (TCP/IP), some embedded chips.
This usually does not matter within a single program on one machine. It matters when you read a binary file written on a different architecture, or send integers over a network — you must convert between host byte order and network byte order.
Memory layout of int x = 0x12345678; on a little-endian (x86) machine:
| Address | Byte stored | Role |
|---|---|---|
0x100 |
0x78 |
Least-significant byte (stored first on little-endian) |
0x101 |
0x56 |
|
0x102 |
0x34 |
|
0x103 |
0x12 |
Most-significant byte (stored last on little-endian) |
Cast a multi-byte integer to unsigned char * and print each byte — you will see the little-endian order directly. This technique is used in binary file parsing and network code. Full theory and conversion functions (htonl, ntohl) are covered in Topic 32 — Number Systems.
IEEE 754 — How float and double Work in C
C's float (32-bit) and double (64-bit) types use the IEEE 754 floating-point standard. IEEE 754 stores numbers as a sign bit, an exponent, and a fraction (mantissa). This representation is an approximation — most decimal fractions cannot be represented exactly in binary, just as 1/3 cannot be written exactly in decimal.
The most famous consequence: 0.1 + 0.2 does not equal 0.3 in C.
#include <stdio.h>
#include <math.h> /* for fabs() — compile with -lm */
int main(void) {
double a = 0.1;
double b = 0.2;
double c = 0.3;
/* BAD: never compare floats with == */
if (a + b == c) {
printf("equal\n"); /* this line is NEVER reached */
} else {
printf("not equal\n"); /* this prints instead — floating point surprise! */
}
/* Print the actual stored values to see why */
printf("0.1 + 0.2 = %.17f\n", a + b); /* 0.30000000000000004 */
printf("0.3 = %.17f\n", c); /* 0.29999999999999999 */
/* CORRECT: use an epsilon (tolerance) comparison */
if (fabs((a + b) - c) < 1e-9) {
printf("close enough — treated as equal\n");
}
return 0;
}
0.1 + 0.2 = 0.30000000000000004
0.3 = 0.29999999999999999
close enough — treated as equal
Always use a small tolerance: fabs(a - b) < 1e-9. The exact threshold depends on your use case — 1e-9 is typical for double. Using == on floats is a logic bug that the compiler will not catch. The full IEEE 754 bit layout (sign + exponent + mantissa) is covered in Topic 32 — Number Systems.
Quick Check — The Octal Gotcha
int x = 010; equal in decimal?
Week 2 Lecture (p.4) — common exam trap
A student writes the following, intending to store the number ten:
int x = 010;
printf("%d\n", x);
What does it actually print? Choose one:
In C, any integer literal that starts with 0 (and is not 0x or 0b) is interpreted as octal (base 8). So 010 means 1×8 + 0 = 8, not ten. This silently produces the wrong value with no warning — making it one of the most dangerous C gotchas for beginners. Never pad a decimal number with leading zeros.