Expressions
Expression: A combination of operands and operators that produces a result.
Operand: A value or expression on which an operator acts.
Operator: A symbol that performs an operation on one or more operands.
int a = 10;
int b = 20;
int result = a + b;
Here:
aandbare operands.+is an operator.a + bis an expression.
4.1 Fundamentals
1) Basic Concepts
Operators can be classified by the number of operands they use.
| Type | Example | Meaning |
|---|---|---|
| Unary | -a | One operand |
| Binary | a + b | Two operands |
| Ternary | cond ? a : b | Three operands |
| Function call | f(a, b) | Variable number of arguments |
Unary Operator
int value = 10;
int negative = -value;
Binary Operator
int a = 10;
int b = 20;
int sum = a + b;
Conditional Operator
int a = 10;
int b = 20;
int larger = (a > b) ? a : b;
Operand Conversions
Operands may be converted to another type before an operation is performed.
int i = 10;
double d = 3.5;
double result = i + d;
Conceptually:
10 -> 10.0
10.0 + 3.5 = 13.5
The int operand is converted to double.
Integral Promotion
Small integral types are often promoted to int.
char a = 10;
char b = 20;
auto result = a + b;
The arithmetic is normally performed using int, not char.
Overloaded Operators
Operators may also be defined for class types.
std::string a = "Hello ";
std::string b = "World";
std::string result = a + b;
For std::string, + means string concatenation.
Operator overloading does not change an operator's:
- precedence
- associativity
- number of operands
Lvalues and Rvalues
Lvalue: An expression that identifies an object.
Rvalue: An expression primarily used for its value.
int value = 10;
value = 20;
value is an lvalue because it identifies an object that can be modified.
The literal:
20
is used only as a value.
Lvalue-to-Rvalue Conversion
When an object's stored value is needed, the lvalue is converted to its value.
int a = 10;
int b = a;
In:
b = a;
the value stored in a is read.
Lvalue Requirement
The left side of assignment must normally be a modifiable lvalue.
Valid:
int value = 10;
value = 20;
Invalid:
// 10 = 20;
A literal does not identify a modifiable object.
2) Precedence and Associativity
Precedence: Determines how operators of different precedence levels are grouped.
Associativity: Determines grouping when operators have the same precedence.
Operator Precedence
Multiplication has higher precedence than addition.
int result = 5 + 10 * 2;
This is interpreted as:
int result = 5 + (10 * 2);
Result:
25
Not:
(5 + 10) * 2
Parentheses
Parentheses override normal precedence.
int result = (5 + 10) * 2;
Result:
30
Use parentheses whenever they make the intended grouping clearer.
Dereference and Arithmetic
Consider:
int values[] = {0, 2, 4, 6, 8};
int last = *(values + 4);
Result:
8
This is different from:
int last = *values + 4;
which means:
int last = values[0] + 4;
Result:
4
The parentheses determine whether the pointer is advanced before dereferencing.
Associativity
Addition is left associative.
a + b + c
is grouped as:
(a + b) + c
Assignment is right associative.
a = b = 10;
is grouped as:
a = (b = 10);
Both a and b become 10.
I/O Associativity
Stream operators are left associative.
std::cout << a << b;
is grouped as:
(std::cout << a) << b;
Likewise:
std::cin >> a >> b;
is grouped as:
(std::cin >> a) >> b;
3) Order of Evaluation
Order of Evaluation: Determines when individual operands are evaluated.
Precedence and associativity determine grouping, but they do not generally determine which operand is evaluated first.
int result = f1() * f2();
Both functions must run before multiplication can occur, but the language rules applicable to the expression determine whether one must run before the other.
Do not rely on an evaluation order unless the language guarantees it.
Guaranteed Evaluation Order
Several operators have important sequencing behavior.
Logical AND &&
The left operand is evaluated first.
The right operand is evaluated only if the left operand is true.
if (ptr != nullptr && *ptr > 0)
{
// safe to dereference ptr
}
If ptr == nullptr, *ptr is never evaluated.
Logical OR ||
The right operand is evaluated only if the left operand is false.
if (value == 0 || 100 / value > 2)
{
// ...
}
If value == 0, the division is not evaluated.
Conditional Operator
Only one result branch is evaluated.
int result =
value != 0
? 100 / value
: 0;
Comma Operator
The left operand is evaluated before the right operand.
int value = (a = 10, a + 5);
a = 10 happens first.
Then:
a + 5
produces the result.
4.2 Arithmetic Operators
Arithmetic operators perform mathematical operations on numeric values.
| Operator | Meaning |
|---|---|
+expr | Unary plus |
-expr | Unary minus |
a * b | Multiplication |
a / b | Division |
a % b | Remainder |
a + b | Addition |
a - b | Subtraction |
Basic Arithmetic
int a = 10;
int b = 3;
int add = a + b;
int subtract = a - b;
int multiply = a * b;
int divide = a / b;
int remainder = a % b;
Results:
add = 13
subtract = 7
multiply = 30
divide = 3
remainder = 1
Integer Division
When both operands are integers, the fractional part is discarded.
int result = 21 / 6;
Result:
3
To obtain a floating-point result:
double result = 21.0 / 6.0;
Result:
3.5
Mixed arithmetic also works:
double result = 21 / 6.0;
The integer operand is converted to double.
Remainder Operator %
The remainder operator works with integral operands.
int remainder = 21 % 6;
Result:
3
A common use is testing whether a number is even.
if (value % 2 == 0)
{
std::cout << "even\n";
}
Division by Zero
Integer division by zero is invalid.
int divisor = 0;
// Undefined behavior
// int result = 10 / divisor;
Check the divisor first.
if (divisor != 0)
{
int result = 10 / divisor;
}
Overflow
A value outside the representable range of a signed integer type can cause undefined behavior.
#include <limits>
int value = std::numeric_limits<int>::max();
// Do not do this:
// ++value;
Choose types and ranges carefully when large results are possible.
4.3 Logical and Relational Operators
Relational operators compare values.
Logical operators combine or invert Boolean conditions.
Relational Operators
| Operator | Meaning |
|---|---|
< | Less than |
<= | Less than or equal |
> | Greater than |
>= | Greater than or equal |
== | Equal |
!= | Not equal |
int a = 10;
int b = 20;
bool r1 = a < b;
bool r2 = a == b;
bool r3 = a != b;
Results:
r1 = true
r2 = false
r3 = true
Logical Operators
| Operator | Meaning | ||
|---|---|---|---|
! | NOT | ||
&& | AND | ||
| ` | ` | OR |
Logical AND
int age = 25;
if (age >= 20 && age < 30)
{
std::cout << "twenties\n";
}
Both conditions must be true.
Logical OR
char c = 'y';
if (c == 'y' || c == 'Y')
{
std::cout << "yes\n";
}
At least one condition must be true.
Logical NOT
bool running = false;
if (!running)
{
std::cout << "stopped\n";
}
Short-Circuit Evaluation
Short circuiting allows safe checks.
std::vector<int> values;
if (!values.empty() && values[0] > 0)
{
std::cout << values[0];
}
values[0] is evaluated only when the vector is not empty.
Relational Chaining
Do not write mathematical-style chained comparisons.
Wrong:
if (0 < value < 100)
{
// ...
}
C++ evaluates it conceptually as:
(0 < value) < 100
The first comparison produces true or false, which is then compared with 100.
Write:
if (0 < value && value < 100)
{
// ...
}
Boolean Conditions
Numeric values can be converted to bool.
int value = 10;
if (value)
{
std::cout << "nonzero\n";
}
Zero means false.
Nonzero means true.
Prefer:
if (value)
{
// ...
}
over:
if (value == true)
{
// ...
}
when the intention is simply to test whether the value is nonzero.
4.4 Assignment Operators
Assignment Operator =: Stores a value in a modifiable object.
int value = 0;
value = 42;
After assignment:
value = 42
Assignment Conversion
The right operand is converted to the type of the left operand when needed.
int value = 3.14;
The stored integer value becomes:
3
The fractional part is discarded.
Braced Assignment
A braced initializer can be used in assignment where supported.
std::vector<int> values;
values = {1, 2, 3, 4};
The vector is replaced by the listed elements.
Assignment Is Right Associative
int a = 0;
int b = 0;
a = b = 42;
Equivalent to:
a = (b = 42);
Result:
a = 42
b = 42
Assignment in Conditions
Assignment produces a result and can therefore appear in a condition.
int value = 0;
if ((value = get_value()) != 0)
{
// ...
}
Parentheses make the intended assignment explicit.
Assignment vs. Equality
These operators have completely different meanings.
value = 10;
means:
assign 10 to value
Whereas:
value == 10
means:
test whether value equals 10
A common mistake is:
if (value = 10)
{
// ...
}
This assigns 10 to value, after which the condition evaluates as true.
Usually the intended expression is:
if (value == 10)
{
// ...
}
Compound Assignment
Compound assignment combines an operation with assignment.
value += 5;
is approximately equivalent to:
value = value + 5;
Important compound assignment operators:
+= -= *= /= %=
<<= >>= &= ^= |=
Example:
int value = 10;
value += 5;
value *= 2;
Result:
30
A compound assignment evaluates its left operand only once.
4.5 Increment and Decrement Operators
Increment ++: Adds one.
Decrement --: Subtracts one.
Both have prefix and postfix forms.
Prefix Increment
int value = 10;
int result = ++value;
First value becomes 11.
Then that modified value is used.
value = 11
result = 11
Postfix Increment
int value = 10;
int result = value++;
The old value is used first.
Then value is incremented.
value = 11
result = 10
The same distinction applies to --.
Prefer Prefix When the Old Value Is Not Needed
for (auto it = values.begin();
it != values.end();
++it)
{
// ...
}
Prefix increment directly advances the iterator.
Dereference and Postfix Increment
A commonly used iterator expression is:
*iter++
Postfix ++ has higher precedence than *, so it is interpreted as:
*(iter++)
The current element is accessed, then the iterator advances.
Example:
std::vector<int> values{10, 20, 30};
auto it = values.begin();
int first = *it++;
Afterward:
first = 10
it points to 20
This is conceptually similar to:
int first = *it;
++it;
Avoid Complicated Side Effects
Expressions that both modify and use the same variable can be difficult to reason about.
Prefer:
std::cout << value << '\n';
++value;
instead of combining unrelated side effects into a complex expression.
Simple expressions are easier to verify and maintain.
4.6 The Member Access Operators
C++ provides two common member-access operators.
Dot Operator .
Used with an object.
std::string text = "hello";
auto size = text.size();
Arrow Operator ->
Used through a pointer.
std::string text = "hello";
std::string* p = &text;
auto size = p->size();
This is equivalent to:
auto size = (*p).size();
Therefore:
object.member
pointer->member
are the standard access forms.
Dot and Dereference Precedence
Parentheses are required here:
(*p).size()
Without parentheses:
// *p.size()
the member-access operator would bind before dereference.
The arrow operator avoids this awkward syntax:
p->size();
4.7 The Conditional Operator
Conditional Operator ?:: Embeds a simple if-else choice inside an expression.
Syntax:
condition ? expression1 : expression2
If the condition is true, expression1 is evaluated.
Otherwise, expression2 is evaluated.
Basic Example
int grade = 85;
std::string result =
grade < 60 ? "fail" : "pass";
Result:
pass
Equivalent logic:
std::string result;
if (grade < 60)
{
result = "fail";
}
else
{
result = "pass";
}
Only One Branch Is Evaluated
int divisor = 0;
int result =
divisor != 0
? 100 / divisor
: 0;
When divisor == 0, the division expression is not evaluated.
Nested Conditional Operator
Conditional expressions may be nested.
std::string final_grade =
grade > 90
? "high pass"
: grade < 60
? "fail"
: "pass";
Conceptually:
grade > 90
-> high pass
otherwise grade < 60
-> fail
otherwise
-> pass
Nested conditional expressions quickly become difficult to read.
Use them only for simple decisions.
Conditional Operator in Output
Because ?: has relatively low precedence, parentheses are useful.
std::cout
<< (grade < 60 ? "fail" : "pass")
<< '\n';
4.8 The Bitwise Operators
Bitwise operators manipulate individual bits of integral values.
| Operator | Meaning | |
|---|---|---|
~ | Bitwise NOT | |
<< | Left shift | |
>> | Right shift | |
& | Bitwise AND | |
^ | Bitwise XOR | |
| ` | ` | Bitwise OR |
Unsigned types are generally preferable for bit manipulation.
Bit Representation Example
Suppose:
unsigned value = 0b00001101;
Binary:
00001101
Decimal:
13
Left Shift <<
unsigned value = 1u;
unsigned result = value << 3;
Binary:
00000001
<< 3
00001000
Result:
8
For non-overflowing unsigned values, shifting left by one position corresponds to multiplication by two.
Right Shift >>
unsigned value = 8u;
unsigned result = value >> 2;
Binary:
00001000
>> 2
00000010
Result:
2
Bitwise AND &
A result bit is 1 only if both input bits are 1.
1100
1010
----
1000
Example:
unsigned a = 0b1100;
unsigned b = 0b1010;
unsigned result = a & b;
result is:
1000
Bitwise OR |
A result bit is 1 when either input bit is 1.
1100
1010
----
1110
unsigned result = a | b;
Bitwise XOR ^
A result bit is 1 when exactly one operand bit is 1.
1100
1010
----
0110
unsigned result = a ^ b;
Bitwise NOT ~
Inverts each bit.
Conceptually:
00001100
~
11110011
unsigned result = ~value;
The exact visible bit pattern depends on the width of the type.
Bit Masks
A mask is commonly used to set, clear, or test individual flags.
Suppose bit 3 represents a feature.
constexpr unsigned feature_mask = 1u << 3;
Binary:
00001000
Set a Bit
Use OR:
flags |= feature_mask;
Clear a Bit
Use AND with an inverted mask:
flags &= ~feature_mask;
Test a Bit
if (flags & feature_mask)
{
std::cout << "feature enabled\n";
}
Toggle a Bit
Use XOR:
flags ^= feature_mask;
This pattern is commonly used for:
- state flags
- permission flags
- input modifiers
- hardware registers
- compact Boolean state
4.9 The sizeof Operator
sizeof: Returns the number of bytes required to represent a type or object.
Two common forms are:
sizeof(type)
sizeof expression
Type Size
std::cout << sizeof(int) << '\n';
std::cout << sizeof(double) << '\n';
The actual values depend on the implementation.
Object Size
int value = 10;
auto size = sizeof value;
Equivalent to:
sizeof(int)
sizeof(char)
sizeof(char) is always 1.
static_assert(sizeof(char) == 1);
This means one C++ byte, not necessarily eight bits.
sizeof Does Not Evaluate Its Operand
int value = 10;
sizeof(++value);
value is not incremented.
It remains:
10
Pointer Size
int* p = nullptr;
auto pointer_size = sizeof p;
This gives the size of the pointer itself.
It does not give the size of int.
To obtain the pointed-to type's size:
auto object_size = sizeof *p;
The pointer is not dereferenced at runtime because the operand of sizeof is unevaluated.
Array Size
sizeof applied directly to an array returns the size of the complete array.
int values[10];
auto bytes = sizeof values;
The number of elements can therefore be calculated as:
constexpr std::size_t count =
sizeof(values) / sizeof(values[0]);
For ten ints:
count = 10
In modern C++ the library alternative is usually clearer:
#include <iterator>
auto count = std::size(values);
string and vector
std::string text = "hello";
std::vector<int> values(1000);
sizeof(text);
sizeof(values);
returns the size of the string or vector object itself.
It does not represent the total dynamic storage occupied by all their elements.
4.10 Comma Operator
The comma operator evaluates its left operand first, discards that result, and then evaluates the right operand.
int a = 0;
int result = (a = 10, a + 5);
Evaluation:
a = 10
a + 5 = 15
Therefore:
a = 10
result = 15
Common Use in for
The comma operator can be useful when multiple expressions must execute in a loop header.
for (int i = 0, j = 10;
i < j;
++i, --j)
{
std::cout << i << ' ' << j << '\n';
}
Both:
++i
and:
--j
run after each loop iteration.
Use the comma operator sparingly because complex comma expressions can reduce readability.
4.11 Type Conversions
Type Conversion: Changes a value from one type to another.
Conversions may be:
- implicit
- explicit
Implicit Conversion
The compiler performs an implicit conversion automatically.
int i = 42;
double d = i;
Conceptually:
42 -> 42.0
Mixed-Type Expressions
int i = 10;
double d = 3.5;
auto result = i + d;
i is converted to double.
The result is:
13.5
Assignment Conversion
double value = 3.99;
int integer = value;
The fractional part is discarded.
integer = 3
Condition Conversion
Arithmetic and pointer values may be converted to bool.
int value = 10;
if (value)
{
// value converts to true
}
For pointers:
int* p = nullptr;
if (p)
{
// executed only if p is non-null
}
1) The Arithmetic Conversions
Arithmetic operations commonly convert operands to a common type.
int i = 10;
double d = 2.5;
auto result = i * d;
The integer is converted to double.
Integral Promotions
Small integral types are promoted before arithmetic.
short a = 10;
short b = 20;
auto result = a + b;
result is typically an int.
Similarly:
char a = 10;
char b = 20;
auto result = a + b;
The arithmetic is performed using promoted integral values.
Signed and Unsigned Values
Mixing signed and unsigned arithmetic requires care.
int signed_value = -1;
unsigned unsigned_value = 1;
auto result =
signed_value + unsigned_value;
The common type is determined by the arithmetic conversion rules.
Negative signed values may therefore become large unsigned values depending on the involved types.
Prefer avoiding unnecessary signed/unsigned mixing.
2) Other Implicit Conversions
Array-to-Pointer Conversion
In most expressions, an array converts to a pointer to its first element.
int values[10];
int* p = values;
Equivalent to:
int* p = &values[0];
Important contexts such as sizeof do not perform this conversion.
sizeof(values);
returns the size of the entire array.
Pointer to bool
int value = 10;
int* p = &value;
if (p)
{
// non-null pointer
}
A null pointer converts to false.
A non-null pointer converts to true.
Conversion to const
A pointer to non-const can be converted to a pointer to const.
int value = 10;
int* p = &value;
const int* cp = p;
Modification through cp is not allowed.
// *cp = 20;
Removing low-level const is not performed implicitly.
void*
A pointer to an object can be converted to void*.
int value = 10;
void* p = &value;
The original type information is not available through void*.
Class-Type Conversions
Classes can define conversions that are used implicitly.
A familiar example is stream state testing.
int value = 0;
while (std::cin >> value)
{
// ...
}
The stream expression can be tested as a Boolean condition to determine whether input succeeded.
3) Explicit Conversions
An explicit conversion is requested directly by the programmer.
Modern C++ provides named casts:
static_cast
dynamic_cast
const_cast
reinterpret_cast
static_cast
static_cast is commonly used for well-defined explicit conversions.
Arithmetic Conversion
int total = 10;
int count = 4;
double average =
static_cast<double>(total) / count;
Without the cast:
total / count
would perform integer division first.
With the cast:
10.0 / 4 = 2.5
void* Conversion
int value = 10;
void* vp = &value;
int* ip =
static_cast<int*>(vp);
This is valid when vp actually came from a pointer to int.
const_cast
const_cast changes low-level const qualification.
const char* p = "hello";
char* q =
const_cast<char*>(p);
Removing const does not make an originally const object safely modifiable.
Writing through such a pointer when the underlying object is actually const results in undefined behavior.
Use const_cast only when interfacing with code whose const interface is inappropriate but whose actual object is safely mutable.
reinterpret_cast
reinterpret_cast requests a low-level reinterpretation.
std::uintptr_t address =
reinterpret_cast<std::uintptr_t>(p);
It is intended for specialized low-level operations.
Its meaning depends strongly on:
- representation
- platform
- alignment
- object model rules
Avoid it in normal application code unless low-level representation manipulation is actually required.
dynamic_cast
dynamic_cast is used with polymorphic class hierarchies for checked run-time conversions.
Derived* derived =
dynamic_cast<Derived*>(base);
This topic becomes important when inheritance and virtual functions are introduced.
Old-Style Casts
C-style cast:
double result = (double)value;
Function-style cast:
double result = double(value);
Modern C++ generally prefers named casts because the intended kind of conversion is explicit.
Prefer:
double result =
static_cast<double>(value);
4.12 Operator Precedence Table
The complete precedence table is useful as a reference, but the most important precedence groups to remember initially are:
| Precedence | Operators | Example |
|---|---|---|
| High | (), [], ., ->, postfix ++ -- | p->member |
prefix ++ --, !, ~, unary + -, *, &, sizeof | *p | |
* / % | a * b | |
+ - | a + b | |
<< >> | a << 2 | |
< <= > >= | a < b | |
== != | a == b | |
& | a & b | |
^ | a ^ b | |
| | a | b | |
&& | a && b | |
|| | a || b | |
?: | cond ? a : b | |
| assignment operators | a = b | |
| Low | , | a, b |
Important Examples
Multiplication before addition:
a + b * c
means:
a + (b * c)
Relational before logical AND:
a < b && c < d
means:
(a < b) && (c < d)
Equality before logical OR:
a == b || c == d
means:
(a == b) || (c == d)
Assignment is right associative:
a = b = c;
means:
a = (b = c);
When an expression is difficult to read, do not depend on memorizing the entire precedence table.
Use parentheses:
result =
(a + b) * (c - d);
Clear grouping is more important than writing the shortest expression.
Essential Study Checklist
- An expression combines operands and operators to produce a result.
- Unary, binary, and conditional operators use different numbers of operands.
- Operators may implicitly convert their operands.
- An lvalue identifies an object; an rvalue is commonly used for its value.
- Precedence determines grouping between operators of different precedence.
- Associativity determines grouping among operators of equal precedence.
- Parentheses override normal grouping.
- Precedence does not generally determine operand evaluation order.
&&,||,?:, and the comma operator have important sequencing behavior.&&and||use short-circuit evaluation.- Integer division discards the fractional part.
%computes the integer remainder.- Relational and logical operators produce Boolean results.
- Do not write mathematical chained comparisons such as
0 < x < 10. =performs assignment;==tests equality.- Assignment operators are right associative.
- Compound assignment combines an operation with assignment.
- Prefix
++returns the updated value; postfix++uses the previous value. *iter++accesses the current element and then advances the iterator.object.memberaccesses a member through an object.pointer->memberaccesses a member through a pointer.?:is useful for simpleif-elseexpressions.- Bitwise operations work on individual bits of integral values.
- Use masks with
|,&,~, and^to manage flags. - Prefer unsigned types for ordinary bit manipulation.
sizeofreturns the storage size of a type or object.- The operand of
sizeofis not evaluated. sizeof(array)gives the size of the complete built-in array.- The comma operator evaluates left before right and returns the right result.
- Implicit conversions occur automatically in many expressions.
- Arithmetic operands are often converted to a common type.
- Small integral types undergo integral promotion.
- Arrays normally convert to pointers to their first element.
- Values and pointers can be converted to
bool. static_castis the normal named cast for explicit ordinary conversions.const_castchanges const qualification.reinterpret_castis reserved for low-level reinterpretation.dynamic_castis used for checked conversions in polymorphic hierarchies.- Prefer named casts over C-style casts.
- Use parentheses when operator precedence is not immediately obvious.