4 Expressions

You know how to declare variables and store data (Chapter 2) and how to work with text (Chapter 3). But so far you have used operators only in passing — assignment with =, plus the string concatenation and comparisons you met in Chapter 3. This chapter treats operators systematically: without them you cannot add two numbers, compare a score to a threshold, or combine conditions. You can store data, but you cannot do anything with it. Expressions are how you tell C++ to compute, compare, and combine values. In this chapter you will learn the full set of operators C++ provides — arithmetic, logical, bitwise, and more — how they combine in expressions, and the precedence rules that determine the order of evaluation.

4.1 Assignment

The simplest operator is assignment with =. As you saw in Chapter 2, assignment stores a value in a variable.

int jumps = 0;
jumps = 42;

The right side is evaluated first, then the result is stored in the variable on the left. This means you can use the variable itself on the right side.

int count = 10;
count = count + 1;  // count is now 11

Trap: Do not confuse = (assignment) with == (equality comparison). Writing if (x = 5) assigns 5 to x instead of comparing x to 5. The compiler may warn you about this, but it is still valid C++.

4.2 Arithmetic Operators

C++ provides the standard math operators.

Operator Operation Example Result
+ addition 7 + 3 10
- subtraction 7 - 3 4
* multiplication 7 * 3 21
/ division 7 / 3 2
% modulo 7 % 3 1

These work the way you expect for the most part, but division has an important detail.

4.2.1 Integer Division

When both operands are integers, / performs integer division — it drops the fractional part.

int result = 7 / 3;  // 2, not 2.333...

The fractional part is simply discarded; it does not round. So 7 / 3 is 2 and -7 / 3 is -2.

If you want the full decimal result, at least one operand must be a floating-point type.

double result = 7.0 / 3;   // 2.333...
double also   = 7 / 3.0;   // 2.333...
int nope      = 7 / 3;     // 2 --- both operands are int

Trap: Integer division by zero crashes your program. There is no exception, no error message — formally it is undefined behavior, and on most systems it simply crashes. Always check your divisor before dividing.

4.2.2 Modulo

The % operator gives the remainder after integer division.

int leftover = 7 % 3;   // 1, because 7 = 2*3 + 1
int even = 10 % 2;      // 0, so 10 is even

With negative operands, the result takes the sign of the left operand (the number being divided). The sign of the right operand does not matter.

int a = -7 % 3;    // -1
int b = 7 % -3;    //  1
int c = -7 % -3;   // -1

This follows from integer division truncating toward zero. C++ guarantees that (a / b) * b + a % b equals a. Since -7 / 3 truncates to -2, the remainder must be -7 - (-2 * 3), which is -1.

Modulo only works with integers. You cannot use % with float or double.

A common use for modulo is checking if a number is even or odd.

if (number % 2 == 0) {
    std::cout << "even" << std::endl;
} else {
    std::cout << "odd" << std::endl;
}

Wut: In math class, -7 mod 3 is 2 — the result is never negative. In C++, -7 % 3 is -1. This means testing for odd with number % 2 == 1 silently fails for negative numbers, because -7 % 2 is -1, not 1. Test with number % 2 != 0 instead.

4.2.3 Mixed-Type Arithmetic and Integer Promotion

When you mix types in an arithmetic expression, C++ does not refuse — it silently converts one operand so the operation makes sense. Most of the time this gives you the answer you expected; sometimes it does not.

Two rules cover the common cases:

  • Integer promotion: any integer type smaller than int (char, short, bool) is widened to int before the operation.
  • Usual arithmetic conversions: when the two operands still differ, the “smaller” operand is converted to match the “larger” one. Floating-point beats integer; wider beats narrower; unsigned beats signed at the same width.

You see promotion in action with char:

char a = 'A';
char b = 'B';
auto sum = a + b;       // sum is an int (131), not a char

Both operands are promoted to int before + runs, and the result is int. That is why auto sum deduces int, even though both inputs were char.

Why does C++ bother promoting at all? Because int is meant to be the natural word size of the processor — the size it computes with fastest. CPUs do arithmetic in registers, and loading a char into a register widens it to full width anyway; many processors have no instructions at all for arithmetic on values narrower than a register. So the language rule simply matches what the hardware was going to do regardless — a design C++ inherited from C. Promotion has a nice side effect: intermediate results get breathing room. 'A' + 'B' is 131, which does not fit in a signed 8-bit char, but sits comfortably in an int.

