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

  1. 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?
  2. Change the floating point program to print with std::setprecision(17) and explain the value of the sum.
  3. 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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.

  10. What is the difference between int x = 3.7; and int 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