Variables and fundamental types
A variable is a name for a piece of memory, and its type says how many bytes that piece has and how the bits inside are read. C++ has a small set of fundamental types and lets you build every other type from them, so the first ten pages of a course spend their time on those.
The lesson covers the types the rest of the course uses, the size and range of each one, and the initialisation rule that turns a silent bug into a compile error. Every program below prints the real values of this machine's compiler and library.
Declare, initialise, assign
A declaration gives a type and a name. Initialisation gives the first value at the same moment, assignment replaces a value later, and the two are different operations even when the syntax looks similar: one constructs, the other overwrites.
auto asks the compiler to deduce the type from the initialiser. It is not a dynamic type: total is a double here because apples * price is a double, and that never changes. Use auto when the type is obvious from the right side or too long to spell, and write the type when the reader needs it.
The program shows the two steps separately: the first total is computed from 3 apples, the second from 5 after the assignment.
#include <iostream>
int main()
{
int apples = 3;
double price = 1.25;
auto total = apples * price;
std::cout << "first total: " << total << '\n';
apples = 5;
total = apples * price;
std::cout << "second total: " << total << '\n';
}
g++ -std=c++23 -Wall -Wextra declare.cpp -o declare
./declare
first total: 3.75
second total: 6.25
Integers and their limits
int is the natural integer type of the machine, usually 32 bits, and long long is at least 64. The standard fixes the minimum ranges, not the exact sizes; sizeof tells you the size on this compiler, and std::numeric_limits tells you the range.
Unsigned integers wrap around: subtracting 1 from 0 gives the largest value of the type. That is defined behaviour, unlike signed overflow, which is undefined and may do anything. Wrapping is useful for hashes and bit patterns and a bug in a counter.
The program prints the size of int, its maximum and the result of 0u - 1. The last line shows that multiplying a large int by 1000 stays inside the range here; with a bigger factor it would overflow silently.
#include <iostream>
#include <limits>
int main()
{
int small = 100;
unsigned int counter = 0;
counter = counter - 1;
std::cout << "int bytes: " << sizeof(int) << '\n';
std::cout << "int max: " << std::numeric_limits<int>::max() << '\n';
std::cout << "0u - 1: " << counter << '\n';
std::cout << "small * 1000: " << small * 1000 << '\n';
}
g++ -std=c++23 -Wall -Wextra integers.cpp -o integers
./integers
int bytes: 4
int max: 2147483647
0u - 1: 4294967295
small * 1000: 100000
Floating point is not exact
double stores about 15 significant decimal digits and float about 7, both in binary. A decimal fraction such as 0.1 has no exact binary representation, so adding it ten times does not reach 1.0 exactly, and comparing the result with 1.0 is false.
The sum still prints as 1, because the default output of std::cout shows six significant digits. The comparison on the next line shows the truth: the stored value is a hair below 1. The two 1/3 lines look identical at this precision as well; printing 17 digits, which needs a manipulator from lesson 066, separates them.
Use double unless a library demands float, and compare floating point values with a tolerance instead of ==. Money belongs in integer cents, not in a double.
#include <iostream>
int main()
{
double tenth = 0.1;
double sum = 0.0;
for (int i = 0; i < 10; ++i) {
sum += tenth;
}
std::cout << "0.1 added ten times: " << sum << '\n';
std::cout << "sum == 1.0: " << (sum == 1.0) << '\n';
std::cout << "1/3 as float: " << 1.0f / 3.0f << '\n';
std::cout << "1/3 as double: " << 1.0 / 3.0 << '\n';
}
g++ -std=c++23 -Wall -Wextra floating.cpp -o floating
./floating
0.1 added ten times: 1
sum == 1.0: 0
1/3 as float: 0.333333
1/3 as double: 0.333333
char and bool
char is a small integer that holds one byte, and a character literal such as 'A' is its numeric value in the execution character set. Printing a char prints the character; casting it to int prints the code, which is 65 for A in ASCII and UTF-8.
bool holds true or false and converts to 1 or 0. std::boolalpha makes the stream print the words instead of the digits, and the flag stays on until it is turned off.
Character arithmetic is legal and is how simple encodings are handled: 'A' + 2 is 'C'. It breaks for letters outside ASCII, which is why the course uses std::string and UTF-8 later instead of char arithmetic.
#include <iostream>
int main()
{
char letter = 'A';
std::cout << letter << ' ' << static_cast<int>(letter) << '\n';
std::cout << static_cast<char>(letter + 2) << '\n';
bool ready = true;
std::cout << ready << ' ' << std::boolalpha << ready << '\n';
}
g++ -std=c++23 -Wall -Wextra char_bool.cpp -o char_bool
./char_bool
A 65
C
1 true
Brace initialisation refuses to narrow
Braces initialise a variable and check the value at the same time. A double that does not fit an int is a narrowing conversion, and brace initialisation makes it a compile error instead of a silent truncation.
The message names the value, its type and the target type, and the fix is either a different type or an explicit cast. A cast is a promise that the loss is intended, and the compiler accepts it.
Prefer braces for every new variable: int count{0}; cannot become an uninitialised value, cannot silently drop a fraction, and reads the same as the older = form for the cases that are safe.
#include <iostream>
int main()
{
int count{3.7};
std::cout << count << '\n';
}
g++ -std=c++23 -Wall -Wextra narrowing.cpp -o narrowing
narrowing.cpp: In function 'int main()':
narrowing.cpp:5:15: error: narrowing conversion of '3.7000000000000002e+0' from 'double' to 'int' [-Wnarrowing]
5 | int count{3.7};
| ^~~
Exercises
- Print the size and the maximum of long, long long and std::size_t on this machine. Which of them change between a 32-bit and a 64-bit build?
- Change the floating point program to print with std::setprecision(17) and explain the value of the sum.
- Write a program that stores 4 billion in an int and in an unsigned int. Which one compiles with a warning, and which one wraps?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does the type of a variable decide?
Answer
How many bytes it has and how the bits are read
The type gives the size and the interpretation, which is why an int and a float over the same bytes differ.
-
What does brace initialisation do with a narrowing value?
Answer
It refuses the conversion at compile time
A brace initialiser is a checked initialisation, and a conversion that could lose information is an error.
-
What happens when an unsigned value goes below zero?
Answer
It wraps around to the largest value of the type
Unsigned arithmetic is modular, which is why 0u - 1 prints the maximum.
-
Why does adding 0.1 ten times not give exactly 1.0?
Answer
Binary floating point cannot represent 0.1 exactly
The stored value is the nearest double, and the small errors accumulate.
-
What does this program print?
#include <iostream> int main() { double first = 1.25 + 2.5; double second = first + 2.5; std::cout << "first total: " << first << " and second total: " << second; }Answer
first total: 3.75 and second total: 6.25
The two variables hold 3.75 and 6.25, and the stream prints them in the default format.
-
What does this program print?
#include <iostream> #include <limits> int main() { std::cout << std::numeric_limits<int>::max() << '\n'; }Answer
2147483647
The maximum of a 32-bit signed int is 2^31 - 1, and the program reads it from the type.
-
What does this program print?
#include <iostream> int main() { double sum = 0; for (int i = 0; i < 10; ++i) sum += 0.1; std::cout << (sum == 1.0); }Answer
0
The ten additions do not give exactly 1.0 in binary floating point, so the comparison is false.
-
What does this program print?
#include <iostream> int main() { char letter = 'A'; bool flag = true; std::cout << letter << ' ' << static_cast<int>(letter) << ", "; std::cout << static_cast<char>(letter + 2) << ", " << flag << ' ' << std::boolalpha << flag; }Answer
A 65, C, 1 true
A has code 65, two letters later is C, and a bool prints as 1 and then as true under boolalpha.
-
What does the compiler say about
int count{3.7};?Answer
narrowing conversion of '3.7...' from 'double' to 'int'
The brace form rejects the lossy conversion, while the equals form would compile with a truncation.
-
What is the difference between
int x = 3.7;andint x{3.7};?Answer
The first compiles and truncates, the second is an error
Only the brace initialisation checks for narrowing.
The code on this page was run with g++ 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev