CS211: Conditional Statements
Table of Contents
Introduction
Think about the last time you went to a grocery store. If it's anything like my local supermarket, when you approach the sliding doors to enter they appear to open as if they were operated by invisible elves. If we're able to reasonably convince ourselves that it isn't magical elves, there must be something present allowing for that movement to occur. Further investigation would have us finding a sensor which is able to detect whether some body is in front of the door and if that's the case the mechanism to open the door should fire. Furthermore, there is something preventing the door from opening if there is nothing detectable. The internal logic of the automatic door is conditional on the state of its components.
Programs too will have special logic that will only be used in the event a certain condition is met. In C, these logical scaffolds are called conditional statements.
Control Flow
Conditional statements are used to direct the runtime behavior of the executing program. Depending on how they are evaluated, conditional statements will decide which set of instructions are processed next. In computer science we call this the control flow of a program—i.e. how each instruction flows into the next. C provides a few different statements to aid in the construction of a program's control flow.
if Statement
The if statement is the fundamental conditional statement. It's syntax is as follows:
1: if(conditional) 2: statement; //Do this
As long as conditional evaluates to a true value then the statement will be executed. A filled in example should illustrate the point:
1: #include <stdio.h> 2: const int X = _; // Will set later... 3: int main(){ 4: printf("Welcome to the even checker!\n"); 5: if(X % 2 == 0) 6: printf("%d is even!\n", X); 7: }
If I compile and run this with X set to 4, I get the following results:
josephraskind@stargazer:/tmp/cond$ gcc if.c -o if; ./if Welcome to the even checker! 4 is even!
What about if I set X equal to 5?
josephraskind@stargazer:/tmp/cond$ gcc if.c -o if; ./if Welcome to the even checker!
What happened to my printout? Well, since the conditional failed the corresponding call to printf was not executed. The printf statement is attached to the if statement evaluating a true conditional—it will never execute if a false is evaluated.
Taking a look at the assembly generated by gcc -O0 -fno-omit-frame-pointer -fno-stack-protector -S if.c should help us understand what's happening:
1: .file "if.c" 2: .text 3: .globl X 4: .section .rodata 5: .align 4 6: .type X, @object 7: .size X, 4 8: X: 9: .long 4 10: .LC0: 11: .string "Welcome to the even checker!" 12: .LC1: 13: .string "%d is even!\n" 14: .text 15: .globl main 16: .type main, @function 17: main: 18: .LFB0: 19: .cfi_startproc 20: endbr64 21: pushq %rbp 22: .cfi_def_cfa_offset 16 23: .cfi_offset 6, -16 24: movq %rsp, %rbp 25: .cfi_def_cfa_register 6 26: leaq .LC0(%rip), %rdi 27: call puts@PLT 28: movl $4, %eax 29: andl $1, %eax 30: testl %eax, %eax 31: jne .L2 32: movl $4, %eax 33: movl %eax, %esi 34: leaq .LC1(%rip), %rdi 35: movl $0, %eax 36: call printf@PLT 37: .L2: 38: movl $0, %eax 39: popq %rbp 40: .cfi_def_cfa 7, 8 41: ret 42: .cfi_endproc 43: .LFE0: 44: .size main, .-main 45: .ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0" 46: .section .note.GNU-stack,"",@progbits 47: .section .note.gnu.property,"a" 48: .align 8 49: .long 1f - 0f 50: .long 4f - 1f 51: .long 5 52: 0: 53: .string "GNU" 54: 1: 55: .align 8 56: .long 0xc0000002 57: .long 3f - 2f 58: 2: 59: .long 0x3 60: 3: 61: .align 8 62: 4:
There's a lot going on here. First, .LC0: and .LC1: represent the locations in memory for the two string constants used in the print statements. Second, there are two labels contained within the main function, .LFB0: which represents the entry point, and .L2: which represents the conditional jump destination. Line 26 loads the "Welcome..." string into the first argument register, and Line 27 call printf1. Lines 28-30 represent the conditional expression x % 2 == 0: 4 is loaded into %eax which is then given a bitwise and with 1 and is then tested to see if the result equals 02. If the zero flag has not been set then control will be redirected over to .L2:, otherwise every further instruction will be processed. Therefore when X is equal to 5 control will be transferred ober to .L2: and the second call to printf will be passed over.
if statements can also encompass more than one statement. For example:
1: if(conditional){ // Start block with { 2: statement 1; 3: statement 2; 4: .... 5: statement N; 6: } // End block with }
Applied to a "real code" expmaple we would see:
1: #include <stdio.h> 2: const int X = _; // Will set later... 3: int main(){ 4: printf("Welcome to the even checker!\n"); 5: if(X % 2 == 0){ 6: printf("%d is even!\n", X); 7: printf("Isn't that neat!\n"); 8: } 9: }
Running the above with X equal to 10 we get:
josephraskind@stargazer:/tmp/cond$ gcc if.c -o if; ./if Welcome to the even checker! 4 is even! Isn't that neat!
And with X equal to 13:
josephraskind@stargazer:/tmp/cond$ gcc if.c -o if; ./if Welcome to the even checker!
Be careful when omitting "{}" from your conditional statements! Without them, C will only match the first statement to the conditional's control flow behaviour. It's good practice to ere on the side of caution and always include brackets when possible.
else if
if statements can be further modified to allow for mutually exclusive behaviors. For example, say we didn't want users to enter negative numbers we could write:
1: #include <stdio.h> 2: const int X = _; // Will set later... 3: int main(){ 4: printf("Welcome to the even checker!\n"); 5: if(X < 0){ 6: printf("Ew! %d is negative!\n", X); 7: } 8: if(X % 2 == 0){ 9: printf("%d is even!\n", X); 10: } 11: if(X % 2 != 0){ 12: printf("%d is odd!\n", X); 13: } 14: }
But if I run the above code with X equal to -4 I get the following results:
josephraskind@stargazer:/tmp/cond$ gcc if.c -o if; ./if Welcome to the even checker! Ew! -4 is negative! -4 is even!
But this wasn't what I wanted! I didn't want to print out the evenness of the negative number!
We can solve this with the use of an else if clause:
1: #include <stdio.h> 2: const int X = _; // Will set later... 3: int main(){ 4: printf("Welcome to the even checker!\n"); 5: if(X < 0){ 6: printf("Ew! %d is negative!\n", X); 7: }else if(X % 2 == 0){ 8: printf("%d is even!\n", X); 9: }else if(X % 2 != 0){ 10: printf("%d is odd!\n", X); 11: } 12: }
Now if I run the above code with X equal to -4 we'll see something different:
josephraskind@stargazer:/tmp/cond$ gcc if.c -o if; ./if Welcome to the even checker! Ew! -4 is negative!
The if else clause makes each subsequent if block mututally exclusive. No two blocks can be entered for the same set of conditional checks.
else
Sometimes we want mutually exclusive behaviors without having to write an additional conditional check. For example,
1: #include <stdio.h> 2: const int X = _; // Will set later... 3: int main(){ 4: printf("Welcome to the even checker!\n"); 5: if(X % 2 == 0){ 6: printf("%d is even!\n", X); 7: } 8: if(X % 2 != 0){ 9: printf("%d is odd!\n", X); 10: } 11: }
gets the job done, but it looks a bit clunky. There's a lot more work our eyes have to do when scanning the code. Instead we can write,
1: #include <stdio.h> 2: const int X = _; // Will set later... 3: int main(){ 4: printf("Welcome to the even checker!\n"); 5: if(X % 2 == 0){ 6: printf("%d is even!\n", X); 7: }else{ 8: printf("%d is odd!\n", X); 9: } 10: }
now X % 2 != 0 is implied as it is the only other possibility if the original conditional check fails.
We can combine this with our negative check above (Listing 12):
1: #include <stdio.h> 2: const int X = _; // Will set later... 3: int main(){ 4: printf("Welcome to the even checker!\n"); 5: if(X < 0){ 6: printf("Ew! %d is negative!\n", X); 7: }else if(X % 2 == 0){ 8: printf("%d is even!\n", X); 9: }else{ 10: printf("%d is odd!\n", X); 11: } 12: }
You can only ever have one else in your if statement conditional chain.
switch Statement
The switch statement is a variation of the if statement. It provides slightly different functionality to using if-else statement chains. The syntax is as follows:
1: switch(expr){ 2: case SOMETHING: 3: statement 1; // Do statement 4: case SOMETHING_ELSE: 5: statement 2; // Do statement 6: break; // Exit control flow 7: default: 8: statement 3; // Do statement 9: }
The switch statement takes an expression that resolves to an integer type (char, int, short int, long int, etc). And will transfer control to the block of code underneath the corresponding label. The default label is used when no corresponding label for the expression's value can be found. The break statement transfers control out of the switch block—this is particularly useful because without it the control will "fall through" to the next case. Meaning in the above example if expr resolves to SOMETHING then both statement 1 and statement 2 will be executed (statement 3 will not because of the break statement).
?: Operator
Although not strictly a conditional statement, the ?: "ternary" operator is often bunched into the same group due to its conditional evaluation logic. The syntax is as follows
conditional ? eval_true : eval_false
Using the evenness example from earlier:
1: #include <stdio.h> 2: const int X = _; // Will set later... 3: int main(){ 4: printf("Welcome to the even checker!\n"); 5: printf(X % 2 == 0 ? "%d is even!\n" : "%d is odd!\n" , X); 6: }
Compiling and running the above gives us:
josephraskind@stargazer:/tmp/cond$ gcc if.c -o if; ./if Welcome to the even checker! 4 is even!
Exercises
- Write a C program using an
ifstatement that checks if aconst int Xis greater than 100 and prints "X is large!" if so. Test it with at least three different values. What happens when the condition is false? - Modify Listing 2 to also print "X is not even!" when
Xis odd using anelseclause. Test with both even and odd values. Why is this preferable to writing a secondifstatement with the conditionX % 2 != 0? - Write a C program that uses
if,else if, andelseto classify aconst int Xinto one of the following categories and prints the result:- Negative
- Zero
- Between 1 and 100 inclusive
- Greater than 100
- The lecture warns that omitting
{}from anifstatement only attaches the first statement to the conditional. Write a program that demonstrates this bug by writing anifwithout braces followed by twoprintfcalls. Whichprintfis controlled by theif? Verify by testing with a condition that evaluates to false. - Write a C program using a
switchstatement that takes aconst int daybetween 1 and 7 and prints the corresponding day of the week. Use adefaultcase for any value outside that range. - The lecture states that without a
breakstatement, control "falls through" to the next case in aswitch. Write a program that demonstrates this by writing aswitchwith three cases and nobreakstatements. What gets printed when the first case matches? When might fall-through behavior actually be useful? - Rewrite the following
if-elsechain as aswitchstatement:
1: const int X = 2; 2: if(X == 1) printf("one\n"); 3: else if(X == 2) printf("two\n"); 4: else if(X == 3) printf("three\n"); 5: else printf("other\n");
Are there any cases where a switch cannot replace an if-else chain?
- Write a C program that uses nested
ifstatements (anifinside anotherif) to check whether aconst int Xis both positive and even. Print an appropriate message for each of the four possible combinations: positive even, positive odd, negative even, negative odd. - The assembly output in Listing 5 shows that the compiler uses a
jne(jump if not equal) instruction to implement theifstatement. Looking at the assembly, identify which line performs the conditional jump and where control is transferred when the condition is false. What doestestl %eax, %eaxdo and why is it needed? - Further modify the ternary example in Listing 19 to include a check for a negative number all in the same line.