본문으로 건너뛰기

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:

  • a and b are operands.
  • + is an operator.
  • a + b is an expression.

4.1 Fundamentals

1) Basic Concepts

Operators can be classified by the number of operands they use.

TypeExampleMeaning
Unary-aOne operand
Binarya + bTwo operands
Ternarycond ? a : bThree operands
Function callf(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.

OperatorMeaning
+exprUnary plus
-exprUnary minus
a * bMultiplication
a / bDivision
a % bRemainder
a + bAddition
a - bSubtraction

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

OperatorMeaning
<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

OperatorMeaning
!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.

OperatorMeaning
~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:

PrecedenceOperatorsExample
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 operatorsa = 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

  1. An expression combines operands and operators to produce a result.
  2. Unary, binary, and conditional operators use different numbers of operands.
  3. Operators may implicitly convert their operands.
  4. An lvalue identifies an object; an rvalue is commonly used for its value.
  5. Precedence determines grouping between operators of different precedence.
  6. Associativity determines grouping among operators of equal precedence.
  7. Parentheses override normal grouping.
  8. Precedence does not generally determine operand evaluation order.
  9. &&, ||, ?:, and the comma operator have important sequencing behavior.
  10. && and || use short-circuit evaluation.
  11. Integer division discards the fractional part.
  12. % computes the integer remainder.
  13. Relational and logical operators produce Boolean results.
  14. Do not write mathematical chained comparisons such as 0 < x < 10.
  15. = performs assignment; == tests equality.
  16. Assignment operators are right associative.
  17. Compound assignment combines an operation with assignment.
  18. Prefix ++ returns the updated value; postfix ++ uses the previous value.
  19. *iter++ accesses the current element and then advances the iterator.
  20. object.member accesses a member through an object.
  21. pointer->member accesses a member through a pointer.
  22. ?: is useful for simple if-else expressions.
  23. Bitwise operations work on individual bits of integral values.
  24. Use masks with |, &, ~, and ^ to manage flags.
  25. Prefer unsigned types for ordinary bit manipulation.
  26. sizeof returns the storage size of a type or object.
  27. The operand of sizeof is not evaluated.
  28. sizeof(array) gives the size of the complete built-in array.
  29. The comma operator evaluates left before right and returns the right result.
  30. Implicit conversions occur automatically in many expressions.
  31. Arithmetic operands are often converted to a common type.
  32. Small integral types undergo integral promotion.
  33. Arrays normally convert to pointers to their first element.
  34. Values and pointers can be converted to bool.
  35. static_cast is the normal named cast for explicit ordinary conversions.
  36. const_cast changes const qualification.
  37. reinterpret_cast is reserved for low-level reinterpretation.
  38. dynamic_cast is used for checked conversions in polymorphic hierarchies.
  39. Prefer named casts over C-style casts.
  40. Use parentheses when operator precedence is not immediately obvious.