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
x
andy
equal to each other in value? - Is
x
greater thany
in value? - Is
i
greater than or equal toj
?
To compare values, we can use comparison operators, like so:
x == y
this expression evaluates to the result of: whetherx
andy
are equal in valuex != y
whetherx
is not equal toy
in valuex > y
whetherx
is greater thany
x < y
whetherx
is less thany
x >= y
whetherx
is greater than or equal toy
x <= y
whetherx
is 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) |
C has special instructions that allow us to execute code only upon certain conditions. These instructions are called conditional instructions or conditional statements.
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();
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{
...
}
}
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.
-
Conjunction (AND) - Test if both things are true (“
A && B
”): Takes 2 boolean inputs, gives 1 boolean output. Given bool variables or expressionsA
andB
, the expression “A && B
” returns a bool variable. It evaluates to true if and only ifA
andB
are 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 expressionsA
andB
, 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 bothA
andB
are 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 whenA
is false; and it evaluates to false whenA
is 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!A
is 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 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 ofx
andy
. Say for examplex = 7
(00000111
) andy = 28
(00011100
),x & y
would be00000100
in binary, or4
. - Bitwise Disjunction (Bitwise OR) - (”
x | y
”) Does the OR operation on each corresponding bit ofx
andy
. Say for examplex = 7
(00000111
) andy = 28
(00011100
),x | y
would be00011111
in binary, or31
. - Bitwise Negation (Bitwise NOT) - (”
~x
”) Does the NOT operation on each single bit ofx
. Say for examplex = 7
(00000111
), then~x
would be11111000
in binary. And ifx
is a signed number, thenx = -8
; ifx
is 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 ofx
andy
. Say for examplex = 7
(00000111
) andy = 28
(00011100
),x ^ y
would be00011011
in binary, or27
.
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
” wherey
is an integer): Shifts the binary value ofx
towards the left byy
bits. The news spaces are filled with 0’s. For examplex = 7
(00000111
), thenx << 2
would be00011100
, or28
. -
Bitwise Right-Shift - (”
x >> y
” wherey
is an integer): Shifts the binary value ofx
towards the right byy
bits. 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 >> 2
would be00000001
, or1
. - If the number is signed and positive, the empty spaces are filled with 0’s.
For example
x = 7
(00000111
), thenx >> 2
would be00000001
, or1
. - If the number is signed and negative, the empty spaces are filled with 1’s.
For example
x = -7
(11111001
), thenx >> 2
would 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 |
=, +=, -=, *=, /=, %=, &=, ^=, | =, <<=, >>= |
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.