You see the wider-wins rule with mixed integer / floating-point:

int    streams = 1000;
double rate    = 1.5;
auto   total   = streams * rate;   // total is double (1500.0)

streams is converted to double first, then multiplied. That is also why integer division 7 / 3 gave you 2 earlier — both operands were already int, so no conversion happened, and integer division ran. Make either operand a double and the rule kicks in: 7 / 3.0 gives 2.333....

The trap is mixing signed and unsigned at the same width:

int          fans  = -1;
unsigned int crowd =  10;
if (fans < crowd) {            // looks obviously true
    std::cout << "outnumbered\n";
} else {
    std::cout << "we are bigger?\n";
}

Following the rules: same width, so fans is converted to unsigned int. -1 as an unsigned 32-bit value is 4'294'967'295, which is not less than 10, so the program prints we are bigger?.

Why that particular giant number? Signed integers are stored in an encoding called two’s complement, where -1 is all 32 bits set to 1. The conversion does not change the bits — it only changes how they are read, and all bits set read as unsigned is the largest possible value. Chapter 7 covers two’s complement in detail.

Trap: Avoid arithmetic and comparisons that mix signed and unsigned integers [@CppCoreGuidelines, rule ES.100]. The compiler will usually warn (-Wsign-compare), but the conversion still happens. Either pick one signedness and stick with it, or convert explicitly with static_cast.

Wut: char + char is an int, not a char. This is integer promotion at work: any integer type narrower than int is widened to int before arithmetic runs. That promotion only applies to integer types — float + float stays float, and double + double stays double.

4.3 Comparison Operators

Comparison operators compare two values and produce a bool result: true or false.

Operator Meaning Example Result
== equal to 5 == 5 true
!= not equal to 5 != 3 true
< less than 3 < 5 true
> greater than 3 > 5 false
<= less than or equal to 5 <= 5 true
>= greater than or equal to 3 >= 5 false

As you saw in Chapter 3, these also work on strings, comparing them character by character.

4.4 Logical Operators

Logical operators combine boolean expressions.

Operator Meaning Example Result
&& AND true && false false
|| OR true || false true
! NOT !true false

These are used to build more complex conditions. They work the same way they do in English. A && B is true if both A and B are true, otherwise it is false. A || B is true if either A or B is true — this includes the case when both are true.

Let’s look at this rule: you must have two pencils or one pen and not have a calculator to take a test. Unfortunately, in English, it is ambiguous whether or not you can take the test if you have two pencils and a calculator. Fortunately, we can make it clear in code using parentheses.

int pencils = 2;
int pens = 1;
bool has_calculator = true;
bool can_take_test = (pencils == 2 || pens == 1) && !has_calculator;

In the above scenario, the student cannot take the test because they have a calculator. If we change has_calculator to false, can_take_test will become true.

Tip: When mixing logical operators, use parentheses. This rule is worth repeating :). Parentheses make your intent clear to both the compiler and anyone reading your code. There are rules about order of operations, but your average developer will not be certain that they remember them correctly. The exception to this rule is the ! operator, which is evaluated before other logical operators.

4.4.1 Non-Zero Means true

The operands of logical operators do not have to be bool. Any number can be used where a condition is expected: zero converts to false, and any non-zero value — positive or negative — converts to true.

int volume = 11;
if (volume) {          // true: 11 is non-zero
    std::cout << "turned up" << std::endl;
}

The ! operator uses the same rule: !volume converts volume to bool and then flips it, making !volume a compact way to test for zero. Apply it twice and you get the !!x idiom: the inner ! converts to bool and flips, the outer ! flips back, so !!volume is true exactly when volume is non-zero. This is an idiom C++ inherited from C, and you will see it in real code collapsing a value into a clean true or false — but volume != 0 says the same thing more clearly, so prefer that in your own code. The conversion also runs in reverse: in arithmetic, true becomes 1 and false becomes 0, because bool is one of the types promoted to int (see the promotion rules above). This is also why the if (x = 5) trap from the assignment section compiles and misbehaves: the assignment expression evaluates to 5, which converts to true, so the branch always runs. You will see the shorthand if (volume) everywhere in real code, though writing volume != 0 makes your intent more explicit.

4.4.2 Short-Circuit Evaluation

C++ always evaluates the built-in logical operators left to right and stops as soon as the result is known.

With &&, if the left side is false, the right side is never evaluated — the result is already false no matter what. With ||, if the left side is true, the right side is never evaluated — the result is already true.

