Arithmetic, and the Integer Division Trap
The five arithmetic operators are + - * / %, and four of them behave exactly as you expect. The fifth, /, has a rule that catches nearly every beginner at least once: if both operands are integers, C++ performs integer division and throws the fractional part away. 7 / 2 is 3, not 3.5. The result is not rounded — it is truncated toward zero.
The reason is that C++ chooses the operation based on the operand types, not on what you plan to do with the answer. Two ints go in, so integer division comes out, and only afterwards is the result converted to whatever you assigned it to. Declaring the target as double changes nothing, because the damage was done before the assignment.
This shows up constantly in real code. An average of marks, a percentage, a progress bar — every one of them is a division, and every one of them silently reports the wrong number if both operands are integers. The fix is to make at least one side a floating-point value, either by writing the literal with a decimal point (2.0) or by casting one operand with static_cast<double>.
% is the remainder operator and works only on integers; for floating-point remainders you need std::fmod from <cmath>. Its behaviour with negatives is worth pinning down: since C++11 the result takes the sign of the left operand, so -7 % 3 is −1, not 2. If you need a genuinely non-negative index — a very common need when wrapping around a circular buffer or an array — write ((a % n) + n) % n.
Finally, dividing an integer by zero is undefined behaviour, not an exception you can catch. Your program will most likely crash, and "most likely" is doing real work in that sentence. Guard the divisor yourself before dividing.
#include <iostream>
#include <cmath>
int main() {
int a = 10, b = 3;
std::cout << a + b << '\n'; // 13
std::cout << a - b << '\n'; // 7
std::cout << a * b << '\n'; // 30
std::cout << a / b << '\n'; // 3 <- truncated, not 3.333
std::cout << a % b << '\n'; // 1
// The average bug
int marks = 7, subjects = 2;
double wrong = marks / subjects; // 3.0
double right = static_cast<double>(marks) / subjects; // 3.5
std::cout << wrong << ' ' << right << '\n';
// Remainder with negatives
std::cout << (-7 % 3) << '\n'; // -1
int n = 3, i = -7;
std::cout << ((i % n) + n) % n << '\n'; // 2 <- always non-negative
// Floating-point remainder needs fmod
std::cout << std::fmod(7.5, 2.0) << '\n'; // 1.5
// Guard the divisor yourself
int divisor = 0;
if (divisor != 0) std::cout << a / divisor << '\n';
} - Compound assignment (
+=,-=,*=,/=,%=) is shorthand:total += 5meanstotal = total + 5. It obeys exactly the same rules, sodouble avg = 0; avg /= 2;is fine butint x = 7; x /= 2;still truncates to 3.
Comparison, Logic and Short-Circuiting
The comparison operators == != < > <= >= all produce a bool. The logical operators combine those bools: && is and, || is or, ! is not.
The behaviour of && and || that you must understand is short-circuit evaluation. For A && B, if A is false the whole expression is already false, so C++ guarantees it will not evaluate B at all. For A || B, if A is true, B is skipped. This is a guarantee in the standard, not an optimisation the compiler might or might not apply, which means you can safely rely on it to protect the right-hand side.
That is why the idiom if (ptr != nullptr && ptr->value > 10) is correct and safe. If the pointer is null, the second half never runs and never dereferences it. Write the two conditions the other way round and your program crashes on exactly the input the check was meant to handle. The same pattern guards array access: if (i < v.size() && v[i] == target).
Two mistakes are worth naming. The first is writing = where you meant ==. if (x = 5) assigns 5 to x and then tests 5, which is non-zero and therefore true, so the branch always runs. It compiles; -Wall warns about it, which is another reason to leave warnings on. The second is chained comparison: if (1 < x < 10) looks mathematical and is meaningless. It evaluates 1 < x to a bool, converts that bool to 0 or 1, and compares it with 10 — which is always true. Write if (x > 1 && x < 10).
Prefix and postfix increment are the last pair here. ++i increments and gives you the new value; i++ gives you the old value and then increments. For a plain int the compiler generates identical code either way, so use whichever reads better. For iterators and other class types, i++ must construct and return a copy of the old object before advancing, so ++i is the habit to build.
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {4, 8, 15};
std::size_t i = 5;
// Short-circuit protects the second condition
if (i < v.size() && v[i] == 15) {
std::cout << "found\n";
} // v[5] is never evaluated — no out-of-bounds read
int* p = nullptr;
if (p != nullptr && *p > 10) { // safe
std::cout << *p << '\n';
}
// The two classic mistakes
int x = 3;
// if (x = 5) { } // assigns, always true — meant ==
// if (1 < x < 10) { } // always true — meant x > 1 && x < 10
if (x > 1 && x < 10) std::cout << "in range\n";
// Prefix vs postfix
int a = 5;
std::cout << a++ << '\n'; // prints 5, a is now 6
std::cout << ++a << '\n'; // prints 7
} - A defensive habit some teams adopt is writing constants on the left:
if (5 == x). Then a slipped=becomesif (5 = x), which will not compile. It reads awkwardly, so many people prefer to rely on-Wallinstead — but it is worth recognising when you see it.
Precedence: When to Reach for Parentheses
Precedence decides which operator grabs its operands first when you do not use brackets. C++ has more precedence levels than any human reliably memorises, so the working advice is not "learn the table" — it is "learn the handful that actually cause bugs, and bracket everything else".
The ones below are the levels people genuinely rely on, roughly from tightest to loosest. Note where the logical and bitwise operators sit: && binds tighter than ||, which is why a || b && c means a || (b && c). And the bitwise operators sit below the comparisons, which is the source of the flags & MASK == 0 bug from the previous section.
One precedence surprise specific to C++ output: << binds tighter than the comparison operators. That makes std::cout << x > y parse as (std::cout << x) > y, which either fails to compile or prints the wrong thing. Any comparison or ternary inside a cout chain needs its own brackets.
++ --(postfix), function calls,[],.and->++ --(prefix), unary-,!,~, casts,sizeof* / %, then+ -<<and>>— tighter than every comparison, which trips upcoutchains< <= > >=, then== !=&, then^, then|— all looser than==&&, then||?:and the assignment family= += -=— loosest of all
Bitwise Operators and Where They Earn Their Keep
Bitwise operators work on the individual binary digits of an integer rather than on its value as a whole. & is bitwise and, | is bitwise or, ^ is exclusive or, ~ flips every bit, and << and >> shift the bits left or right.
They look like a curiosity until you meet the two places they are genuinely the right tool. The first is flags: when an object has a set of independent yes/no properties, packing them into the bits of one integer lets you store and pass them as a single value. Graphics APIs, file-open modes and permission systems all do this, which is why you see calls like open(path, READ | WRITE). Setting a flag is value |= FLAG, testing one is (value & FLAG) != 0, and clearing one is value &= ~FLAG.
The second is competitive programming and interview problems, where a handful of bit tricks turn a loop into a single expression. n & 1 tells you whether n is odd. n > 0 && (n & (n - 1)) == 0 tests whether n is a power of two, because a power of two has exactly one set bit and subtracting one flips it and everything below it. XOR-ing every element of an array finds the one value that appears an odd number of times, because x ^ x is 0 and x ^ 0 is x.
Two traps. First, precedence: &, | and ^ bind more loosely than ==, so if (flags & MASK == 0) is parsed as flags & (MASK == 0) and is almost certainly not what you meant. Always parenthesise the bitwise part. Second, shifting: shifting an integer by more bit positions than the type has, or by a negative amount, is undefined behaviour. Shifting a 32-bit int left by 32 is not "zero", it is a bug. When you need 64-bit shifts, start from a 64-bit value such as 1LL << 40.
The ternary operator condition ? a : b belongs in this lesson too. It is an expression rather than a statement, which is exactly why it exists — it lets you initialise a const variable conditionally, something a plain if cannot do. Keep it to a single simple choice; nesting ternaries inside ternaries is where readable code goes to die.
#include <iostream>
#include <string>
#include <vector>
// Flags packed into one integer
constexpr int CAN_READ = 1; // 0001
constexpr int CAN_WRITE = 2; // 0010
constexpr int CAN_ADMIN = 4; // 0100
int main() {
int a = 5; // 0101
int b = 3; // 0011
std::cout << (a & b) << '\n'; // 1 0001
std::cout << (a | b) << '\n'; // 7 0111
std::cout << (a ^ b) << '\n'; // 6 0110
std::cout << (a << 1) << '\n'; // 10 1010
std::cout << (a >> 1) << '\n'; // 2 0010
int perms = CAN_READ | CAN_WRITE;
bool isAdmin = (perms & CAN_ADMIN) != 0; // note the parentheses
perms |= CAN_ADMIN; // grant
perms &= ~CAN_WRITE; // revoke
std::cout << isAdmin << ' ' << perms << '\n'; // 0 5
// Interview classics
int n = 64;
bool odd = (n & 1) != 0;
bool powerOfTwo = n > 0 && (n & (n - 1)) == 0;
std::cout << odd << ' ' << powerOfTwo << '\n'; // 0 1
std::vector<int> nums = {4, 1, 2, 1, 2};
int unique = 0;
for (int x : nums) unique ^= x;
std::cout << unique << '\n'; // 4
// Ternary: an expression, so it can initialise a const
int age = 20;
const std::string status = (age >= 18) ? "Adult" : "Minor";
std::cout << status << '\n';
} &and&&are different operators and are not interchangeable.&works bit by bit and always evaluates both sides;&&works on truth values and short-circuits. Writingif (ptr & ptr->value)instead of&&removes the protection you were relying on.
