Expressions in C++
Introduction
Expressions in C++ are fundamental constructs made up of operators, constants, and variables, following the language’s syntactical rules. Every expression is a segment of a code that returns a value. For instance:
This example demonstrates the creation of variables to store values: a box for $x$ and another for $y$, where $y$ equals the expression $x + 13$ (thus, $y = 23$). Now, let’s delve into a more complex example:
This statement encompasses three expressions:
It’s essential to remember the precedence of operations: multiplication and division are executed before addition and subtraction. For example:
1-3*4 = -11
2/3-4*2/3 = -2
2/3-4/4*2/3 = 0
Operator precedence in C++
determines the sequence of operations in an expression. Operators have a specific order of execution relative to others. For instance, in the expression $\frac{2}{4} - 3 + 4 \times 6$, the subexpressions $\frac{2}{4}$and
$4 \times 6$ are calculated first, followed by the addition and subtraction. When operators have the same precedence, their associativity dictates the order - either left-to-right or right-to-left.
Associativity specifies the order of operations for operators with the same precedence level. It can be left-to-right or right-to-left. Typically, addition, subtraction, multiplication, and division are left-associative, while assignment operators are right-associative. Some operators are non-associative, meaning their behaviour is undefined if used sequentially in an expression. Parentheses can alter the default associativity, enforcing a specific order.
Using Parentheses ()
The operator ()
has the highest precedente order (see Table 1), as consequence, we can use parentheses to change the sequence of operators.
Consider the following example:
5 + 6 * 7
The *
operator is evaluated firstly, followed by the +
operator, so the result is $5+6\times 7 = 47$. However, if we want to account for the addiction first and then the multiplication, we can rewrite the code as:
(5 + 6) * 7
Then, the program will compute $\left(5+6\right)\times 7=11\times 7=77$. Sometimes, parentheses’ inclusion should be important to make your code easier to understand, and therefore easier to maintain.
Modulus operator (%)
The modulus operator evaluates the remainder when dividing the first operand by the second one. Ex.: a % b
is the remainder when $a$ is divided
Short hand | Meaning | Prefix and Postfix |
---|---|---|
$x+=y$ | $x=x+y$ | |
$x-=y$ | $x=x-y$ | |
$x*=y$ | $x= x \times y$ | |
$x/=y$ | $x=x/y$ | |
$x++$ | $x=x+1$ | Return the value of $x$ first then increment it |
$++x$ | $x=x+1$ | Increment first then return the value of $x$ |
$x--$ | $x=x-1$ | Return the value of $x$ first then increment it |
$--x$ | $x=x-1$ | Increment first then return the value of $x$ |
Example 1:
Here you can see that y ++= x * z;
is calculate as $y=y+x \times z = 30 + 2 \times 4 = 38$.
Example 2:
In this example you can see that we used the postfix x++
to first initialize $y$ ($y=8 \times x = 8 \times 7 = 56$) and then update $x$ to x=x+1=8
. On the other hand, we used the prefix --y
to first update the variable $y$ to y=y-1=55
and then calculate the variable z using the updated $y$ $\left(z = y/5 = 55/5 = 11 \right)$.
Note that when we use x*= (y/z) % 2
the variable $x$ multiply the entire expression after =
symbol. This expression is equivalent to x = x * ((y/z) % 2));
.
Operator precedence and associativity
Table 1 shows a list of precedence (ordered) and associativity of C operators. This table was obtained from cppreference.com.
Precedence | Operator | Description | Associativity |
---|---|---|---|
1 | ++ \-\- | Suffix/postfix increment and decrement | Left-to-right |
() | Function call | ||
[] | Array subscripting | ||
. | Structure and union member access | ||
-> | Structure and union member access through pointer | ||
(type){list} | Compound literal(C99) | ||
2 | ++ \-\- | Prefix increment and decrement[note 1] | Right-to-left |
+ - | Unary plus and minus | ||
! ~ | Logical NOT and bitwise NOT | ||
(type) | Cast | ||
* | Indirection (dereference) | ||
& | Address-of | ||
sizeof | Size-of[note 2] | ||
_Alignof | Alignment requirement(C11) | ||
3 | * / % | Multiplication, division, and remainder | Left-to-right |
4 | + - | Addition and subtraction | |
5 | << >> | Bitwise left shift and right shift | |
6 | < <= | For relational operators < and ≤ respectively | |
> >= | For relational operators > and ≥ respectively | ||
7 | == != | For relational = and ≠ respectively | |
8 | & | Bitwise AND | |
9 | ^ | Bitwise XOR (exclusive or) | |
10 | | | Bitwise OR (inclusive or) | |
11 | && | Logical AND | |
12 | || | Logical OR | |
13 | ?: | Ternary conditional[note 3] | Right-to-Left |
14[note 4] | = | Simple assignment | |
+= -= | Assignment by sum and difference | ||
*= /= %= | Assignment by product, quotient, and remainder | ||
<<= >>= | Assignment by bitwise left shift and right shift | ||
&= ^= |= | Assignment by bitwise AND, XOR, and OR | ||
15 | , | Comma | Left-to-right |
- ↑ The operand of prefix
++
and\-\-
can't be a type cast. This rule grammatically forbids some expressions that would be semantically invalid anyway. Some compilers ignore this rule and detect the invalidity semantically. - ↑ The operand of
sizeof
can't be a type cast: the expressionsizeof (int) * p
is unambiguously interpreted as(sizeof(int)) * p
, but notsizeof((int)*p)
. - ↑ The expression in the middle of the conditional operator (between
?
and:
) is parsed as if parenthesized: its precedence relative to?:
is ignored. - ↑ Assignment operators' left operands must be unary (level-2 non-cast) expressions. This rule grammatically forbids some expressions that would be semantically invalid anyway. Many compilers ignore this rule and detect the invalidity semantically. For example, e = a < d ? a++ : a = d is an expression that cannot be parsed because of this rule. However, many compilers ignore this rule and parse it as e = ( ((a < d) ? (a++) : a) = d ), and then give an error because it is semantically invalid.
Impact of Data Types on Expressions
In C++
, the data type of the variables involved in an expression significantly impacts the result. For instance, dividing two integers results in an integer, while using at least one floating-point number yields a floating-point result. Understanding how data types interact within expressions is crucial for accurate calculations and avoiding common pitfalls like integer truncation.
Here are some key points about integer truncation and other common pitfalls in C++:
Integer Truncation: This occurs when the result of a division or other operation between integers is a floating-point number, but the data type is an integer. For example,
int result = 5 / 2;
will store2
inresult
, not2.5
, as the fractional part is truncated.Implicit Type Conversions:
C++
automatically converts types in certain situations, which can lead to unexpected results. For instance, mixing signed and unsigned integers in expressions can cause unexpected behaviours due to implicit type conversions.Overflow and Underflow: This happens when a variable is assigned a value outside its range. For example, storing a value larger than the maximum value that an
int
can hold will result in overflow, leading to unexpected values.Precision Loss in Floating-Point Numbers: Floating-point variables can lose precision, especially when dealing with very large or very small numbers. This can result in rounding errors in calculations.
Division by Zero: This can occur if a program inadvertently divides a number by zero. It’s a critical error in
C++
and can cause a program to crash or behave unpredictably.Uninitialized Variables: Using variables before initializing them can lead to unpredictable results, as they may contain random data.
Pointer Errors: Common mistakes with pointers include dereferencing a null or uninitialized pointer, pointer arithmetic errors, and memory leaks.
Operator Precedence Mistakes: Misunderstanding the order in which operations are performed can lead to bugs. For example, assuming that
a + b * c
addsa
andb
before multiplying byc
(it doesn’t; multiplication is done first).Assuming Size of Data Types is Constant: The size of data types like
int
can vary depending on the system. Assuming a constant size can lead to errors, particularly when performing operations like bit manipulation or working with binary file formats.Not Checking the Return Value of Functions: When functions return values to indicate success or failure, not checking these can lead to the program continuing as if nothing went wrong, even when errors have occurred.
Role of Type Casting in Expressions
Type casting in expressions can be used to explicitly convert data from one type to another. This technique is particularly useful in situations where operations between different data types are necessary. For example, casting an integer to a float in a division operation to obtain a floating-point result. However, it’s important to use type casting judiciously to maintain the precision and integrity of data.
The Significance of Expression Evaluation Order
While operator precedence and associativity rules dictate the order of operations in an expression, the sequence in which expressions are evaluated can also be influenced by function calls, side effects, and sequence points. Understanding how C++
evaluates expressions, especially in complex statements, is essential for debugging and writing predictable code.
Compiler Optimizations and Expressions
Modern C++
compilers often optimize expressions to enhance performance. These optimizations might include reordering operations (while respecting the language rules), eliminating redundant calculations, or simplifying expressions at compile time. Being aware of these potential optimizations can help in writing more efficient code and understanding any discrepancies between the written code and its execution behaviour.
Best Practices for Writing Expressions
To maintain readability and reduce errors in C++
, it’s advisable to write clear and simple expressions. Avoid overly complex expressions, use parentheses to clarify order of operations, and follow coding standards and guidelines. Readable expressions are easier to debug, maintain, and understand, especially in collaborative environments.
Adding these paragraphs can provide a more comprehensive and nuanced understanding of expressions in C++
, catering to both beginners and experienced programmers.
References
- C Operator Precedence - https://en.cppreference.com/w/c/language/operator_precedence#cite_note-1
Citation
- For attribution, please cite this work as:
- BibTeX citation
@misc{oliveira2020expression,
author = {Oliveira, Thiago},
title = {Expressions in C++},
url = {https://prof-thiagooliveira.netlify.app/post/expressions/},
year = {2020}
}
Did you find this page helpful? Consider sharing it 🙌