int x = 0;
if (x != 0 && 10 / x > 2) {
    // safe: if x is 0, the division never happens
}

This is not just an optimization — it is a guarantee you can rely on. The example above would crash without short-circuit evaluation because dividing by zero is undefined behavior.

Tip: Short-circuit evaluation lets you write guard conditions. Check that an operation is safe on the left side of && before performing it on the right side.

4.5 Increment and Decrement

The ++ and -- operators add or subtract 1 from a variable. They come in two forms: prefix and postfix.

int n = 5;
++n;    // prefix: n is now 6
n++;    // postfix: n is now 7
--n;    // prefix: n is now 6
n--;    // postfix: n is now 5

When used as a standalone statement, prefix and postfix do the same thing. The difference appears when the result is used in a larger expression.

  • Prefix (++n): increments n, then returns the new value.
  • Postfix (n++): returns the current value of n, then increments it.
int a = 5;
int b = ++a;   // a is 6, b is 6 (increment first, then use)
int c = a++;   // a is 7, c is 6 (use current value, then increment)

Tip: When you do not need the old value, prefer prefix ++n out of habit. For built-in types the compiler optimizes them to be the same, but with more complex types (like iterators) prefix can be faster because it does not need to make a copy of the old value.

4.6 Compound Assignment Operators

Compound assignment operators combine an arithmetic or bitwise operation with assignment. Instead of writing x = x + 5, you can write x += 5.

Operator Equivalent
+= x = x + y
-= x = x - y
*= x = x * y
/= x = x / y
%= x = x % y
&= x = x & y
|= x = x | y
^= x = x ^ y
<<= x = x << y
>>= x = x >> y
int score = 100;
score += 50;   // score is now 150
score -= 25;   // score is now 125
score *= 2;    // score is now 250

These are just shorthand. For simple variables there is no difference in behavior between score += 50 and score = score + 50.

4.7 Bitwise Operators

Bitwise operators work on the individual bits of integer values. These are not something you will use every day, but they are essential for systems programming, hardware interfaces, and flags. Chapter 7 explains how to think about a number as a row of bits and works through what each of these operators does to those bits. For now, here is a quick preview of which symbols exist.

Operator Operation
& AND
| OR
^ XOR
~ NOT (complement)
<< left shift
>> right shift

Trap: && and & and || and | are very different operations. The compiler doesn’t always detect when you mix them up, so make sure you are using the correct one.

Trap: ^ is not exponentiation! 2^2 is not 4. As we will see later, it is 0.

4.8 The Ternary Operator

The ternary operator ?: is a compact way to choose between two values based on a condition.

condition ? value_if_true : value_if_false
int temperature = 30;
std::string weather = (temperature > 25) ? "hot" : "cool";
std::cout << "it is " << weather << std::endl;

This is equivalent to an if-else, but in a single expression.

// same thing with if-else
std::string weather;
if (temperature > 25) {
    weather = "hot";
} else {
    weather = "cool";
}

The ternary operator is best for simple choices. If the logic is complex, use a regular if-else — readability matters more than brevity.

4.9 Operator Precedence

When an expression has multiple operators, C++ uses precedence rules to determine the order of evaluation. Operators with higher precedence are evaluated first.

When operators share the same precedence level, associativity decides the order. Most operators are evaluated left to right: 10 - 4 - 3 means (10 - 4) - 3, which is 3, not 10 - (4 - 3), which would be 9. The exceptions are assignment, the ternary ?:, and the prefix unary operators, which are evaluated right to left — that is why a = b = 5 assigns 5 to b first, then assigns that result to a.

Here is a simplified precedence table, from highest to lowest.

Precedence Operators Description
1 () [] . -> grouping, subscript
2 ++ -- (postfix) postfix increment
3 ++ -- (prefix) ! ~ - prefix, unary
4 * / % multiplicative
5 + - additive
6 << >> shift
7 < <= > >= relational
8 == != equality
9 & bitwise AND
10 ^ bitwise XOR
11 | bitwise OR
12 && logical AND
13 || logical OR
14 ?: ternary
15 = += -= *= etc. assignment

(One simplification worth knowing: in the official grammar, ?: and the assignment operators actually share a single precedence level and group right to left.)

The classic gotcha is forgetting that comparison binds tighter than bitwise operators.

// BUG: this checks (flags) & (2 == 2), not (flags & 2) == 2
if (flags & 2 == 2) { ... }

