Data Types
Reviewed & published by Brayan K
Every value in C++ has a type that decides how much memory it uses, what range of values it can hold, and which operations make sense. In this lesson you'll meet the full set of fundamental types, learn how big each one is, and avoid the classic traps of overflow and floating-point rounding.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- The fundamental types: int, long long, double, float, char, bool
- How to measure type sizes with sizeof and ranges with <limits>
- Why integer overflow wraps around — and the undefined-behavior trap
- Why doubles can't be compared with == and how to use an epsilon
- How auto deduces a type, and what literal suffixes (LL, u, f) mean
- How to pick the right type for whole numbers vs decimals
💡 Real-World Analogy
Think of types as differently sized buckets. A char is a thimble (1 byte) that holds a single drop. An int is a coffee mug (4 bytes). A long long is a bucket (8 bytes) that holds far more. A double is also a bucket but it's built to hold decimals precisely. Pour too much into a bucket and it doesn't just spill on the floor — it wraps around to the bottom, which is exactly what integer overflow does. Choosing the right-sized bucket up front prevents nasty surprises.
📊 The Fundamental Types
| Type | Holds | Typical Size | Approx Range |
|---|---|---|---|
| bool | true / false | 1 byte | 2 values |
| char | One character | 1 byte | -128..127 |
| int | Whole numbers | 4 bytes | ±2.1 billion |
| long long | Big whole numbers | 8 bytes | ±9.2 quintillion |
| float | Decimals (~7 digits) | 4 bytes | ±3.4e38 |
| double | Decimals (~15 digits) | 8 bytes | ±1.7e308 |
The C++ standard only guarantees minimum sizes; these are the common sizes on a 64-bit desktop. Use sizeof to check on your own machine.
1. The Fundamental Types in Action
Let's declare one of each and print it. Notice char uses single quotes, bool prints as 1 or 0, and the f suffix on a float literal and the LL suffix on a long long literal pin the value to that exact type.
#include <iostream>
using namespace std;
int main() {
// The fundamental built-in types of C++.
int score = 90; // whole number, ~+/- 2.1 billion
long long bigCount = 9000000000LL; // 64-bit whole number
double pi = 3.14159265; // decimal, ~15 significant digits
float ratio = 0.5f; // decimal, ~7 digits (note the 'f')
char letter = 'C'; // ONE character, single quotes
bool passed = true; // true / false -> prints as 1 / 0
cout << "score: " << score << '\n'; // 90
cout << "bigCount: " << bigCount << '\n'; // 9000000000
cout << "pi: " << pi << '\n'; // 3.14159
cout << "ratio: " << ratio << '\n'; // 0.5
cout << "letter: " << letter << '\n'; // C
cout << "passed: " << passed << '\n'; // 1
return 0;
}
// ✅ Expected output:
// score: 90
// bigCount: 9000000000
// pi: 3.14159
// ratio: 0.5
// letter: C
// passed: 12. Measuring Size & Range
sizeof tells you how many bytes a type occupies, and the <climits> and <limits> headers expose each type's minimum and maximum value. Knowing the range matters because a value that exceeds it doesn't error — it silently wraps.
#include <iostream>
#include <climits> // INT_MAX, INT_MIN
using namespace std;
int main() {
// sizeof reports how many BYTES a type uses (as size_t).
cout << "sizeof(char): " << sizeof(char) << " byte\n"; // 1
cout << "sizeof(int): " << sizeof(int) << " bytes\n"; // 4
cout << "sizeof(long long): " << sizeof(long long) << " bytes\n"; // 8
cout << "sizeof(double): " << sizeof(double) << " bytes\n"; // 8
// <climits> exposes the limits of integer types.
cout << "int range: " << INT_MIN << " .. " << INT_MAX << '\n';
// int range: -2147483648 .. 2147483647
return 0;
}
// ✅ Expected output:
// sizeof(char): 1 byte
// sizeof(int): 4 bytes
// sizeof(long long): 8 bytes
// sizeof(double): 8 bytes
// int range: -2147483648 .. 21474836473. Overflow: When a Bucket Runs Out
Add 1 to the largest possible int and it wraps around to the most negative value. For signed integers this is actually undefined behavior — the compiler is free to do anything — so never rely on it. For unsigned integers the wrap is well-defined and predictable.
#include <iostream>
#include <limits>
using namespace std;
int main() {
// OVERFLOW: pushing past the top of a type's range wraps around.
int maxInt = numeric_limits<int>::max(); // 2147483647
cout << "max int: " << maxInt << '\n';
cout << "max int + 1: " << maxInt + 1 << '\n';
// 2147483647 + 1 wraps to -2147483648 (undefined behavior for signed!)
// unsigned wraps cleanly (this IS defined): 0 - 1 -> the maximum.
unsigned int u = 0;
cout << "0u - 1 = " << (u - 1) << '\n'; // 4294967295
return 0;
}
// ✅ Expected output:
// max int: 2147483647
// max int + 1: -2147483648
// 0u - 1 = 42949672954. The Floating-Point Trap
Decimals like 0.1 can't be stored exactly in binary, so arithmetic introduces tiny rounding errors. cout hides them by rounding to 6 digits, but they're really there. The rule: never compare two doubles with ==. Instead check that their difference is smaller than a tiny tolerance (an "epsilon").
#include <iostream>
#include <iomanip> // setprecision
#include <cmath> // fabs
using namespace std;
int main() {
double a = 0.1 + 0.2;
cout << a << '\n'; // 0.3 (rounded for display)
cout << setprecision(17) << a << '\n'; // 0.30000000000000004
// NEVER use == on doubles. Compare the gap to a small epsilon.
if (fabs(a - 0.3) < 1e-9)
cout << "close enough to 0.3\n"; // this prints
else
cout << "not equal\n";
return 0;
}
// ✅ Expected output:
// 0.3
// 0.30000000000000004
// close enough to 0.35. auto & Literal Suffixes
auto lets the compiler deduce a variable's type from its initializer — with zero runtime cost. The type it picks depends on the literal: 10 is an int, 2.5 is a double, and suffixes like LL, u, and f nudge it toward a specific type.
#include <iostream>
#include <typeinfo>
using namespace std;
int main() {
// auto deduces the type from the value on the right.
auto count = 10; // int (whole number literal)
auto rate = 2.5; // double (decimal literal)
auto flag = false; // bool
auto ch = 'x'; // char
// Literal suffixes pin down a specific type:
auto big = 5000000000LL; // long long (the LL suffix)
auto small = 7u; // unsigned int (the u suffix)
cout << count << ' ' << rate << ' ' << flag << ' ' << ch << '\n';
cout << big << ' ' << small << '\n';
// 10 2.5 0 x
// 5000000000 7
return 0;
}
// ✅ Expected output:
// 10 2.5 0 x
// 5000000000 7Now you try. Fill in the blanks below with the right type or value:
#include <iostream>
using namespace std;
int main() {
// 🎯 YOUR TURN — fill each ___ with the right type or value.
// 1) A whole number for a year:
___ year = 2026; // 👉 the type for whole numbers
// 2) A decimal for a bank balance:
double balance = ___; // 👉 a decimal like 1250.75
// 3) A single character grade:
char grade = ___; // 👉 one character in 'single quotes'
cout << "Year " << year << ", balance $" << balance
<< ", grade " << grade << '\n';
// ✅ Example: Year 2026, balance $1250.75, grade A
return 0;
}🧩 Reorder Challenge
These lines compute a percentage and print it, but they're scrambled. Put them in the correct order so the program compiles and prints Pass rate: 75%.
A. cout << "Pass rate: " << percent << "%\n";
B. int main() {
C. return 0;
D. int passed = 30, total = 40;
E. double percent = 100.0 * passed / total;
F. #include <iostream>
G. }F. #include <iostream>
B. int main() {
D. int passed = 30, total = 40;
E. double percent = 100.0 * passed / total;
A. cout << "Pass rate: " << percent << "%\n";
C. return 0;
G. }Why: the #include must come before main. Inside main, the two int variables are declared first, then percent uses them — and the 100.0 forces double division so we get 75, not integer-truncated 0. The print and return 0; come last, and the closing brace ends the function.
🧠 Quick Recall
Predict the output before revealing each answer.
cout << (7 / 2) << " " << (7.0 / 2);
3 3.5 — 7 / 2 is integer division (remainder dropped), but 7.0 / 2 is double division.
bool b = true; cout << b + b;
2 — a bool promotes to int in arithmetic, so true + true is 1 + 1.
unsigned int u = 0; cout << (u - 1);
4294967295 — unsigned subtraction wraps around to the maximum value rather than going negative.
Pro Tips
- 💡 Default to int and double: they cover the vast majority of cases. Reach for long long only when a value might exceed ~2 billion.
- 💡 Never == two doubles: compare fabs(a - b) < 1e-9 instead.
- 💡 Avoid mixing signed and unsigned: in comparisons the signed value is converted to unsigned, turning -1 into a huge positive number.
- 💡 Force decimal division: write 100.0 * a / b, not 100 * a / b, when you want a fractional result.
Common Errors (and the fix)
- Silent overflow: storing 3000000000 in an int wraps to a negative number. Use long long for large values.
- Comparing doubles with ==: 0.1 + 0.2 == 0.3 is false. Use an epsilon comparison.
- Integer division surprise: 5 / 2 is 2. Make one side a decimal: 5.0 / 2.
- Signed/unsigned mismatch: looping for (unsigned i = 5; i >= 0; --i) never ends because i wraps instead of going below 0.
📋 Quick Reference
| Type | Typical Size | Holds / Range |
|---|---|---|
| bool | 1 byte | true / false (prints 1 / 0) |
| char | 1 byte | one character, -128..127 |
| int | 4 bytes | whole numbers, ±2.1 billion |
| long long | 8 bytes | big whole numbers, ±9.2 quintillion |
| float | 4 bytes | decimals ~7 digits, ±3.4e38 |
| double | 8 bytes | decimals ~15 digits, ±1.7e308 |
| unsigned int | 4 bytes | non-negative, 0..4294967295 |
| sizeof(T) | — | bytes a type uses (a size_t) |
| auto x = ...; | — | deduce type from initializer (compile time) |
| 5000000000LL | — | LL suffix → long long literal |
| 7u / 0.5f | — | u → unsigned, f → float literal |
| 7 / 2 | — | integer division → 3 (use 7.0 / 2 for 3.5) |
| numeric_limits<int>::max() | — | largest value of a type (from <limits>) |
| fabs(a - b) < 1e-9 | — | compare doubles instead of == |
Mini-Challenge: Storage Report
Pick the right type for each value, and remember to force double division when converting bytes to megabytes. Run it and match the expected output in the comments.
#include <iostream>
using namespace std;
int main() {
// 🎯 MINI-CHALLENGE: storage report
// 1. Declare: fileBytes (long long), megabytes (double),
// drive (char e.g. 'C'), isCompressed (bool).
// 2. Set fileBytes to 3221225472 and compute megabytes as
// fileBytes / 1048576.0 (note the .0 to force double division!).
// 3. Print a report. Expected with the values above:
// Drive C: 3221225472 bytes = 3072 MB (compressed: 1)
// your code here
return 0;
}🎉 Lesson Complete
- ✅ Fundamental types: bool, char, int, long long, float, double
- ✅ sizeof gives byte sizes; <limits> gives ranges
- ✅ Signed overflow is undefined behavior; unsigned wraps predictably
- ✅ Never compare doubles with == — use an epsilon
- ✅ auto deduces the type at compile time; suffixes LL/u/f pick a type
Practice quiz
Which type is guaranteed to be at least 64 bits for very large whole numbers?
- int
- short
- long long
- byte
Answer: long long. long long is at least 64 bits, holding values up to about 9.2 quintillion.
What does sizeof actually return?
- The value of a variable
- The number of bytes a type occupies
- The number of bits
- The maximum value of a type
Answer: The number of bytes a type occupies. sizeof gives the number of bytes a type or object occupies, as a size_t.
What happens when you add 1 to the maximum value of a signed int?
- It stays at the maximum
- It is undefined behavior (typically wraps to the minimum)
- It throws an exception
- It becomes 0
Answer: It is undefined behavior (typically wraps to the minimum). Signed integer overflow is undefined behavior; in practice it often wraps to the most negative value.
Why should you never compare two doubles with == ?
- == is too slow
- Binary rounding means they may differ slightly
- doubles cannot be compared at all
- == only works on ints
Answer: Binary rounding means they may differ slightly. Values like 0.1 have no exact binary form, so tiny rounding errors make == unreliable; compare against an epsilon.
What does an unsigned int print for (0u - 1)?
- -1
- 0
- 4294967295
- It throws an error
Answer: 4294967295. Unsigned subtraction wraps around cleanly to the maximum value, 4294967295.
What type does 'auto big = 5000000000LL;' deduce?
- int
- double
- long long
- unsigned
Answer: long long. The LL suffix pins the literal to long long, so auto deduces long long.
What is the typical size of a double on a 64-bit desktop?
- 2 bytes
- 4 bytes
- 8 bytes
- 16 bytes
Answer: 8 bytes. double is typically 8 bytes (and int is typically 4).
How much runtime cost does 'auto' add compared to writing the type by hand?
- A small constant cost
- Zero — it is resolved at compile time
- It doubles compile time only
- It allocates extra memory
Answer: Zero — it is resolved at compile time. auto is deduced at compile time and produces identical machine code — zero runtime cost.
What does cout print for (7 / 2) followed by (7.0 / 2)?
- 3.5 and 3.5
- 3 and 3.5
- 3 and 3
- 4 and 3.5
Answer: 3 and 3.5. 7 / 2 is integer division (3); 7.0 / 2 is double division (3.5).
How can you store a value like 3000000000 without silent overflow?
- Use int
- Use char
- Use long long
- Use bool
Answer: Use long long. 3000000000 exceeds int's ~2.1 billion range; use long long for large values.
Continue this course
- Previous: Variables & Data Types
- Next: Input & Output (cin & cout) — Read and print to the console with iostream
- Quick reference: C++ cheat sheet
Frequently asked questions
What is the difference between int and long long?
Both store whole numbers, but long long is guaranteed to be at least 64 bits, so it can hold much larger values (up to about 9.2 quintillion). Use int for everyday counters and indexes; reach for long long when a value might exceed roughly 2 billion, such as a file size in bytes or a factorial.
Why does my double print 0.1 + 0.2 as 0.3 sometimes but not 0.30000000000000004?
Floating-point types store values in binary, and 0.1 and 0.2 have no exact binary representation. cout rounds to 6 significant digits by default, hiding the error. If you print with full precision you will see 0.30000000000000004. Never compare doubles for exact equality — compare the absolute difference against a small epsilon instead.
When should I use unsigned types?
Use unsigned when a value can genuinely never be negative AND you need the extra positive range, for example a bitmask or a raw byte count. Beware mixing signed and unsigned in the same expression: -1 compared against an unsigned 0 wraps to a huge number and causes subtle bugs. Many style guides recommend plain signed int for ordinary counting.
What is auto and does it slow my program down?
auto asks the compiler to deduce a variable's type from its initializer at compile time. It produces exactly the same machine code as writing the type by hand — there is zero runtime cost. It is purely a convenience for readability and for types that are awkward to spell out.
What does sizeof actually return?
sizeof gives the number of bytes a type or object occupies in memory, as a value of type std::size_t. sizeof(int) is typically 4 and sizeof(double) is 8 on most desktop platforms, but the standard only guarantees minimum sizes, so they can differ on other hardware.