⚙️ Unit 03 • C++ Systems Engineering

Operators, Expressions & Precedence

Mastering computational primitives in C++: arithmetic expressions, compound assignments, relational comparisons, logical short-circuiting, unary operations, prefix vs postfix increment semantics (lvalue vs rvalue mechanics), bitwise masking and shifting, ternary conditional operators, compile-time sizeof calculus, and operator precedence and associativity.

➕ Arithmetic & Logic: +, -, *, /, %, &&, ||, !
⚡ Low-Level Bitwise: &, |, ^, ~, <<, >>
🔄 Increment Semantics: Prefix (++x) vs Postfix (x++)
⚖️ Precedence: 16-Tier Associativity Hierarchy
01

Arithmetic & Compound Assignment Operators

In C++, an expression is a sequence of operators and operands that specifies a computation and yields a result value. Arithmetic operators execute fundamental mathematical calculations:

Arithmetic Operators & Integer Division
  • + (Addition), - (Subtraction), * (Multiplication).
  • / (Division): When both operands are integers, C++ performs integer division, truncating all fractional components toward zero (e.g., 7 / 2 yields 3, not 3.5).
  • % (Modulo / Remainder): Returns the integer remainder of division (e.g., 7 % 2 yields 1). Both operands must strictly be integral types; attempting 7.5 % 2 triggers a compile-time error.
Compound Assignment Operators

Combines an arithmetic or bitwise operation with value assignment:

x += y ≡ x = x + y (evaluating x only once).

  • Arithmetic: +=, -=, *=, /=, %=
  • Bitwise: &=, |=, ^=, <<=, >>=
  • Right-to-Left Associativity: a = b = c = 10; assigns 10 to c, then b, then a.
⚠️ The Division by Zero Trap

Dividing an integer by literal zero (x / 0) or modulo zero (x % 0) triggers Undefined Behavior (UB) in C++, typically terminating the program instantly with a CPU Hardware Exception (SIGFPE: Floating Point Exception). In floating-point arithmetic (IEEE 754), dividing by 0.0 results in inf (Infinity) or nan (Not a Number).

02

Relational Operators & Logical Short-Circuiting

Relational and logical operators evaluate conditions to produce a bool result (true or false):

Operator Syntactic Name Evaluation Rule Example (int a=5, b=10)
== Equal To Returns true if both operands possess identical values (a == b) → false
!= Not Equal To Returns true if operand values differ (a != b) → true
<, > Less than, Greater than Strict magnitude comparison (a < b) → true
<=, >= Less/Greater than or equal Non-strict magnitude comparison (a <= 5) → true
&& Logical AND Returns true only if both operands evaluate to true (a > 0 && b > 0) → true
|| Logical OR Returns true if at least one operand evaluates to true (a > 10 || b > 5) → true
! Logical NOT (Unary) Inverts truth value (!true → false, !false → true) !(a == b) → true
⚡ Short-Circuit Evaluation Mechanics

C++ guarantees strict left-to-right evaluation for logical operators, halting execution the instant the final outcome is determined:

  • Logical AND (A && B): If A evaluates to false, the overall expression must be false; expression B is never executed!
  • Logical OR (A || B): If A evaluates to true, the overall expression must be true; expression B is never executed!

Life-saving Idiom (Null-Pointer Guard):
if (ptr != nullptr && ptr->isActive()) { /* Safe! ptr->isActive() is skipped if ptr is null */ }

1
Evaluate Left Operand: (ptr != nullptr)
Compiler checks pointer address in memory. If address is 0x0 (null), result is false.
2
Short-Circuit Decision Gate
Because operation is &&, false AND anything is false. CPU bypasses second operand entirely!
3
Crash Prevented (Zero Segmentation Fault)
Dereferencing null pointer ptr->val is avoided, preserving process runtime stability.
03

Unary, Increment & Decrement Operators (Prefix vs Postfix)

Unary operators operate on a single operand. The most widely used—and nuanced—are the increment (++) and decrement (--) operators:

Prefix Increment (++x)

Increments the operand in memory first, and returns the newly incremented lvalue reference.

  • Mechanism: Increment → Fetch.
  • Return Type: Returns an lvalue (can be assigned to: ++++x; is valid in C++).
  • Performance: Faster for non-primitive classes/iterators because no temporary copy is created.
Postfix Increment (x++)

