Control Flow & Bitwise
Comparison Operators & The Boolean Type
In order to have our code behave differently depending on the conditions during runtime, we need to be able to represent and answer “yes no” questions in our code.
Some of the questions that we might ask are, for example:
- Are the variables
xandyequal to each other in value? - Is
xgreater thanyin value? - Is
igreater than or equal toj?
To compare values, we can use comparison operators, like so:
x == ythis expression evaluates to the result of: whetherxandyare equal in valuex != ywhetherxis not equal toyin valuex > ywhetherxis greater thanyx < ywhetherxis less thanyx >= ywhetherxis greater than or equal toyx <= ywhetherxis less than or equal toy
Yet we still need a way to store the answer to these questions (the results of these expressions). Thus we have another data type called the “boolean” type in C.
Booleans can only have two values: true (1) or false (0).
To declare a boolean variable, we use the bool keyword.
(Note: We also need to do #include <stdbool.h> to enable bool type in C.)
int a = 4, b = 7;
bool x;
x = (a < b); // x has value **true**
x = (a >= b); // x now has value **false**
// Important Note: Boolean values are also represented with integers,
// where true is 1 and false is 0.
int x;
x = (a < b); // x has value 1
****x = (a >= b); // x now has value 0
Comparison operators have lower priority than arithmetic operators, for example:
For the expression x == 2 * y, “2 * y” is evaluated first, then == is evaluated.
Our now updated operator precedence table would be:
| +, -, ++, – | (unary operator) |
|---|---|
| *, /, % | |
| +, - | (binary operator) |
| <, <=, >, >= | (comparison) |
| ==, != | (comparison) |
| =, +=, -=, *=, /=, %= | (assignments) |
Conditional Execution
C has special instructions that allow us to execute code only upon certain conditions. These instructions are called conditional instructions or conditional statements.
Simple If Statement
The simple if statement takes the form:
if(bool_expression){
... // Do this if bool_expression evaluates to true.
}
// If bool_expression is false, then the instructions inside the parentheses
// will be skipped.
We can put many instructions inside the brackets, and if bool_expression is true, then the instructions inside will be conditionally executed. These instructions are called a compound statement or a block, and they are seen as 1 statement by the compiler.
If we have multiple instructions inside an if statement, they must be indented by 1 level from the code outside the brackets, like so:
if(x > y){
x = x + y;
y = y + x;
}
x = y;
This style of writing code is called the K & R Style, named after the creators of C.
Note that we can also omit the brackets whenever there’s only one statement inside the if statement, like so:
if(bool_expression)
doSomething();
If-Else Statement
The if ... else statement takes the form:
if(bool_expression){
... // Do this IF AND ONLY IF bool_expression is true.
}else{
... // Do this IF AND ONLY IF bool_expression is false.
}
Note that we can also nest if statements inside of if statements, this is called nesting. For example:
if(condition){
if(another_condition){
...
}
}else{
if(a_third_condition){
...
}else{
...
}
}
Else If
The else if statement is a bit complex, it takes the form:
if(first_condition){
... // Do this if first_condition is true, then SKIP all remaining conditions.
}else if(second_condition){
... // Do this if first_condition is false, but second_condition is true.
}else if(third_condition){
... // Do this if both conditions above are false, but third_condition is true.
}
We can also add a condition to handle the case when all conditions are false, like so:
if(first_condition){
...
}else if(second_condition){
...
}else if(third_condition){
...
}else{
... // Do this when ALL conditions above are false.
}
Note that the program will examine the conditions sequentially. Whenever a condition is met, the corresponding code block will be run, and afterwards the rest of the conditions will be skipped.
So say for example if first_condition and third_condition are both true, then only the block corresponding to first_condition will be run, and the block corresponding to third_condition will be skipped.
Boolean Logic
-
Conjunction (AND) - Test if both things are true (“
A && B”): Takes 2 boolean inputs, gives 1 boolean output. Given bool variables or expressionsAandB, the expression “A && B” returns a bool variable. It evaluates to true if and only ifAandBare both true. Otherwise it evaluates to false.A B A && B True True True True False False False True False False False False -
Disjunction (OR) - Test if at lease one of both things is true (“
A || B”): Takes 2 boolean inputs, gives 1 boolean output. Given bool variables or expressionsAandB, the expression “A || B” returns a bool variable. It evaluates to true when at least one of the two is true. It evaluates to false if and only if bothAandBare false.A B A || B True True True True False True False True True False False False -
Negation (NOT) - Test if a thing is false or Flip a bool value (from true to false, or from false to true) (“
!A”): Takes 1 boolean input, gives 1 boolean output. Given a single bool variable or expressionA, the expression “!A” returns a bool variable. It evaluates to true whenAis false; and it evaluates to false whenAis true. From this we know that, for any bool expressionA, when we use!on it (negate it) 2 times, the result is equal to the original value, i.e.!(!A) == A. Important Note: When negating an expression (which is not a single variable), remember to “protect the expression” by enclosing it in parentheses, because!takes priority over other operators like&&or||, and will often times mess up the expression that you are trying to negate. For example,!(A && B)is not the same as!A && B(because!Ais evaluated first), thus we need to write!(A && B)when we need to negateA && B. In short, it’s good practice to write!(...)instead of!...for safety when negating any bool expression.A !A True False False True
Bitwise Operations
Bitwise operations treat values (integer values like int, short, char) as arrays of binary digits (e.g. short x = 126; would be 00000000 11111110), and then do logical operations on them bit by bit.
- Bitwise Conjunction (Bitwise AND) - (”
x & y”): Does the AND operation on each corresponding bit ofxandy. Say for examplex = 7(00000111) andy = 28(00011100),x & ywould be00000100in binary, or4. - Bitwise Disjunction (Bitwise OR) - (”
x | y”) Does the OR operation on each corresponding bit ofxandy. Say for examplex = 7(00000111) andy = 28(00011100),x | ywould be00011111in binary, or31. - Bitwise Negation (Bitwise NOT) - (”
~x”) Does the NOT operation on each single bit ofx. Say for examplex = 7(00000111), then~xwould be11111000in binary. And ifxis a signed number, thenx = -8; ifxis an unsigned number, thenx = 120. (Very important to take the sign into account.) - Bitwise Exclusive Disjunction (Bitwise XOR) - (”
^”): Does the XOR operation on each corresponding bit ofxandy. Say for examplex = 7(00000111) andy = 28(00011100),x ^ ywould be00011011in binary, or27.
Bit Shifting
Bit shifting treat values (integer values like int, short, char) as arrays of binary digits (e.g. short x = 126; would be 00000000 11111110), and then shift the bits either left or right.
- Bitwise Left-Shift - (”
x << y” whereyis an integer): Shifts the binary value ofxtowards the left byybits. The news spaces are filled with 0’s. For examplex = 7(00000111), thenx << 2would be00011100, or28. - Bitwise Right-Shift - (”
x >> y” whereyis an integer): Shifts the binary value ofxtowards the right byybits. However, depending on whether the number is signed or unsigned, and whether the number is positive or negative if signed, the spaces will be filled with either 1 or 0:- If the number is unsigned, the empty spaces are filled with 0’s.
For example
x = 7(00000111), thenx >> 2would be00000001, or1. - If the number is signed and positive, the empty spaces are filled with 0’s.
For example
x = 7(00000111), thenx >> 2would be00000001, or1. - If the number is signed and negative, the empty spaces are filled with 1’s.
For example
x = -7(11111001), thenx >> 2would be11111110, or-2.
In other words, if the number is signed, then when we right shift it, the empty spaces will be filled with the rightmost bit of the original value.
- If the number is unsigned, the empty spaces are filled with 0’s.
For example
Thus the up-to-date operator precedence table would be:
| !, ~, (typecast), +, -, ++, – | (unary operators) | |
|---|---|---|
| *, /, % | (binary arithmetic - high priority) | |
| +, - | (binary arithmetic - low priority) | |
| «, » | (bit shift operators) | |
| <, <=, >, >= | (comparison operators - high priority) | |
| ==, != | (comparison operators - low priority) | |
| & | bitwise AND | |
| | | bitwise OR | |
| && | logical AND | |
| || | logical OR | |
| =, +=, -=, *=, /=, %=, &=, ^=, | =, «=, »= | (assignments and shorthands) |
Switch-Case
A switch … case statement takes the form:
switch(expression){
case LITERAL_1:
... // Runs if expression == LITERAL_1
break; // After running, skip to the end
case LITERAL_2:
... // Runs if expression == LITERAL_2
break; // skip to end
case LITERAL_3:
... // Runs only when expression is LITERAL_3
case LITERAL_4:
... // Runs when expression is LITERAL_3 or LITERAL_4,
// since there's not break statement at the end of LITERAL_3's case
break;
default:
... // Runs if expression is not equal to any of the above literals.
}
Each of the cases are scanned sequentially, just like in if ... else if statements.
Note that unlike if ... else if statements, after running instructions inside a case, the program does not skip to the end, and keeps on running, unless there’s a break; statement at the end of a case block.