// CORRECT:
if ((flags & 2) == 2) { ... }

Tip: When in doubt, use parentheses. They make your intent clear to both the compiler and anyone reading your code. You do not get bonus points for memorizing the precedence table.

4.10 Try It

Here is a small program that uses several operators. Try modifying it and predicting the output before running it.

#include <iostream>
#include <string>

int main() {
    int x = 10;
    int y = 3;

    std::cout << "x + y  = " << x + y << std::endl;
    std::cout << "x / y  = " << x / y << std::endl;
    std::cout << "x % y  = " << x % y << std::endl;

    x += 5;
    std::cout << "x += 5 => " << x << std::endl;

    int a = 5;
    int b = ++a;
    int c = a++;
    std::cout << "a=" << a << " b=" << b
              << " c=" << c << std::endl;

    std::string result =
        (x > y) ? "Jump around!" : "U can't touch this";
    std::cout << result << std::endl;

    return 0;
}

4.11 Key Points

  • = is assignment; == is comparison. Confusing them is one of the most common bugs.
  • Integer division (/ with two integers) truncates the result. Use a floating-point operand if you need the decimal part.
  • Mixed-type arithmetic triggers integer promotion (anything narrower than int becomes int) and the usual arithmetic conversions (the “smaller” operand is converted to match the “larger” one). Mixing signed and unsigned at the same width is a frequent source of bugs.
  • % (modulo) gives the remainder of integer division; with negative operands the result takes the sign of the left operand. It only works with integers.
  • In a condition, zero converts to false and any non-zero value converts to true; in arithmetic, true is 1 and false is 0.
  • Logical operators && and || short-circuit: they stop evaluating as soon as the result is determined.
  • Prefix ++n increments then returns the new value; postfix n++ returns the old value then increments.
  • Compound assignment operators like += are shorthand for x = x + value.
  • Bitwise operators work on individual bits and are essential for low-level programming.
  • The ternary operator ?: is a concise alternative to simple if-else statements.
  • When operator precedence is not obvious, use parentheses to make your intent clear.

4.12 Exercises

  1. What is the difference between 7 / 2 and 7.0 / 2 in C++? Why does it matter?

  2. What does the following code print?

    int a = 10;
    int b = a++;
    int c = ++a;
    std::cout << a << " " << b << " " << c << std::endl;
  3. What is the value of each expression?

    • 17 % 5
    • 20 % 4
    • 3 % 7
    • -17 % 5
  4. What does this expression evaluate to?

    int x = 0;
    bool result = (x != 0) && (100 / x > 5);

    Why does it not crash even though x is 0?

  5. Where is the bug?

    int x = 5;
    if (x = 10) {
        std::cout << "x is 10" << std::endl;
    }
  6. Where is the bug?

    int flags = 10;
    if (flags & 2 == 2) {
        std::cout << "bit is set" << std::endl;
    }
  7. What does this code print?

    int score = 85;
    std::string grade = (score >= 90) ? "A"
                      : (score >= 80) ? "B"
                      : (score >= 70) ? "C"
                      : "F";
    std::cout << grade << std::endl;
  8. Write a short program that asks the user for an integer and prints whether it is even or odd, positive or negative (or zero), using the modulo and comparison operators.

  9. What does this print?

    #include <iostream>
    
    int main() {
        int x = 10;
        x += 5;
        x *= 2;
        x -= 3;
        x /= 4;
        x %= 5;
        std::cout << x << "\n";
        return 0;
    }

    Walk through each line and show the value of x after that line runs.

  10. Think about it: Without using parentheses, what does C++ make of the following expression?

    bool result = a < b && c == d || !e;

    List the operators in the order C++ evaluates them, then rewrite the expression with parentheses that make the precedence explicit. Why is the second form preferable even though both produce the same result?

  11. What does this print?

    #include <iostream>
    
    int main() {
        char a = 'A';
        char b = 'B';
        auto sum = a + b;
        std::cout << sum << " " << sizeof(sum) << "\n";
        return 0;
    }

    What is the deduced type of sum? Why is sizeof(sum) not 1?

  12. Where is the bug?

    #include <iostream>
    
    int main() {
        int          temperature = -5;
        unsigned int threshold   =  0;
        if (temperature < threshold) {
            std::cout << "cold\n";
        } else {
            std::cout << "warm\n";
        }
        return 0;
    }

    Which branch runs, and why? How would you fix the program so that -5 < 0 evaluates the way the reader expects?