Makes a temporary copy of the current value, increments the original variable in memory, and returns the old un-incremented copy.

  • Mechanism: Copy → Increment → Return copy.
  • Return Type: Returns an rvalue (temporary value; cannot be chained).
  • Performance: Incurs overhead on complex custom iterators due to temporary object creation.
prefix_vs_postfix.cpp
#include <iostream>

int main() {
    int a = 10;
    int b = 10;

    // Prefix: increments 'a' to 11, then assigns 11 to res1
    int res1 = ++a; 
    std::cout << "Prefix:  res1 = " << res1 << ", a = " << a << "\n"; // 11, 11

    // Postfix: copies old value 10 to res2, then increments 'b' to 11
    int res2 = b++; 
    std::cout << "Postfix: res2 = " << res2 << ", b = " << b << "\n"; // 10, 11

    return 0;
}
04

Bitwise Operators & Low-Level Bit Manipulation

Bitwise operators perform Boolean algebraic operations directly on individual binary bits inside CPU registers, essential for embedded drivers, network protocols, and game physics:

Operator Name Truth Rule Binary Example (x=5 [0101], y=3 [0011])
& Bitwise AND Bit is 1 only if both corresponding bits are 1 0101 & 0011 → 0001 (1)
| Bitwise OR Bit is 1 if at least one corresponding bit is 1 0101 | 0011 → 0111 (7)
^ Bitwise XOR Bit is 1 if corresponding bits are different 0101 ^ 0011 → 0110 (6)
~ Bitwise NOT Unary: inverts every bit (1 → 0, 0 → 1) ~00000101 → 11111010 (-6 in 2's comp)
<< Left Shift Shifts bits left by N positions (Equivalent to multiplying by 2N) 00000101 << 1 → 00001010 (10)
>> Right Shift Shifts bits right by N positions (Equivalent to dividing by 2N) 00000101 >> 1 → 00000010 (2)
bitmasking_techniques.cpp
#include <iostream>
#include <bitset>

// Industry Bitmasking Flags (File Permissions)
constexpr unsigned char READ_PERMISSION    = 0b00000001; // 1
constexpr unsigned char WRITE_PERMISSION   = 0b00000010; // 2
constexpr unsigned char EXECUTE_PERMISSION = 0b00000100; // 4

int main() {
    unsigned char userFlags = 0;

    // 1. SET A BIT (OR operator |)
    userFlags |= (READ_PERMISSION | WRITE_PERMISSION);

    // 2. CHECK A BIT (AND operator &)
    bool canWrite = (userFlags & WRITE_PERMISSION) != 0;

    // 3. CLEAR A BIT (AND with NOT: & ~)
    userFlags &= ~WRITE_PERMISSION;

    // 4. TOGGLE A BIT (XOR operator ^)
    userFlags ^= EXECUTE_PERMISSION;

    std::cout << "Current Permissions Bitmask: " 
              << std::bitset<8>(userFlags) << "\n"; // 00000101
    return 0;
}
05

Conditional (Ternary) & sizeof Operators

Conditional (Ternary) Operator (?:)

C++'s only ternary operator, providing a compact, expression-based conditional:

condition ? expression_if_true : expression_if_false
  • Expression vs Statement: Because it is an expression, it returns a value and can initialize const variables inline: const int max = (a > b) ? a : b;.
  • Type Compatibility: Both branch expressions must possess compatible or convertible data types.
The sizeof Compile-Time Operator

Returns the size in bytes of the object representation of a type or expression:

  • Compile-Time Evaluation: sizeof(expr) evaluates at compile time without executing expr! (e.g., sizeof(i++) does not increment i).
  • Array Dimension Calculus: Calculates fixed array element counts:
    size_t count = sizeof(arr) / sizeof(arr[0]);
06

Operator Precedence & Associativity Hierarchy

When an expression contains multiple operators, Precedence dictates the binding order of operators, and Associativity dictates the direction of evaluation (Left-to-Right or Right-to-Left) when operators share equal precedence:

Tier Operators Category Description Associativity
Level 1 (Highest) :: Scope Resolution Left-to-Right
Level 2 (), [], ., ->, x++, x-- Member access, subscripts, postfix increment Left-to-Right
Level 3 ++x, --x, +, -, !, ~, *ptr, &var, sizeof Unary operators, prefix increment, address-of Right-to-Left
Level 4 .*, ->* Pointer-to-member Left-to-Right
Level 5 *, /, % Multiplication, Division, Modulo Left-to-Right
Level 6 +, - Binary Addition, Subtraction Left-to-Right
Level 7 <<, >> Bitwise Shift (and Stream I/O) Left-to-Right
Level 8 <, <=, >, >= Relational comparisons Left-to-Right
Level 9 ==, != Equality and Inequality Left-to-Right
Level 10 & Bitwise AND Left-to-Right
Level 11 ^ Bitwise XOR Left-to-Right
Level 12 | Bitwise OR Left-to-Right
Level 13 && Logical AND Left-to-Right
Level 14 || Logical OR Left-to-Right
Level 15 ?: Conditional Ternary Right-to-Left
Level 16 =, +=, -=, *=, /=, &=, etc. Direct and compound assignments Right-to-Left
Level 17 (Lowest) , Comma Operator Left-to-Right
⚠️ The Golden Rule of Parentheses

Never rely on colleagues or compiler edge cases to memorize complex precedence chains! Writing if ((flags & MASK) == TARGET) is clear and unambiguous. Writing if (flags & MASK == TARGET) causes a critical bug because == has higher precedence than &, evaluating to flags & (MASK == TARGET)!

07

Viva Voce & Engineering Interview Questions

Q1 • Semantics Why is prefix increment (++x) preferred over postfix increment (x++) in C++?
Prefix increment (++x) mutates the variable and returns the modified lvalue reference directly, requiring no extra memory or temporary copy. Postfix increment (x++) must construct a temporary copy of the old value, mutate the original variable, and return the temporary copy by value. For complex user-defined types (like STL container iterators), postfix increment incurs an avoidable performance and memory overhead.
Q2 • Execution How does Short-Circuit Evaluation protect against null-pointer dereferencing?
In a logical AND expression (expr1 && expr2), if expr1 evaluates to false, C++ guarantees that expr2 will not be evaluated at all. When written as if (ptr != nullptr && ptr->value > 0), if ptr is null, the left condition fails and the right condition (which would dereference a null pointer and trigger a segmentation fault) is safely bypassed.
Q3 • Bitwise How do bitwise shift operators (<< and >>) relate mathematically to arithmetic operations?
Left-shifting an unsigned integer by N bits (x << N) multiplies the value by 2N (assuming no overflow). Right-shifting an unsigned integer by N bits (x >> N) divides the value by 2N, discarding the remainder. In low-level systems programming, bit shifts execute in a single CPU clock cycle, providing maximum performance.
Q4 • Compile Time Does the sizeof operator execute runtime side effects inside its operand?
No. sizeof is completely evaluated at compile time by the compiler inspecting the type yielded by the expression. Therefore, an expression with side effects such as sizeof(i++) does not actually execute i++ at runtime; the value of i remains completely unchanged.
Q5 • Precedence Trap Why does the expression flags & MASK == 0 fail to evaluate as expected without parentheses?
According to the C++ operator precedence hierarchy, the equality operator (==, Level 9) has higher precedence than the bitwise AND operator (&, Level 10). Consequently, the compiler parses the expression as flags & (MASK == 0) instead of the intended (flags & MASK) == 0, yielding unexpected logic errors.
Q6 • Arithmetic What is the result of -7 % 3 and 7 % -3 according to modern C++ standards?
Since C++11, integer division truncates toward zero, and the sign of the modulo operator result is strictly governed by the formula: (a / b) * b + (a % b) == a. This guarantees that the sign of the remainder matches the sign of the numerator (left operand). Thus, -7 % 3 yields -1, while 7 % -3 yields 1.
Q7 • Ternary Operator Can the Conditional Ternary operator (?:) be assigned to an lvalue?
Yes! In C++, if both the second and third expressions of a ternary operator are lvalues of the exact same type, the result of the ternary operator is also an lvalue. This allows syntax like (a > b ? a : b) = 100;, which assigns 100 directly to whichever variable is greater.
Q8 • Bitwise Logic How does the Bitwise XOR operator (^) enable swapping two integers without a temporary variable?
The XOR swap algorithm operates via the algebraic property that x ^ x = 0 and x ^ 0 = x. By executing: a = a ^ b; b = a ^ b; a = a ^ b;, the variables swap their values in place without allocating extra memory or registers.