Lesson 04: Operations and Operators
Operations are fundamental actions performed on data in programming languages. Operators are symbols that represent these operations and provide a concise way to express computations. C/C++ offers a rich set of operators for various data types, enabling programmers to perform various calculations and manipulations.
4.1 Expressions, Operands and Operators
Expression, Operands, and Operators
Expressions are used to calculate a result involving a number of operands (either variables or constants). Operators allow us to manipulate the variables and constants in the expressions. Operands are the constants or variables upon which the operators operate.
In programming terms, an expression is something that expresses a value. It is either a simple value — or a single operand to express a single value.
120
"Welcome to EE2450"
score
or a set of operands that are combined together using operators:
5 + 12 + 15
value1 + value2
pow(x, 2) * r + 2
The operands of an expression may be variables, constants, other sub-expressions, or non-void function callings. Figure 1.1 illustrates the difference between operands, operators, and expressions.
Figure 1.1: Operands, operators, and expressions
Expressions
Expressions
A simple expression is:
top = 10;
Here, the variable top is assigned the constant value 10. In this example, 10 is the expression.
Another example is:
sum = 2 + 6 + 10;
The expression 2 + 6 + 10 consists of two operators and three operands. This expression is simple to work out as variables 2, 6, and 10 are added together and the result of 18 is stored in sum.
Operators
Operators
Operators are symbols that instruct the computer to perform a single, simple task, and most consist of just one or two characters (=, +, *, <=, or == — note that spaces between two-character operators are not allowed), but some operators consist of a complete word (new or return). Operands are expressions or values on which an operator acts or works.
Operands
Operands
An operand is simply something that an operator works on. In an expression like sum / 3.0, one operand is a variable (sum), and the other is a constant (3.0). Constants, variables, and function return values may all serve as operands and may be intermixed when forming expressions.
(a)
(b)
(c)
(d)
- Operands may be constants
- Operands may be variables
- Operands may be more complex expressions; in this example, 2, x, 4, and b are * operands, while 2 * x and 4 * y are + operands.
- The value returned by a non-void function may also serve as an operand.
Number of Operands
The number of operands of an operator is called its arity. Based on arity, operators are classified as nullary (no operands), unary (1 operand), binary (2 operands), ternary (3 operands), etc.
Unary Operators
Unary operators only require one operand. Most of the time, the operand is placed to the operator's right, but sometimes it is placed to the left.
+a | Positive |
-a | Negative |
!x | Logical NOT |
~x | Bit-wise not |
++a | Increment |
a++ | Increment |
--a | Decrement |
a-- | Decrement |
*a | Pointer to an address |
&a | The address of a variable |
Binary Operators
Binary operators require two operands. The arithmetic operators are the most familiar examples of binary operators. The assignment operator is also common. For example:
x = counter + 10;
counter and 10 are the + operator's operands, and x and the value of the expression counter + 10 are the = operator's operands. Note that the above example ends with a semicolon, which also makes it an example of a statement (specifically, an assignment statement).
Ternary Operator
The single C/C++ ternary operator is the conditional operator, which is formed with two symbols and requires three operands:
op1 ? op2 : op3;
The conditional operator is described in detail later in this chapter.
Practice Exercise 4.1
Convert the following operations to C expression:
- \({rate^2} + delta\)
- \(2(salary + bonus)\)
- \({1 \over {time + 3mass}}\)
- \({{a - 7} \over {t + 9v}}\)
Practice Exercise 4.1 - Answer
- (rate * rate) + delta
- 2 * (salary + bonus)
- 1 / (time + (3 * mass))
- (a - 7) / (t + (9 * v))
4.2 Operators
C/C++ has a rich set of operators that can be divided into the following classes:
- Binary Operators
Arithmetic, bitwise, shift operators - Non-binary Operators
Assignment, Logical, relational, conditional, and pointer operators
Assignment
Operators
Assignment Operators
The equal sign (=) in C/C++ is an operator called the assignment operator. It operates by setting the operand on its left to a value of the operand on its right. In other words, the equal sign (=) causes the value on its right to be assigned to (placed in) the variable on its left; that is, following the statement:
The variable a will have the value '8'.
Arithmetic
Operators
Arithmetic Operators
C/C++ has the following arithmetic operators:
Operator | Meaning | Example (A = 25, B = 7) | Description | |
---|---|---|---|---|
+ | Addition | A + B | 25 + 7 = 32 | Add A and B |
- | Subtraction | A - B | 25 -7 = 18 | Subtract B from A |
* | Multiplication | A * B | 25 * 7 = 175 | Multiply A and B |
/ | Division | A / B | 25 / 7 = 3 | Divide A by B |
% | Modulus (Remainder) | A % B | 25 % 7 = 4 | The remainder of dividing A by B |
+ | Unary Positive | +A | +25 | |
- | Unary Negative | -B | -7 |
Unary Positive
The role of a positive operator is to preserve the sign.
int i = 100;
int j = +i; // j = 100
Although such construction is syntactically correct, using it doesn’t make much sense, and it would be hard to find a good rationale for doing so.
Unary Negative
The negative operator changes the sign of its operand. A positive number becomes negative, and a negative number becomes positive.
int i = -100;
int j = -i; // j = 100
int k = -j; // k = -100
Remainder
The remainder operator (%) is quite peculiar because it has no equivalent amount of traditional arithmetic operators. You can use the % operator to find the remainder of a division, it is a binary operator (it performs the modulo operators), and both operands can not be floats.
int i = 12;
int j = 5;
int k = i % j;
The k variable is set to the value of 2.
Compute Quotient and Remainder
Write a program that asks a user to enter two integers (dividend and divisor), which are stored in variables dividend and divisor, respectively. Then the quotient is evaluated using / (the division operator) and stored in the quotient. Similarly, the remainder is evaluated using % (the modulo operator) and stored in the remainder.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
int main() {
int dividend, divisor, quotient, remainder;
printf("Enter dividend: ");
scanf("%d", ÷nd);
printf("Enter divisor: ");
scanf("%d", &divisor);
// Computes quotient
quotient = dividend / divisor;
// Computes remainder
remainder = dividend % divisor;
printf("Quotient = %d\n", quotient);
printf("Remainder = %d", remainder);
return 0;
}
Logical
Operators
Logical Operators
It's often convenient to test several expressions' true/false values simultaneously and combine the results into a single true/false value.
True/false values can be operated on with logical operators. There are three logical operators in C/C++ (see below Table). Two of them combine two true/false values, and the third negates a true/false value.
Operator | Meaning | Example | Description |
---|---|---|---|
|| | Logical OR | (x==3) || (x<1) | It returns true when at least one of the conditions is true |
&& | Logical AND | (x>0) && (x<10) | It returns true when both conditions are true |
! | Logical NOT | !(x > 10) | It reverses the state of the operand ("(x>10)") If (x>10) is true, the logical NOT operator makes it false. |
The logical operator will convert the operands into Boolean values. Any value that is not equal to zero would be considered logic 1 (even negative numbers), and values that equal 0 are considered logic 0.
Example of all the logical operators
Try the following example:
#include <stdio.h>
main() {
int a = 5;
int b = 20;
int c ;
if ( a && b ) {
printf("Line 1 - Condition is true.\n" );
}
if ( a || b ) {
printf("Line 2 - Condition is true.\n" );
}
/* lets change the value of a and b */
a = 0;
b = 10;
if ( a && b ) {
printf("Line 3 - Condition is true.\n" );
} else {
printf("Line 3 - Condition is not true.\n" );
}
if ( !(a && b) ) {
printf("Line 4 - Condition is true.\n" );
}
}
When you compile and execute the above program, it produces the following results −
Line 1 - Condition is true.
Line 2 - Condition is true.
Line 3 - Condition is not true.
Line 4 - Condition is true.
Bitwise
Operators
Bitwise Operators
C/C++ provides operators that act upon the actual bits that comprise a value. The bitwise operators can be used only on integral types. The bitwise operators are as follows:
Operator | Meaning | Example (A = 12, B = 38) | Description |
---|---|---|---|
~ | Bitwise not | ~A = -13 | Binary One's Complement Operator is unary and has the effect of 'flipping' bits. |
& | Bitwise and | (A & B) = 4 | Binary AND Operator copies a bit to the result if it exists in both operands. |
| | Bitwise or | (A | B) = 46 | Binary OR Operator copies a bit if it exists in either operand. |
^ | Bitwise xor | (A ^ B) = 42 | Binary XOR Operator copies the bit if it is set in one operand but not both. |
The truth tables for bitwise operators are as follows:
x | y | ~x | x & y | x | y | x ^ y |
---|---|---|---|---|---|
0 | 0 | 1 | 0 | 0 | 0 |
0 | 1 | 1 | 0 | 1 | 1 |
1 | 0 | 0 | 0 | 1 | 1 |
1 | 1 | 0 | 1 | 1 | 0 |
These rules are applied to each bit in each operand when the bitwise and/or, and xor operations are performed.
A simple bitwise and operation is shown below:
00001100 = 12
& 00100110 = 38
------------------
00000100 = 4
A bitwise or operation looks like this:
00001100 = 12
| 00100110 = 38
------------------
00101110 = 46
A bitwise xor operation is shown here:
00001100 = 12
^ 00100110 = 38
------------------
00101010 = 42
The One's Complement Operator
The one's complement operator, ~, will invert all the bits in its operand. For example, if a is an 8-bit integer variable, and its value is a = 12. The binary value of a is 0b00001100.
The one's complement of an operation is shown here:
~ 00001100 = 12
------------------
11110011 = -13
Bit 7 is the sign bit, so 0b11110011 is -13 in decimal.
Masking and Setting Bits
Bit-wise or can be used to set a bit, and and can be used to clear a bit. It can be observed that:
- Anything or with a 1, results in a 1; anything or with a 0, results in no change.
- Anything and with a 1, results in no change; anything and with a 0, results in a zero.
- Anything xor with a 1, results in the complement; anything xor with a 0, results in no change.
Example of all the bitwise operators
#include <stdio.h>
int main() {
unsigned int a = 60; // 60 = 0011 1100
unsigned int b = 13; // 13 = 0000 1101
int c = 0;
c = a & b; // 12 = 0000 1100
printf("Line 1 - Value of c is %d\n", c );
c = a | b; // 61 = 0011 1101
printf("Line 2 - Value of c is %d\n", c );
c = a ^ b; // 49 = 0011 0001
printf("Line 3 - Value of c is %d\n", c );
c = ~a; //-61 = 1100 0011
printf("Line 4 - Value of c is %d\n", c );
return 0;
}
#include <stdio.h>
#include <iostream>
using namespace std;
int main() {
unsigned int a = 60; // 60 = 0011 1100
unsigned int b = 13; // 13 = 0000 1101
int c = 0;
c = a & b; // 12 = 0000 1100
cout << "Line 1 - Value of c is " << c << endl;
c = a | b; // 61 = 0011 1101
cout << "Line 2 - Value of c is " << c << endl;
c = a ^ b; // 49 = 0011 0001
cout << "Line 3 - Value of c is " << c << endl;
c = ~a; //-61 = 1100 0011
cout << "Line 4 - Value of c is " << c << endl;
return 0;
}
When you compile and execute the above program, it produces the following results −
Line 1 - Value of c is 12
Line 2 - Value of c is 61
Line 3 - Value of c is 49
Line 4 - Value of c is -61
Relational
Operators
Relational Operators
The relational operators compare two expressions, producing a Boolean (true/false) result. They are most often used in the flow of control statements such as if-else statements, for-loops, while-loops, and do-while loops.
Operator | Meaning | Example A = 5; B = 2; | Description |
---|---|---|---|
> | Greater than | A > B | 5 > 2, the result is True (1) |
>= | Greater than or equal | A >= B | 5 >= 2, the result is True (1) |
< | Less than | A < B | 5 < 2, the result is False (0) |
<= | Less than or equal | A <= B | 5 <= 2, the result is False (0) |
== | Equal | A == B | 5 == 2, the result is False (0) |
!= | Not equal | A != B | 5 != 2, the result is True (1) |
Chaining of Comparison Operators in C
In Python, an expression like "c > b > a" is treated as "c > b and b > a", but this type of chaining does not happen in C. For example, consider the following program. The output of the following program is "FALSE".
#include <stdio.h>
int main()
{
int a = 10, b = 20, c = 30;
if (c > b > a)
printf("TRUE");
else
printf("FALSE");
return 0;
}
In line 7, the (c > b > a) is treated as ((c > b) > a), associativity of '>' is left to right. So, the expression becomes ((30 > 20) > 10), while the (30 > 20) is ture (1). The final expression becomes (1 > 20), and the result is false (0).
Inc/Decrement
Operators
Increment and Decrement Operators
Increment operators are used to increase the variable's value by one, and decrement operators are used to decrease the variable's value by one in the C/C++ program.
Operator | Meaning | Example a = 3; b = 7; | Description |
---|---|---|---|
++ var var ++ |
Increment operator | ++a; b++; |
++a ⇒ a += 1 ⇒ a + 1 = 4 b++ ⇒ b += 1 ⇒ b + 1 = 8 |
-- var var -- |
Decrement operator | --a; b--; |
--a ⇒ a -= 1 ⇒ a - 1 = 2 b-- ⇒ b -= 1 ⇒ b - 1 = 6 |
Pre/Post Increment and Decrement Operators
When you use the increment and decrement operators in the expressions, they have different meanings. The below table will explain the difference between pre/post-increment and decrement operators in the C/C++ programming language.
- Pre-Increment Operator
A pre-increment operator is used to increment the value of a variable before using it in an expression. In the Pre-Increment, the value is first incremented/decremented and then used inside the expression. - Pre-Decrement Operator
A pre-decrement operator is used to decrement the value of a variable before using it in an expression. In the Pre-Decrement, the value is first decremented and then used inside the expression. - Post-Increment Operator
A post-increment operator is used to increment the variable's value after executing the expression completely in which post-increment is used. In the Post-Decrement, the value is first used in an expression and then incremented. - Post-Decrement operator
A post-decrement operator is used to decrement the variable's value after executing the expression completely in which post-decrement is used. In the Post-Decrement, the value is first used in an expression and then decremented.
Syntax (i = 5, j = 2) | Function | Description |
---|---|---|
a = ++i; | Pre-increment operator (++i) |
|
a = --i; | Pre-decrement operator (--i) |
|
a = i++; | Post-increment operator (i++) |
|
a = i--; | Post-decrement operator (i--) |
|
a = (++i) + (j--); | Pre-increment (++i) and Post-decrement (j--) operators |
|
Simple post and pre-increment operator
// C program to demonstrate working of unary increment
// and decrement operators
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
int main()
{
// Post increment
int a = 1;
printf("a value: %d \n", a);
int b = a++;
printf("b value after b = a++ : %d \n", b);
printf("a value after b = a++ : %d \n\n", a);
// Pre increment
a = 1;
printf("a value: %d \n", a);
b = ++a;
printf("b value after b = ++a : %d \n", b);
printf("a value after b = ++a : %d \n\n", a);
// Post decrement
a = 5;
printf("a value before decrement: %d \n", a);
b = a--;
printf("b value after b = a-- : %d \n", b);
printf("a value after b = a-- : %d \n\n", a);
// Pre decrement
a = 5;
printf("a value: %d \n", a);
b = --a;
printf("b value after b = --a : %d \n", b);
printf("a value after b = --a : %d \n", a);
return 0;
}
Result:
a value: 1
b value after b = a++ : 1
a value after b = a++ : 2
a value: 1
b value after b = ++a : 2
a value after b = ++a : 2
a value before decrement: 5
b value after b = a-- : 5
a value after b = a-- : 4
a value: 5
b value after b = --a : 4
a value after b = --a : 4
Evaluating post and pre-increment together
#include <iostream>
using namespace std;
int main()
{
int A = 10, B = 20, C = 0;
C = A++ + ++B * 10 + B++; // 1. ++B => B= B + 1 = 21
// 2. C = A + B * 10 + B = 10 + 21 * 10 + 21 = 241
// 3. A++ => A = A + 1 = 11
// B++ => B = B + 1 = 22
cout << "A = " << A << ","
<< "B = " << B << ","
<< "C = " << C << endl;
return 0;
}
Output:
A = 11,B = 22,C = 241
Post and Pre-increment Operators on Same Variable
#include <stdio.h>
#include <iostream>
using namespace std;
int main()
{
int x;
int y = 10;
x = (++y)++;
cout << "x = " << x << endl;
cout << "y = " << y << endl;
return 0;
}
The output:
x = 11
y = 12
Shift
Operators
Shift Operators
There are two bit-wise shift operators in C:
- Shift Left (<< data <<) number of bit-positions to be shifted left.
- Shift Right (>> data >>) number of bit-positions to be shifted right.
Operator | Meaning | Example (A = 0b0011 1100 = 60) | Description |
---|---|---|---|
X << n | Binary Left Shift Operator. The shift left operand value is moved left by the number of bits specified by the right operand. | A << 2 => 0b1111 0000 = 240 | Shift the value 60 to the left 2 bits |
X >> n | Binary Right Shift Operator. The shift left operand value is moved right by the number of bits specified by the right operand. | A >> 2 => 0b0000 1111 = 15 | Shift the value 60 to the right 2 bits |
X must be an integer (signed or unsigned)
n must be an unsigned integer and the value is from 0 to less than the number of bits of X value.
Logical Shift (for unsigned integers)
In a logical shift, zeros are shifted in to replace the discarded bits. Therefore, the logical and arithmetic left-shifts are exactly the same.
However, as the logical right-shift inserts value 0 bits into the most significant bit, instead of copying the sign bit, it is ideal for unsigned binary numbers, while the arithmetic right-shift is ideal for signed two's complement binary numbers.
Left Logical Shift
The left-shift operator causes the bits in X to be shifted to the left by the number of positions specified by n. The bit positions that have been vacated by the shift operation are zero-filled. A left shift is a logical shift (the bits that are shifted off the end are discarded, including the sign bit).
Right Logical Shift
The right-shift operator causes the bit pattern in X to be shifted to the right by the number of positions specified by n. For unsigned numbers, the bit positions that have been vacated by the shift operation are zero-filled.
Arithmetic shift (for signed integers)
In an arithmetic shift, the bits that are shifted out of either end are discarded. In a left arithmetic shift, zeros are shifted in on the right; in a right arithmetic shift, the sign bit (the MSB in two's complement) is shifted in on the left, thus preserving the sign of the operand.
Left Arithmetic Shift
A left arithmetic shift of a number X by n is equivalent to multiplying X by 2n and is thus equivalent to a logical left shift; a logical shift would also give the same result since MSB anyway falls off the end and there's nothing to preserve. The left-shift operations can be used on unsigned and signed numbers.
Right Arithmetic Shift
A right arithmetic shift of a number X by n is equivalent to an integer division of X by 2n ONLY if X is non-negative! Integer division is nothing but mathematical division and rounds towards 0. For signed numbers, the sign bit is used to fill the vacated bit positions. In other words, if the number is positive, 0 is used, and if the number is negative, 1 is used.
Logical and Arithmetic Shifts
When shifting left (<<), there is no difference between arithmetic and logical shift.
When shifting an unsigned value, the >> operator in C is a logical shift. When shifting a signed value, the >> operator is an arithmetic shift.
#include <stdio.h>
#include <stdint.h>
void bin8ToStr(uint8_t num, char* bitStr);
int main()
{
int8_t i8 = 0b11101010;
uint8_t u8 = 0b11101010;
int8_t iResult;
uint8_t uResult;
char bitStr[17] = {0};
bin8ToStr(i8, bitStr);
printf("Original signed Value: %s (0x%02X, %03d) \n", bitStr, i8 & 0xFF, i8);
iResult = i8 >> 2;
bin8ToStr(iResult, bitStr);
printf("Shifted 2 right: %s (0x%02X, %03d) \n", bitStr, iResult & 0xFF, iResult);
iResult = i8 << 2;
bin8ToStr(iResult, bitStr);
printf("Shifted 2 left: %s (0x%02X, %03d) \n\n", bitStr, iResult & 0xFF, iResult);
i8 = 22;
bin8ToStr(i8, bitStr);
printf("Original signed Value: %s (0x%02X, %03d) \n", bitStr, i8, i8);
iResult = i8 >> 2;
bin8ToStr(iResult, bitStr);
printf("Shifted 2 right: %s (0x%02X, %03d) \n", bitStr, iResult & 0xFF, iResult);
iResult = i8 << 2;
bin8ToStr(iResult, bitStr);
printf("Shifted 2 left: %s (0x%02X, %03d) \n\n", bitStr, iResult & 0xFF, iResult);
bin8ToStr(u8, bitStr);
printf("Original unsigned Value : %s (0x%02X, %03d) \n", bitStr, u8, u8);
uResult = u8 >> 2;
bin8ToStr(uResult, bitStr);
printf("Shifted 2 right: %s (0x%02X, %03d) \n", bitStr, uResult & 0xFF, uResult);
uResult = u8 << 2;
bin8ToStr(iResult, bitStr);
printf("Shifted 2 left: %s (0x%02X, %03d) \n\n", bitStr, uResult & 0xFF, uResult);
return 0;
}
void bin8ToStr(uint8_t num, char* bitStr)
{
int i;
for (i = 0; i < 8; i++)
bitStr[i] = (char)((num >> (7-i)) & 0x01) + '0';
}
Output
Original signed Value: 11101010 (0xEA, -22)
Shifted 2 right: 11111010 (0xFA, -06)
Shifted 2 left: 10101000 (0xA8, -88)
Original signed Value: 00010110 (0x16, 022)
Shifted 2 right: 00000101 (0x05, 005)
Shifted 2 left: 01011000 (0x58, 088)
Original unsigned Value : 11101010 (0xEA, 234)
Shifted 2 right: 00111010 (0x3A, 058)
Shifted 2 left: 01011000 (0xA8, 168)
Rotate (Circular Shifts)
In this operation, sometimes called rotate no carry, the bits are "rotated" as if the left and right ends of the register were joined. The value that is shifted into the right during a left shift is whatever value was shifted out on the left, and vice versa for a right shift operation. This is useful if it is necessary to retain all the existing bits, and is frequently used in digital cryptography.
Left Circular Shift or Rotate
Right Circular Shift or Rotate
Circular Shifts (Rotate) in C
C++ 20 already provides std::rotl and std::rotr rotate functions, but C does not have operators or standard functions for rotating, even though most processors have bitwise operation instructions for rotating (e.g., Intel x86 has ROL and ROR instructions).
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h> // for uint8_t, to get 8-bit-wide rotates
#include <stdbool.h>
void bin8ToStr(uint8_t num, char* bitStr);
uint8_t rotl8(uint8_t num, uint8_t count);
uint8_t rotr8(uint8_t num, uint8_t count);
int main()
{
uint8_t u8 = 0b10010110;
uint8_t uResult;
char bitStr[9] = {0};
bin8ToStr(u8, bitStr);
printf("Original Value: %s (0x%02X, %03d) \n", bitStr, u8 & 0xFF, u8);
uResult = rotl8(u8, 1);
bin8ToStr(uResult, bitStr);
printf("Left circular Shift by 1 : %s (0x%02X, %03d) \n", bitStr, uResult & 0xFF, uResult);
uResult = rotl8(u8, 2);
bin8ToStr(uResult, bitStr);
printf("Left circular Shift by 2 : %s (0x%02X, %03d) \n", bitStr, uResult & 0xFF, uResult);
uResult = rotl8(u8, 3);
bin8ToStr(uResult, bitStr);
printf("Left circular Shift by 3 : %s (0x%02X, %03d) \n", bitStr, uResult & 0xFF, uResult);
uResult = rotl8(u8, 4);
bin8ToStr(uResult, bitStr);
printf("Left circular Shift by 4 : %s (0x%02X, %03d) \n", bitStr, uResult & 0xFF, uResult);
uResult = rotl8(u8, 5);
bin8ToStr(uResult, bitStr);
printf("Left circular Shift by 5 : %s (0x%02X, %03d) \n", bitStr, uResult & 0xFF, uResult);
uResult = rotl8(u8, 6);
bin8ToStr(uResult, bitStr);
printf("Left circular Shift by 6 : %s (0x%02X, %03d) \n", bitStr, uResult & 0xFF, uResult);
uResult = rotl8(u8, 7);
bin8ToStr(uResult, bitStr);
printf("Left circular Shift by 7 : %s (0x%02X, %03d) \n", bitStr, uResult & 0xFF, uResult);
uResult = rotl8(u8, 8);
bin8ToStr(uResult, bitStr);
printf("Left circular Shift by 8 : %s (0x%02X, %03d) \n\n", bitStr, uResult & 0xFF, uResult);
uResult = rotr8(u8, 1);
bin8ToStr(uResult, bitStr);
printf("Right circular Shift by 1 : %s (0x%02X, %03d) \n", bitStr, uResult & 0xFF, uResult);
uResult = rotr8(u8, 2);
bin8ToStr(uResult, bitStr);
printf("Right circular Shift by 2 : %s (0x%02X, %03d) \n", bitStr, uResult & 0xFF, uResult);
uResult = rotr8(u8, 3);
bin8ToStr(uResult, bitStr);
printf("Right circular Shift by 3 : %s (0x%02X, %03d) \n", bitStr, uResult & 0xFF, uResult);
uResult = rotr8(u8, 4);
bin8ToStr(uResult, bitStr);
printf("Right circular Shift by 4 : %s (0x%02X, %03d) \n", bitStr, uResult & 0xFF, uResult);
uResult = rotr8(u8, 5);
bin8ToStr(uResult, bitStr);
printf("Right circular Shift by 5 : %s (0x%02X, %03d) \n", bitStr, uResult & 0xFF, uResult);
uResult = rotr8(u8, 6);
bin8ToStr(uResult, bitStr);
printf("Right circular Shift by 6 : %s (0x%02X, %03d) \n", bitStr, uResult & 0xFF, uResult);
uResult = rotr8(u8, 7);
bin8ToStr(uResult, bitStr);
printf("Right circular Shift by 7 : %s (0x%02X, %03d) \n", bitStr, uResult & 0xFF, uResult);
uResult = rotr8(u8, 8);
bin8ToStr(uResult, bitStr);
printf("Right circular Shift by 8 : %s (0x%02X, %03d) \n", bitStr, uResult & 0xFF, uResult);
return 0;
}
//------------------------------------------------------------------------------
void bin8ToStr(uint8_t num, char* bitStr)
{
int i;
for (i = 0; i < 8; i++)
bitStr[i] = (char)((num >> (7-i)) & 0x01) + '0';
}
//------------------------------------------------------------------------------
uint8_t rotl8(uint8_t num, uint8_t count)
{
uint8_t result;
result = (count % 8) ? (num << count) | (num >> (8 - count)) : num;
return result;
}
//------------------------------------------------------------------------------
uint8_t rotr8(uint8_t num, uint8_t count)
{
uint8_t result;
result = (count % 8) ? (num >> count) | (num << (8 - count)) : num;
return result;
}
//------------------------------------------------------------------------------
Output
Original Value: 10010110 (0x96, 150)
Left circular Shift by 1 : 00101101 (0x2D, 045)
Left circular Shift by 2 : 01011010 (0x5A, 090)
Left circular Shift by 3 : 10110100 (0xB4, 180)
Left circular Shift by 4 : 01101001 (0x69, 105)
Left circular Shift by 5 : 11010010 (0xD2, 210)
Left circular Shift by 6 : 10100101 (0xA5, 165)
Left circular Shift by 7 : 01001011 (0x4B, 075)
Left circular Shift by 8 : 10010110 (0x96, 150)
Right circular Shift by 1 : 01001011 (0x4B, 075)
Right circular Shift by 2 : 10100101 (0xA5, 165)
Right circular Shift by 3 : 11010010 (0xD2, 210)
Right circular Shift by 4 : 01101001 (0x69, 105)
Right circular Shift by 5 : 10110100 (0xB4, 180)
Right circular Shift by 6 : 01011010 (0x5A, 090)
Right circular Shift by 7 : 00101101 (0x2D, 045)
Right circular Shift by 8 : 10010110 (0x96, 150)
Rotate through Carry
Rotate through carry is a variant of the rotate operation, where the bit that is shifted in (on either end) is the old value of the carry flag, and the bit that is shifted out (on the other end) becomes the new value of the carry flag.
A single rotate-through carry can simulate a logical or arithmetic shift of one position by setting up the carry flag beforehand. For example, if the carry flag contains 0, then x RIGHT-ROTATE-THROUGH-CARRY-BY-ONE is a logical right-shift, and if the carry flag contains a copy of the sign bit, then x RIGHT-ROTATE-THROUGH-CARRY-BY-ONE is an arithmetic right-shift. For this reason, some microcontrollers such as low-end PICs just have to rotate and rotate through carry and don't bother with arithmetic or logical shift instructions.
Rotate-through carry is especially useful when performing shifts on numbers larger than the processor's native word size because if a large number is stored in two registers, the bit that is shifted off one end of the first register must come in at the other end of the second. With rotate-through-carry, that bit is "saved" in the carry flag during the first shift, ready to shift in during the second shift without any extra preparation.
Left Rotate Through Carry
Right Rotate Through Carry
Conditional
Operators
Conditional Operator
The conditional operator is the only C/C++ operator that operates on three operands. It consists of two symbols: a question mark (?) and a colon (:).
Operator | Meaning | Example | Description |
---|---|---|---|
( )? : | Conditional Operator | nabs = (n > 0)? n : -n; | Find the absolute value of variable n |
The syntax of the conditional operator is:
(condition) ? (expr_true) : (expr_false) ;
The conditional operator was invented because a particular construction occurs often in C/C++ programs and it is nice to shorten it.
Pointer
Operators
Pointer Operators
There are two types of pointer operators: * and &. A pointer is an object that contains the address of another object. Or, put differently, an object that contains the address of another object is said to "point to" the other object.
The & Pointer Operator
The & operator returns the address of the object it precedes. For example, if the integer x is located at memory address $1000, then
p = &x;
places the value 0x1000 into p. The & can be thought of as "the address of". For example, the above statement could be read as "place the address of x into p.
The * Pointer Operator
The * is the indirection operator. It uses the current value of the variable it precedes as the address at which data will be stored or obtained. For example, the following fragment
p = & x; // put address of x into p
*p = 100; // use address contained in p
places the value 100 into x. The * can be remembered as "at address". In this example, it could be read as "place the value 100 at address p". Since p contains the address of x, the value 100 is actually stored in x. In other words, p is said to "point to" x. The * operator can also be used on the right-hand side of an assignment. For example:
p = &x;
*p = 100;
z = *p / 25;
places the value of 4 into z.
The detail information about pointers will be discussed in EE2440-Lesson 03: Minterms and Maxterms
Comma
Operators
The comma in C/C++
In C/C++, comma (,) can be used in two contexts:
Comma as an operator
Comma as an operator
The comma operator (,) is a binary operator that evaluates its first operand and discards the result, it then evaluates the second operand and returns this value (and type). The use of the comma token as an operator is distinct from its use in function calls and definitions, variable declarations, enum declarations, and similar constructs, where it acts as a separator.
The comma operator has the lowest precedence of any C operator and acts as a sequence point.
// comma as an operator
int i = (5, 10); // 10 is assigned to i
int j = (f1(), f2()); // f1() is called (evaluated) first followed by f2().
// The returned value of f2() is assigned to j
The primary use of the comma operator is to produce side effects in the following situations:
- Calling a function
- Entering or repeating an iteration loop
- Testing a condition
- Other situations where a side effect is required but the result of the expression is not immediately needed
The following table gives some examples of the uses of the comma operator.
Statement | Description |
---|---|
f (a, (t = 3, t + 2), c); | The function f() has only three arguments: the value of a, the value 5, and the value of c. |
for ( i=0; i < 2; ++i, f() ); | A for statement in which i is incremented and f() is called at each iteration. |
if ( f(), ++i, i > 1 ) { /* ... */ } |
An if statement in which function f() is called, variable i is incremented, and variable i is tested against a value. The first two expressions within this comma expression are evaluated before the expression i > 1. Regardless of the results of the first two expressions, the third is evaluated, and its result determines whether the if statement is processed. |
func( ( ++a, f(a) ) ); | A function call to func() in which a is incremented, the resulting value is passed to a function f(), and the return value of f() is passed to func(). The function func() is passed only a single argument because the comma expression is enclosed in parentheses within the function argument list. |
Comma Operator in for-Loops
The most common use is to allow multiple assignment statements without using a block statement, primarily in the initialization and the increment expressions of a for-loop. This is the only idiomatic use in elementary C programming. In the following example, the order of the loop's initializers is significant:
void rev(char *s, size_t len)
{
char *first;
for (first = s, s += len; s >= first; --s) {
putchar(*s);
}
}
Comma Operator in Condition
The comma can be used within a condition (of an if, while, do-while, or for) to allow auxiliary computations, particularly calling a function and using the result, with block scoping:
if (y = f(x), y > x) {
... // statements involving x and y
}
Use Comma Operator to avoid a Block
For brevity, the comma can be used to avoid a block and associated braces, as in:
if (x == 1) y = 2, z = 3;
if (x == 1)
y = 2, z = 3;
instead of
if (x == 1) {y = 2; z = 3;}
if (x == 1) {
y = 2; z = 3;
}
Comma Operator in Place of a Semicolon
Comma operator in place of a semicolon.
We know that in C and C++, every statement is terminated with a semicolon, but the comma operator is also used to terminate the statement after satisfying the following rules.
- The variable declaration statements must be terminated with a semicolon.
- The comma operator can terminate the statements after the declaration statement.
- A semicolon must terminate the last statement of the program.
Example:
#include <iostream>
using namespace std;
int main()
{
cout << "First Line\n",
cout << "Second Line\n",
cout << "Third Line\n",
cout << "Last line";
return 0;
}
Output
First Line
Second Line
Third Line
Last line
Comma as a Separator
Commas as a Separator
Comma acts as a separator when used with function calls and definitions, function-like macros, variable declarations, enum declarations, and similar constructs.
// comma as a separator
int a = 1, b = 2;
void fun(x, y);
The use of the comma as a separator should not be confused with the use of an operator. For example, in the below statement, f1() and f2() can be called in any order.
// Comma acts as a separator here and doesn't enforce any sequence.
// Therefore, either f1() or f2() can be called first
void fun(f1(), f2());
See this for C vs C++ differences of using the comma operator. You can try the below programs to check your understanding of commas in C.
// PROGRAM 1
#include <stdio.h>
int main()
{
int x = 10;
int y = 15;
printf("%d", (x, y));
getchar();
return 0;
}
// PROGRAM 2: Thanks to Shekhu for suggesting this program
#include <stdio.h>
int main()
{
int x = 10;
int y = (x++, ++x);
printf("%d", y);
getchar();
return 0;
}
// PROGRAM 3: Thanks to Venki for suggesting this program
#include <stdio.h>
int main()
{
int x = 10, y;
// The following is equivalent
// to y = x + 2 and x += 3,
// with two printings
y = (x++,
printf("x = %d\n", x),
++x,
printf("x = %d\n", x),
x++);
// Note that last expression is evaluated
// but side effect is not updated to y
printf("y = %d\n", y);
printf("x = %d\n", x);
return 0;
}
More Examples for Comma
In this example, the different behavior between the second and third lines is due to the comma operator having lower precedence than the assignment. The last example also differs since the return expression must be fully evaluated before the function can return.
/**
* Commas act as separators in this line, not as an operator.
* Results: a=1, b=2, c=3, i=0
*/
int a=1, b=2, c=3, i=0;
/**
* Assigns value of b into i.
* Results: a=1, b=2, c=3, i=2
*/
int a=1, b=2, c=3;
int i = (a, b);
/**
* Assigns value of a into i. Equivalent to (i = a), b;
* Results: a=1, b=2, c=3, i=1
* (The braces on the second line are needed to
* avoid a compiler error. The second 'b' declared
* is given no initial value.)
*/
int a=1, b=2, c=3;
{ int i = a, b; }
/**
* Increases value of a by 2, then assigns value of resulting operation a+b into i .
* Results: a=3, b=2, c=3, i=5
*/
int a=1, b=2, c=3;
int i = (a += 2, a + b);
/**
* Increases value of a by 2, then stores value of a to i, and discards unused
* values of resulting operation a + b . Equivalent to (i = (a += 2)), a + b;
* Results: a=3, b=2, c=3, i=3
*/
int a=1, b=2, c=3;
int i;
i = a += 2, a + b;
/**
* Assigns value of a into i; the following 'b' and 'c'
* are not part of the initializer but declarators for
* second instances of those variables.
* Results: a=1, b=2, c=3, i=1
* (The braces on the second line are needed to
* avoid a compiler error. The second 'b' and second
* 'c' declared are given no initial value.)
*/
int a=1, b=2, c=3;
{ int i = a, b, c; }
/**
* Assigns value of c into i, discarding the unused a and b values.
* Results: a=1, b=2, c=3, i=3
*/
int a=1, b=2, c=3;
int i = (a, b, c);
/**
* Returns 6, not 4, since comma operator sequence points following the keyword
* 'return' are considered a single expression evaluating to rvalue of final
* subexpression c=6 .
*/
return a=4, b=5, c=6;
/**
* Returns 3, not 1, for same reason as previous example.
*/
return 1, 2, 3;
/**
* Returns 3, not 1, still for same reason as above. This example works as it does
* because return is a keyword, not a function call. Even though compilers will
* allow for the construct return(value), the parentheses are only relative to "value"
* and have no special effect on the return keyword.
* Return simply gets an expression and here the expression is "(1), 2, 3".
*/
return (1), 2, 3;
Miscellaneous
Operators
Miscellaneous Operators
Member Operators
The dot (.) operator and the arrow (->) operator can be used to reference individual members of classes, structures, and unions. The dot operator is applied to the actual object. The arrow operator is used with a pointer to an object.
Operator | Description | Example | Description |
---|---|---|---|
. | member access | a.b | access member b of struct, union, or class a. |
-> | member access through a pointer | a->b | access member b of struct, union, or class pointed to by a. |
Scope Resolution
Operator (C++)
Scope Resolution Operator
The scope resolution operator :: is used to identify and disambiguate identifiers used in different scopes.
Syntax | Description |
---|---|
::identifier | |
className::identifier | |
namespace::identifier | |
enum class::identifier | |
enum struct::identifier |
The identifier can be a variable, a function, or an enumeration value.
With Classes and Namespace
The following example shows how the scope resolution operator is used with namespaces and classes:
namespace NamespaceA{
int x;
class ClassA {
public:
int x;
};
}
int main() {
// A namespace name used to disambiguate
NamespaceA::x = 1;
// A class name used to disambiguate
NamespaceA::ClassA a1;
a1.x = 2;
}
A scope resolution operator without a scope qualifier refers to the global namespace.
namespace NamespaceA{
int x;
}
int x;
int main() {
int x;
// the x in main()
x = 0;
// The x in the global namespace
::x = 1;
// The x in the A namespace
NamespaceA::x = 2;
}
You can use the scope resolution operator to identify a member of a namespace, or to identify a namespace that nominates the member's namespace in a using-directive. In the example below, you can use NamespaceC to qualify ClassB, even though ClassB was declared in namespace NamespaceB, because NamespaceB was nominated in NamespaceC by a using directive.
namespace NamespaceB {
class ClassB {
public:
int x;
};
}
namespace NamespaceC{
using namespace B;
}
int main() {
NamespaceB::ClassB c_b;
NamespaceC::ClassB c_c;
c_b.x = 3;
c_c.x = 4;
}
You can use chains of scope resolution operators. In the following example, NamespaceD::NamespaceD1 identifies the nested namespace NamespaceD1, and NamespaceE::ClassE::ClassE1 identifies the nested class ClassE1.
namespace NamespaceD{
namespace NamespaceD1{
int x;
}
}
namespace NamespaceE{
class ClassE{
public:
class ClassE1{
public:
int x;
};
};
}
int main() {
NamespaceD:: NamespaceD1::x = 6;
NamespaceE::ClassE::ClassE1 e1;
e1.x = 7 ;
}
With Static Members
You must use the scope resolution operator to call static members of classes.
class ClassG {
public:
static int get_x() { return x;}
static int x;
};
int ClassG::x = 6;
int main() {
int gx1 = ClassG::x;
int gx2 = ClassG::get_x();
}
With Scoped Enumerations
The scoped resolution operator is also used with the values of a scoped enumeration Enumeration Declarations, as in the following example:
enum class EnumA{
First,
Second,
Third
};
int main() {
EnumA enum_value = EnumA::First;
}
Pointer-to-Member Operators (C++)
Pointer-to-Member Operators
C++ allows you to generate a special type of pointer that "points" generically to a member of a class, not to a specific instance of that member in an object. This sort of pointer is called a pointer to a member, or pointer-to-member.
I/O Operators (C++)
I/O Operators
output-stream << expressions
input_stream >> variable
4.3 Compound Assignment Operators
The compound assignment operators consist of a binary operator and a simple assignment operator. They perform the operation of the binary operator on both operands and store the result of that operation in the left operand, which must be a modifiable lvalue.
The following table lists the compound assignment operators and shows an expression using each operator:
Operator | Meaning | Example | Description |
---|---|---|---|
+= | Add AND assignment operator. It adds the right operand to the left operand and assigns the result to the left operand. | a += exp; | a = a + (exp); |
-= | Subtract AND assignment operator. It subtracts the right operand from the left operand and assigns the result to the left operand. | a -= exp; | a = a - (exp); |
*= | Multiply AND assignment operator. It multiplies the right operand with the left operand and assigns the result to the left operand. | a *= exp; | a = a * (exp); |
/= | Divide AND assignment operator. It divides the left operand with the right operand and assigns the result to the left operand. | a /= exp; | a = a / (exp); |
%= | Modulus AND assignment operator. It takes modulus using two operands and assigns the result to the left operand. | a %= exp; | a = a % (exp); |
<<= | Left shift AND assignment operator. | a <<= exp; | a = a << (exp); |
>>= | Right shift AND assignment operator. | a >>= exp; | a = a >> (exp); |
&= | Bitwise and assignment operator. | a &= exp; | a = a & (ex); |
|= | Bitwise or and assignment operator | a |= exp; | a = a | (exp); |
^= | Bitwise xor and assignment operator. | a ^= exp; | a = a ^ (exp); |
Note: the expression
a *= b + c;
is equivalent to
a = a * (b + c);
and not
a = a * b + c;
Example of compound operators
Let's have a look at an example using some of these operators:
#include <iostream>
using namespace std;
int main() {
int a = 3, b = 2;
a += b;
cout << a << endl;
a -= b;
cout << a << endl;
a *= b;
cout << a << endl;
a /= b;
cout << a << endl;
return 0;
}
This will give the output:
5
3
6
3
4.4 Order of Operations
In mathematics and computer programming, the order of operations (or operator precedence) is a collection of rules that reflect conventions about which procedures to perform first in order to evaluate a given mathematical expression.
The following table is the operator precedence in C/C++
Precedence 1 | Operator 2 | Description | Example | Associativity 3 |
---|---|---|---|---|
1 | :: | Scope resolution | class::mdata class::mfunction() |
Left-to-right |
:: | Global scope | ::variable | Right-to-left | |
2 | ++ -- |
Post increment and post decrement |
v++ v-- |
Left-to-right |
( ) | Grouping (parentheses) | (expr) | Left-to-right | |
( ) | Function call | function(args) | Left-to-right | |
[ ] | Array indexing or subscripting | array[index] | Left-to-right | |
. | Member selection by reference | obj.field obj.mfunc() |
Left-to-right | |
-> | Member selection by pointer | obj->field obj->mfunc() |
Left-to-right | |
3 | ++ -- |
Pre increment and pre decrement |
++v --v |
Right-to-left |
+ - |
Unary positive and negative |
-N -(a+b) |
Right-to-left | |
! ~ |
Logical NOT and bitwise not |
!a, !(a&&b) ~k |
Right-to-left | |
(dataType) | Type cast (convert value to temporary value of type) | (int) var | Right-to-left | |
* | indirection (dereference) | *p | Right-to-left | |
& | address-of | &var | Right-to-left | |
sizeof | Size-of (Determine size in bytes) | sizeof(int), sizeof(var) | Right-to-left | |
new new[] |
Dynamic memory allocation | Person *p = new Person; int *scores = new int[100]; |
Right-to-left | |
delete delete[] |
Dynamic memory deallocation | delete p; delete [] score; |
Right-to-left | |
4 | .* ->* |
Pointer to member | Left-to-right | |
5 | * / % |
Multiplication Division Remainder |
3 * x y / z a % 2 |
Left-to-right |
6 | + - |
Addition Subtraction |
a + b a - b |
Left-to-right |
7 | << >> |
Bitwise shift left Bitwise shift right |
x << 2 y >> 2 |
Left-to-right |
<< >> |
Overloaded stream I/O | cout << x cin >> y |
Left-to-right | |
8 | < <= > >= |
For relational operators < and ≦ respectively For relational operators > and ≧ respectively |
x < 10, x <= 10 x > 10, x >= 10 |
Left-to-right |
9 | == != | For relational = and ≠ respectively | x == y, x != y | Left-to-right |
10 | & | Bitwise and | x & 0x00008000 | Left-to-right |
11 | ^ | Bitwise xor | x ^ 0xFFFF0000 | Left-to-right |
12 | | | bitwise or | x | 0x00000004 | Left-to-right |
13 | && | Logical AND | x && y | Left-to-right |
14 | || | Logical OR | x || y | Left-to-right |
15 | ? : | Conditional operator | (x < y)? x : y; | Right-to-left |
= | Direct assignment (provided by default for C++ classes) | x = y + b * sqrt(c) | Right-to-left | |
+= -= | Assignment by sum and difference | x += 2, x -= 2 | Right-to-left | |
*= /= %= | Assignment by product, quotient, and remainder | x *= 2, x /= 2, x %= 2 | Right-to-left | |
<<= >>= | Assignment by bitwise left shift and right shift | x <<= 2, x >>= 2 | Right-to-left | |
&= ^= != | Assignment by bitwise and, xor, and or | a &= b, a |= b, a ^= b | Right-to-left | |
16 | throw | Throw operator (for exceptions) | throw over_flow | Left-to-right |
17 | , | The comma (separate expressions) | i = 0, j = 0, i++, j++ | Left-to-right |
- Highest operator precedence to lowest. Operators in the same group (horizontal lines separate groups) have the same precedence.
- Unary operators are in blue, binary operators are in yellow, and the single ternary operator is in green
- Associativity is only used when two or more operators of the same precedence exist.
- Left-to-right: evaluates left to right. e.g. in a + b + c, a + b is evaluated first.
- Right-to-left: evaluates right to left. eg. in a = b = c, b = c is evaluated first.
Precedence of Function Calls
Consider the following program: associativity of the + operator is left-to-right, but it does not mean f1() is always called before f2(). The output of the following program is in fact compiler-dependent.
#include <stdio.h>
int x = 0;
int f1()
{
x = 5;
return x;
}
int f2()
{
x = 10;
return x;
}
int main()
{
int p = f1() + f2();
printf("p = %d, x = %d \n\n", p, x);
return 0;
}
Carefully to Use Comma
Comma has the least precedence among all operators and should be used carefully.
For example, consider the following program: the output is 1. See this and this for more details.
#include <stdio.h>
int main()
{
int a;
a = 1, 2, 3; // Evaluated as (a = 1), 2, 3
printf("%d", a);
return 0;
}
Precedence of Logical and Increment Operators
x && y++
- The second operand, y++, is evaluated only if x is true (non-zero).
- Thus, y is not incremented if x is false (zero).
#include <stdio.h>
#include <iostream>
using namespace std;
int main()
{
int x = 0;
int y = 10;
if (x && y++) {
cout << "Condition result is true." << endl;
} else {
cout << "Condition result is false." << endl;
}
cout << "y = " << y << endl;
return 0;
}
4.5 Multiple Unsequenced Modifications to the Same Variable
The modification of a variable is unsequenced relative to another operation on the same variable. This may lead to undefined behavior.
The analyzer detected an expression leading to undefined behavior. A variable is used several times between two sequence points while its value is changing. We cannot predict the result of such an expression. Let's consider the notions of "undefined behavior" and "sequence point" in detail.
Undefined behavior is a feature of some programming languages — most famously C/C++. In these languages, to simplify the specification and allow some flexibility in implementation, the specification leaves the results of certain operations specifically undefined.
For example, in C, the use of any automatic variable before it has been initialized yields undefined behavior, as do division by zero and indexing an array outside of its defined bounds. This specifically frees the compiler to do whatever is easiest or most efficient should such a program be submitted. In general, any behavior afterward is also undefined. In particular, it is never required that the compiler diagnose undefined behavior — therefore, programs invoking undefined behavior may appear to compile and even run without errors at first, only to fail on another system, or even on another date. When an instance of undefined behavior occurs, so far as the language specification is concerned anything could happen, maybe nothing at all.
A sequence point in imperative programming defines any point in a computer program's execution at which it is guaranteed that all side effects of previous evaluations will have been performed, and no side effects from subsequent evaluations have yet been performed. They are often mentioned in reference to C and C++ because the result of some expressions can depend on the order of evaluation of their subexpressions. Adding one or more sequence points is one method of ensuring a consistent result because this restricts the possible orders of evaluation.
It is worth noting that in C++11, the terms sequenced before/after, sequenced, and unsequenced were introduced instead of sequence points. Many expressions, resulting in undefined behavior in C++03, became defined (for instance, i = ++i). These rules were also supplemented in C++14 and C++17. The analyzer issues a false positive regardless of the used standard. The certainty of the expressions of i = ++i type is not an excuse to use them. It is better to rewrite such expressions to make them more understandable. Also, if you need to support an earlier standard, you can get a bug that is hardly debugged.
i = ++i + 2; // undefined behavior until C++11
i = i++ + 2; // undefined behavior until C++17
f(i = -2, i = -2); // undefined behavior until C++17
f(++i, ++i); // undefined behavior until C++17,
// unspecified after C++17
i = ++i + i++; // undefined behavior
cout << i << i++; // undefined behavior until C++17
a[i] = i++; // undefined behavior until C++17
n = ++i + i; // undefined behavior
Sequence points come into play when the same variable is modified more than once within a single expression. An often-cited example is an expression i = i++, which both assigns i to itself and increments i. The final value of i is ambiguous, because, depending on the language semantics, the increment may occur before, after, or interleaved with the assignment. The definition of a particular language might specify one of the possible behaviors or simply say the behavior is undefined. In C and C++, evaluating such an expression yields undefined behavior.
C and C++ define the following sequence points:
- Between evaluation of the left and right operands of the && (logical AND), || (logical OR), and comma operators (,). For example, in the expression *p++ != 0 && *q++ != 0;, all side effects of the sub-expression *p++ != 0 are completed before any attempt to access q.
- Between the evaluation of the first operand of the ternary "question-mark" operator and the second or third operand. For example, in the expression a = (*p++) ? (*p++) : 0; there is a sequence point after the first *p++, meaning it has already been incremented by the time the second instance is executed.
- At the end of a full expression. This category includes expression statements (such as the assignment a = b;), return statements, the controlling expressions of if, switch, while, or do-while statements, and all three expressions in a for statement.
- Before a function is entered in a function call. The order in which the arguments are evaluated is not specified, but this sequence point means that all of their side effects are complete before the function is entered. In the expression f(i++) + g(j++) + h(k++), f is called with a parameter of the original value of i, but i is incremented before entering the body of f. Similarly, j and k are updated before entering g and h respectively. However, it is not specified in which order f(), g(), h() are executed, nor in which order i, j, k are incremented. The values of j and k in the body of f are therefore undefined. Note that a function call f(a,b,c) is not a use of the comma operator and the order of evaluation for a, b, and c is unspecified.
- At a function return, after the return value is copied into the calling context. (This sequence point is only specified in the C++ standard; it is present only implicitly in C[4].)
- At the end of an initializer; for example, after the evaluation of 5 in the declaration int a = 5;.
- In C++, overloaded operators act as functions, so a call of an overloaded operator is a sequence point.
Now let's consider several samples causing undefined behavior:
int i, j;
...
X[i] = ++i;
X[i++] = i;
j = i + X[++i];
i = 6 + i++ + 2000;
j = i++ + ++i;
i = ++i + ++i;
We cannot predict the calculation results in all these cases. Of course, these samples are artificial and we can notice the danger right away. So let's examine a code sample taken from a real application:
while (!(m_pBitArray[m_nCurrentBitIndex >> 5] &
Powers_of_Two_Reversed[m_nCurrentBitIndex++ & 31])) {
}
return (m_nCurrentBitIndex - BitInitial - 1);
The compiler can calculate either of the left or right arguments of the '&' operator first. It means that the m_nCurrentBitIndex variable might be already incremented by one when calculating "m_pBitArray[m_nCurrentBitIndex >> 5]". Or it might still be not incremented.
This code may work well for a long time. However, you should keep in mind that it will behave correctly only when it is built in some particular compiler version with a fixed set of compilation options. This is the correct code:
while (!(m_pBitArray[m_nCurrentBitIndex >> 5] &
Powers_of_Two_Reversed[m_nCurrentBitIndex & 31])) { ++m_nCurrentBitIndex; }
return (m_nCurrentBitIndex - BitInitial);
This code does not contain ambiguities anymore. We also got rid of the magic constant "-1".
Programmers often think that undefined behavior may occur only when using post-increment, while pre-increment is safe. It's not so. Further is an example from a discussion on this subject.
[Warning] multiple unsequenced modifications
Question
When you compile the following code in C++Builder 10.3, you will get a warning message: [bcc32c Warning] multiple unsequenced modifications to 'a'. The variable a is modified while being used twice between sequence points.
The code:
a = (++a) % 2;
It seems to me that there is no undefined behavior because the 'a' variable does not participate in the expression twice.
Answer
There is undefined behavior here. Another thing is that the probability of error occurrence is rather small in this case. The '=' operator is not a sequence point. It means that the compiler might first put the value of the a variable into the register and then increment the value in the register. After that, it calculates the expression and writes the result into the a variable, and then again writes a register with the incremented value into the same variable. As a result, we will get a code like this:
In assembly code:
MOV R1, a
LOAD R2, [R1]
ADD R2, #1
MOD R3, R1, #2
SAVE R3, [R1]
SAVE R1, [R1]
Pseudocode:
REG = a;
REG++;
a = (REG) % 2;
a = REG;
The compiler has the absolute right to do so. Of course, in practice, it will most likely increment the variable's value at once, and everything will be calculated as the programmer expects. But you should not rely on that.
Consider one more situation with function calls.
The order of calculating function arguments is not defined. If a variable change over time serves as an argument, the result will be unpredictable. This is unspecified behavior. Consider this sample:
int a = 0;
foo(a = 2, a);
The Foo() function may be called both with the arguments (2, 0) and with the arguments (2, 2). The order in which the function arguments will be calculated depends on the compiler and optimization settings.
References:
- PVS-Studio Documentation. V567.
- Wikipedia. Undefined behavior.
- Wikipedia. Sequence point.
- Klaus Kreft & Angelika Langer. Sequence Points and Expression Evaluation in C++.
- Discussion at Bytes.com. Sequence points.
- Discussion at StackOverflow.com. Why is a = (a+b) - (b=a) a bad choice for swapping two integers?
- cppreference.com. Order of evaluation.
4.6 Bitmask
In computer science, a mask or bitmask is data that is used for bitwise operations, particularly in a bit field. Using a mask, multiple bits in a byte, nibble, word, etc., can be set either on, off, or inverted from on to off (or vice versa) in a single bitwise operation.
Masking bit to 1
The bitwise-or (|) operation can be used to turn certain bits on, following the principle that n or 1 = 1 and n or 0 = n. Therefore, bitwise-or can be used with a 1 to ensure a bit is on. To leave a bit unchanged, bitwise-or is used with a 0.
Example: Masking the higher nibble (bits 4, 5, 6, 7) to 1, and the lower nibble (bits 0, 1, 2, 3) unchanged.
1001 0101 = 0x95
or 1111 0000 = 0xF0 (mask 1 value)
––-––––––––––
1111 0101 = 0xF5
1010 0011 = 0xA3
or 1111 0000 = 0xF0 (mask 1 value)
––-––––––––––
1111 0011 = 0xF3
0001 0001 = 0x11
or 1111 0000 = 0xF0 (mask 1 value)
–––-–––––––––
1111 0001 = 0xF1
Masking bit to 0
More often in practice, bits are "masked off" (or masked to 0) than "masked on" (or masked to 1). When a bit is ANDed with a 0, the result is always 0, i.e. n and 0 = 0. To leave the other bits as they were originally, they can be ANDed with 1, since n and 1 = n.
Example: Masking the higher nibble (bits 4, 5, 6, 7) to 0, and the lower nibble (bits 0, 1, 2, 3) unchanged.
1001 0101 = 0x95
and 0000 1111 = 0x0F (mask 0 value)
––––-––––––––
0000 0101 = 0x05
1010 0011 = 0xA3
and 0000 1111 = 0x0F (mask 0 value)
––-––––––––––
0000 0011 = 0x03
0001 0001 = 0x11
and 0000 1111 = 0x0F (mask 0 value)
––-––––––––––
0000 0001 = 0x01
Toggling bit values
So far we have covered how to turn bits on and turn bits off, but not both at once. Sometimes it does not really matter what the value is, but it must be made the opposite of what it currently is. This can be achieved using the bitwise-xor ( ^, exclusive or) operator. The xor returns 0 if the two corresponding bits are the same value, and the result will be a 1 if only the bits are different values. Therefore inversion of the values of bits can be done by XORing them with a 1. Since $n\;xor\;0 = n\(, and \)n\;xor\;1 = \bar n$.
Example: Toggling bit values
1001 0101 = 0x95
xor 0000 1111 = 0x0F (mask 1 value)
––––-––––––––
1001 1010 = 0x9A
1010 0011 = 0xA3
xor 1111 1111 = 0xFF (mask 1 value)
––-––––––––––
0101 1100 = 0x5C
0001 0001 = 0x11
xor 1100 0011 = 0x0F (mask 1 value)
––-––––––––––
1101 0010 = 0xD2
Querying the status of bits values
It is possible to use bitmasks to easily check the state of individual bits regardless of the other bits. To do this, turning off all the other bits using the bitwise-and (&) is done as discussed above and the value is compared with 1. If it is equal to 0, then the bit is off, but if the value is any other value, then the bit is on. What makes this convenient is that it is not necessary to figure out what the value actually is, just that it is not 0.
Example: Querying the status of the 4th bit
1001 1101
and 0001 0000 = 0x10 (mask 1 value)
-------------
0001 0000
if (a & 0x10)
cout << "The bit 4 is one" << endl;
else
cout << "The bit 4 is zero" << endl;
Example: Querying the status of the higher nibble (bit 4, 5, 6, 7)
1001 1101
and 1111 0000 =0xF0 (mask 1 Value)
-------------
1001 1101
if ( (a & 0xF0) >> 4) == 0x09)
cout << "The higher nibble are 0x09" << endl;
else
cout << "The higher nibble are not 0x09" << endl;
Questions
Write a console application for each question.
- Calculate the Area of a Triangle: Input the width of the base and the height, then display the area on the screen.
- Mileage to Kilometers: Input a value in miles and convert it to kilometers. (1 mile is equal to 1.60934 kilometers)