Module I · Basics of C · Lecture
🎯 Why This Matters
Back in the “Identifiers, Keywords and Variables” post, we compared a variable to a labeled storage box — and mentioned that every box can only hold one type of item. A box labeled “screws” shouldn’t suddenly hold water. A data type is exactly what tells the compiler which kind of “item” a variable is allowed to hold, how much memory to set aside for it, and what operations make sense on it.
Get the data type wrong, and strange things happen: numbers silently lose their decimal points, huge values wrap around to negative numbers, or a calculation that should give 2.5 quietly gives you 2 instead. This post covers every basic data type in C, with a working code example for nearly every concept, so you can see — not just read about — exactly how each one behaves.
🗂️ The Data Type Family in C
C organizes its data types into four broad categories. This post focuses mainly on the first one, since that’s what you’ll use in almost every program you write early on.
| Category | Examples | Description |
|---|---|---|
| Basic (Primary) | int, float, double, char, void | Built directly into the language — the foundation everything else is built from |
| Derived | Arrays, Pointers, Functions | Built from basic types — covered in a later post |
| User-Defined | struct, union, enum | Types you design yourself by combining other types — also a later post |
| Void | void | Represents the deliberate absence of a value |
1️⃣ int — Whole Numbers
The int type stores whole numbers — no decimal point. It’s typically 4 bytes (32 bits) on most modern systems, though the C standard only guarantees it’s at least 2 bytes.
#include <stdio.h>
int main() {
int age = 21;
int temperature = -5;
int zero = 0;
printf("Age: %d\n", age);
printf("Temperature: %d\n", temperature);
printf("Zero: %d\n", zero);
printf("Size of int: %zu bytes\n", sizeof(int));
return 0;
}
🖥️ Output:
Age: 21
Temperature: -5
Zero: 0
Size of int: 4 bytes
Notice int handles negative numbers just as easily as positive ones — by default, int is signed, meaning it can represent both positive and negative values. We’ll see what happens when that changes later in this post.
2️⃣ float — Single-Precision Decimals
float stores numbers with a decimal point, using 4 bytes. It trades away some precision in exchange for using less memory than double.
#include <stdio.h>
int main() {
float price = 49.99;
float pi = 3.14159265358979;
printf("Price: %.2f\n", price);
printf("Pi (stored as float): %.10f\n", pi);
printf("Size of float: %zu bytes\n", sizeof(float));
return 0;
}
🖥️ Output:
Price: 49.99
Pi (stored as float): 3.1415927410
Size of float: 4 bytes
Look closely at the second line. We typed in 14 digits of π, but printing it back out to 10 decimal places reveals the value has already drifted from the true value of π starting around the 7th digit. This isn’t a bug — float can only reliably store about 6–7 significant digits, so anything beyond that is essentially noise.
3️⃣ double — Double-Precision Decimals
double also stores decimal numbers, but uses 8 bytes — double the storage of float — giving roughly 15–16 reliable significant digits instead of 6–7.
#include <stdio.h>
int main() {
double pi = 3.14159265358979;
printf("Pi (stored as double): %.10lf\n", pi);
printf("Size of double: %zu bytes\n", sizeof(double));
return 0;
}
🖥️ Output:
Pi (stored as double): 3.1415926536
Size of double: 8 bytes
Compare this output to the float example above — the exact same value now survives to 10 decimal places without visibly drifting. Use double whenever precision genuinely matters — scientific calculations, financial totals, GPS coordinates — and reserve float for cases where memory is tight and rough precision is acceptable.
Figure: how many digits of π actually survive in float vs. double
4️⃣ char — A Single Character
char stores exactly one character, using just 1 byte. Character literals are written in single quotes, not double quotes.
#include <stdio.h>
int main() {
char grade = 'A';
char newline = '\n';
printf("Grade: %c\n", grade);
printf("Size of char: %zu byte\n", sizeof(char));
return 0;
}
🖥️ Output:
Grade: A
Size of char: 1 byte
Here’s the part that surprises most beginners: a char isn’t really its own special type — it’s just a small integer. Every character your computer displays is secretly stored as a number (its ASCII code), and char is simply an int that’s restricted to a smaller range. Let’s prove it:
#include <stdio.h>
int main() {
char letter = 'A';
printf("As a character: %c\n", letter);
printf("As a number: %d\n", letter);
char nextLetter = letter + 1;
printf("letter + 1 as a character: %c\n", nextLetter);
return 0;
}
🖥️ Output:
As a character: A
As a number: 65
letter + 1 as a character: B
Figure: ‘A’ and 65 are the same bits — char is just a restricted int
The letter 'A' is really the number 65 underneath. Adding 1 to it and printing the result as a character gives 'B' — because 'B' is stored as 66. This is exactly how programs convert between uppercase and lowercase, or check whether a character is a digit, without any special “character math” — it’s all just ordinary arithmetic on numbers.
5️⃣ void — The Absence of a Value
void doesn’t store any value at all — it represents the deliberate absence of one. You’ve technically already seen it in every program in this course:
#include <stdio.h>
void printGreeting() {
printf("Hello from a void function!\n");
// no 'return' with a value needed — this function returns nothing
}
int main() {
printGreeting();
return 0;
}
🖥️ Output:
Hello from a void function!
void shows up in two places: as a function’s return type (meaning “this function doesn’t hand back a value,” as above), and inside main()‘s parentheses as int main(void) in more formal code (meaning “this function accepts no arguments”). We’ll cover functions properly in a later post — for now, just recognize void as C’s way of saying “nothing goes here.”
🔧 Type Modifiers: Fine-Tuning Size and Range
Beyond the basic types, C lets you adjust int and char (technically double too, for long double) using four modifiers: short, long, signed, and unsigned. These change how much memory is used and what range of values can be stored.
#include <stdio.h>
int main() {
printf("short int: %zu bytes\n", sizeof(short int));
printf("int: %zu bytes\n", sizeof(int));
printf("long int: %zu bytes\n", sizeof(long int));
printf("long long int: %zu bytes\n", sizeof(long long int));
return 0;
}
🖥️ Output (typical on most modern systems):
short int: 2 bytes
int: 4 bytes
long int: 8 bytes
long long int: 8 bytes
Figure: relative memory footprint of each type (drawn to scale by byte count)
💡 Why “typical”? Unlike char (always 1 byte) or the fixed-width types in <stdint.h>, the exact size of int, long, and friends is allowed to vary between compilers and operating systems — the C standard only guarantees minimum sizes. Always check with sizeof rather than assuming, if your program’s correctness depends on exact sizes.
signed vs. unsigned
By default, numeric types are signed — able to represent negative numbers. Adding unsigned tells C to use that same memory to represent only non-negative numbers, which doubles the largest positive value you can store.
#include <stdio.h>
int main() {
signed int a = -10;
unsigned int b = 10;
printf("Signed int: %d\n", a);
printf("Unsigned int: %u\n", b);
// What happens if we force a negative number into an unsigned variable?
unsigned int c = -1;
printf("Unsigned int holding -1: %u\n", c);
return 0;
}
🖥️ Output:
Signed int: -10
Unsigned int: 10
Unsigned int holding -1: 4294967295
Figure: storing -1 in an unsigned int wraps it around to the maximum value
That last line is important to understand, not just memorize. An unsigned int has no way to represent -1, so the bit pattern that would mean -1 in a signed variable gets reinterpreted as the largest possible unsigned value instead. This is called integer overflow / wraparound, and it’s a real source of bugs — especially in loops that count downward using an unsigned counter.
📋 Format Specifiers: Matching Type to printf
Every data type needs its own matching format specifier in printf() and scanf(). Using the wrong one won’t always stop your program from compiling — but it can print garbage values.
| Type | Format Specifier |
|---|---|
int | %d |
float | %f |
double | %lf |
char | %c |
unsigned int | %u |
long int | %ld |
short int | %hd |
Here’s what actually goes wrong when the specifier doesn’t match the type:
#include <stdio.h>
int main() {
float price = 19.99;
printf("Using %%f (correct): %f\n", price);
printf("Using %%d (wrong): %d\n", price);
return 0;
}
🖥️ Output (exact garbage value varies by system):
Using %f (correct): 19.990000
Using %d (wrong): 1374389535
The second line isn’t 19, or 20, or any reasonable rounding of 19.99 — it’s meaningless garbage. This happens because %d and %f tell printf to read the value’s underlying bits completely differently, and a float‘s bit pattern makes no sense when interpreted as an int.
🔄 Type Conversion: When C Changes Types For You
C constantly converts values between types, sometimes automatically and sometimes only when you explicitly ask for it. Understanding the difference prevents some of the most common beginner bugs.
Implicit Conversion (Automatic)
When an expression mixes types, C automatically promotes the “smaller” type to match the “larger” one before doing the calculation.
#include <stdio.h>
int main() {
int wholeNumber = 5;
float decimalNumber = 2.5;
float result = wholeNumber + decimalNumber;
printf("5 + 2.5 = %.1f\n", result);
return 0;
}
🖥️ Output:
5 + 2.5 = 7.5
Here, wholeNumber (an int) is automatically converted to 5.0 (a float) before the addition happens — you didn’t have to ask for that, C did it for you because mixing an int and a float in the same expression triggers automatic promotion.
The Classic Trap: Integer Division
Implicit conversion has a famous gotcha. Watch what happens when both operands are integers:
#include <stdio.h>
int main() {
int total = 7;
int people = 2;
float average = total / people;
printf("Average (wrong): %.2f\n", average);
return 0;
}
🖥️ Output:
Average (wrong): 3.00
The mathematically correct answer is 3.5, but we got 3.00. Here’s why: since both total and people are int, C performs integer division first — throwing away the remainder completely — and only afterward converts the already-wrong whole-number result into a float. By the time the conversion happens, the .5 is already gone.
Explicit Conversion (Type Casting) — The Fix
To fix the bug above, we force one of the integers to become a float before the division happens, using a cast: writing the target type in parentheses right before the value.
#include <stdio.h>
int main() {
int total = 7;
int people = 2;
float average = (float) total / people;
printf("Average (correct): %.2f\n", average);
return 0;
}
🖥️ Output:
Average (correct): 3.50
The moment total is cast to (float) total, the division sees a float and an int together — which triggers the same implicit promotion we saw earlier, converting people to a float too. Now the division happens in decimal arithmetic from the very start, and the result is correct.
Casting the Other Direction: Losing Data on Purpose
Casting can also go from a larger type down to a smaller one — but this direction can silently throw away information.
#include <stdio.h>
int main() {
float temperature = 98.6;
int wholeDegrees = (int) temperature;
printf("Original: %.1f\n", temperature);
printf("After casting to int: %d\n", wholeDegrees);
return 0;
}
🖥️ Output:
Original: 98.6
After casting to int: 98
Figure: casting 3.99 to int gives 3, not the nearer value 4
Casting a float to an int doesn’t round to the nearest whole number — it truncates, simply chopping off everything after the decimal point. 98.6 becomes 98, not 99. If you need proper rounding, you’d use a function like round() from <math.h> instead of a plain cast.
🌍 Real-World Analogy: Shipping Containers
Think of data types like different shipping containers at a port:
- An
intis a standard dry container — great for whole units of cargo, nothing fractional - A
floatis a smaller refrigerated container — handles delicate, precise cargo, but has limited capacity - A
doubleis a larger refrigerated container — the same idea asfloat, but with far more capacity for precision - A
charis a small parcel box — built for exactly one small item - Casting a
doubledown to anintis like forcing refrigerated cargo into a dry container — whatever needed refrigeration (the decimal part) simply doesn’t survive the move
🚫 Common Mistakes Beginners Make
| Mistake | Why it happens |
|---|---|
Dividing two ints and expecting a decimal result | Integer division truncates before any conversion to float happens |
Using %d to print a float or double | Mismatched specifiers read the value’s bits incorrectly, producing garbage |
Storing a negative number in an unsigned variable | Causes wraparound to a very large positive number instead of an error |
Assuming float can store any decimal exactly | float only reliably holds about 6–7 significant digits |
Expecting (int) casting to round | Casting truncates (chops off the decimal) rather than rounding |
🎓 Code-Tracing Exercises
Exercise 1: Predict the Output
#include <stdio.h>
int main() {
char c = 'z';
printf("%d\n", c);
return 0;
}
(Answer: 122 — the ASCII code for lowercase 'z'.)
Exercise 2: Spot the Bug
#include <stdio.h>
int main() {
int a = 9, b = 4;
float result = a / b;
printf("%.2f\n", result);
return 0;
}
(Answer: prints 2.00, not 2.25 — integer division happens first because both a and b are int. Fix: cast one operand, e.g. (float) a / b.)
Exercise 3: Trace the Cast
#include <stdio.h>
int main() {
double pi = 3.99;
int whole = (int) pi;
printf("%d\n", whole);
return 0;
}
(Answer: 3 — casting truncates rather than rounding, even though 3.99 is very close to 4.)
❓ Frequently Asked Questions
Q: Why does C have both float and double instead of just one decimal type?
It’s a trade-off between memory and precision. Early computers had very limited memory, so having a smaller, less precise option (float) mattered. Today, double is often the safer default unless you have a specific reason to save space.
Q: Is char signed or unsigned by default?
It technically depends on the compiler and platform — char may be signed or unsigned by default. If it matters for your program (for instance, storing raw byte values above 127), it’s safer to explicitly write signed char or unsigned char.
Q: What’s the difference between %f and %lf if they’re printed the same way?
In printf(), both actually work for double due to how C automatically promotes float arguments — but in scanf(), the distinction matters and is not interchangeable: you must use %f to read into a float variable and %lf to read into a double.
Q: How do I know the exact size of a type on my system instead of guessing?
Use the sizeof operator, exactly as shown throughout this post — sizeof(int), sizeof(double), and so on — and print the result. Never hard-code an assumed size into logic that depends on being correct.
✅ Key Takeaways
- C’s basic types are
int,float,double,char, andvoid— each reserving a different amount of memory and suited to a different kind of value. charis secretly just a small integer — every character is stored as its ASCII numeric code, which is why character arithmetic works at all.- Modifiers like
short,long,signed, andunsignedadjust size and range — and forcing a negative value into anunsignedtype causes silent wraparound, not an error. floatanddoubleboth trade off exactness for range —floatreliably holds ~6–7 digits,doubleholds ~15–16.- Integer division truncates before any conversion happens — casting one operand to
floatbefore the division is the fix, not casting the final result afterward. - Casting from a decimal type to an integer type truncates, it does not round.
🚀 Next up: Now that you know how C stores and converts individual values, we’ll look at operators and expressions — arithmetic, relational, and logical operators — and how C decides the order in which a complex expression gets evaluated.