Skip to content

3. Numbers & Math

What & why

Numbers in C++ map directly onto the hardware. That makes them fast — and it means you need to know the hardware's rules: integers have fixed sizes and can overflow, integer division throws away the remainder, and floating-point numbers are approximations.

Arithmetic operators

int a = 17, b = 5;

a + b    // 22
a - b    // 12
a * b    // 85
a / b    // 3    ← integer division: the fraction is discarded
a % b    // 2    ← remainder (modulo), integers only

Integer division vs. floating-point division

The type of the operands decides which division happens:

7 / 2        // 3     (int / int)
7.0 / 2      // 3.5   (double / int → double)
7 / 2.0      // 3.5
static_cast<double>(7) / 2   // 3.5

A classic bug:

int total = 7, count = 2;
double avg = total / count;                        // 3.0 — division happened in int!
double avg2 = static_cast<double>(total) / count;  // 3.5 — correct

Integer division truncates toward zero: -7 / 2 is -3, and -7 % 2 is -1.

Compound assignment and increment

int x = 10;
x += 5;    // x = x + 5   → 15
x -= 3;    // 12
x *= 2;    // 24
x /= 4;    // 6
x %= 4;    // 2

++x;       // pre-increment: add 1, then use the value
x++;       // post-increment: use the value, then add 1
--x;

Prefer ++x when you don't need the old value. Never modify the same variable twice in one expression (x = x++ + ++x;) — the result is undefined or unspecified.

Precedence

*, /, % bind tighter than +, -. Use parentheses whenever it helps a reader:

int r = 2 + 3 * 4;      // 14
int s = (2 + 3) * 4;    // 20

Overflow

Integers have limits:

#include <iostream>
#include <limits>

int main() {
    std::cout << std::numeric_limits<int>::max() << '\n';   // 2147483647
    std::cout << std::numeric_limits<int>::min() << '\n';   // -2147483648
}
  • Signed overflow (int) is undefined behavior. The compiler assumes it never happens; anything can result.
  • Unsigned overflow wraps around: 0u - 1 is 4294967295.

Use long long (or std::int64_t) when values can exceed ~2 billion.

Floating-point pitfalls

#include <cmath>
#include <iostream>

int main() {
    double x = 0.1 + 0.2;
    std::cout << (x == 0.3) << '\n';                    // 0 (false!)
    std::cout << std::abs(x - 0.3) < 1e-9 ? "close" : "far";
}

Oops — that last line doesn't even compile: << binds tighter than <, so it tries to compare the stream itself with 1e-9. Parenthesize:

std::cout << (std::abs(x - 0.3) < 1e-9 ? "close\n" : "far\n");

Rules:

  • Never compare floating-point values with ==. Compare with a tolerance.
  • Use double, not float, unless you have a specific reason.
  • Never use floating point for money. Store cents in an integer.

Controlling printed precision:

#include <format>
std::cout << std::format("{:.2f}\n", 3.14159);   // 3.14

<cmath>

#include <cmath>

std::sqrt(16.0)      // 4
std::pow(2.0, 10)    // 1024
std::abs(-5)         // 5
std::floor(2.7)      // 2
std::ceil(2.1)       // 3
std::round(2.5)      // 3
std::fmod(7.5, 2)    // 1.5 (remainder for doubles)
std::hypot(3.0, 4.0) // 5

#include <numbers>
std::numbers::pi     // 3.14159... (C++20)

Random numbers with <random>

Forget rand(). Use an engine plus a distribution:

#include <iostream>
#include <random>

int main() {
    std::random_device rd;                         // seed source
    std::mt19937 gen{rd()};                        // Mersenne Twister engine
    std::uniform_int_distribution<int> die{1, 6};  // inclusive range

    for (int i = 0; i < 5; ++i) {
        std::cout << die(gen) << ' ';
    }
    std::cout << '\n';

    std::uniform_real_distribution<double> unit{0.0, 1.0};
    std::cout << unit(gen) << '\n';
}

Create the engine once and reuse it. Seeding a new engine every call gives poor randomness.

Number literals

1'000'000      // digit separators for readability
0xFF           // hexadecimal (255)
0b1010         // binary (10)
3.0e8          // scientific notation
42u  42L  42LL // unsigned, long, long long
2.5f           // float

Common mistakes

  • double avg = sum / count; with two ints — integer division. Cast first.
  • Comparing doubles with ==.
  • % on doubles — doesn't compile. Use std::fmod.
  • Dividing by zero — integer division by zero is undefined behavior (usually a crash).
  • std::pow for integers — it returns a double; std::pow(10, 2) might be 99.9999... when truncated. Multiply integers directly.
  • abs vs std::abs — the unqualified C abs only handles int. Use std::abs with <cmath>.

Exercises

  1. Read two integers and print their sum, difference, product, integer quotient, remainder, and exact (floating-point) quotient.
  2. Read a total number of seconds and print H:MM:SS (use / and %).
  3. Compute the hypotenuse of a right triangle from two side lengths.
  4. Simulate rolling two dice 10,000 times and print how often each total (2–12) appears.
  5. Print std::numeric_limits<int>::max(), then add 1 to it and print again. Compile with -fsanitize=undefined and run. What happens?
  6. Compound interest: read a principal, annual rate, and years, and print the final amount to 2 decimal places.

Recap

  • int / int is integer division; cast to double for a real quotient.
  • % gives the remainder (integers only).
  • Signed overflow is undefined behavior; use wider types when needed.
  • Floating point is approximate: compare with a tolerance, never use it for money.
  • <cmath> for math functions, <numbers> for constants, <random> for randomness.

Next: Strings →