| Home | Classes | Papers | Essays | Short Fiction | About |

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

  1. Write a C program using an if statement that checks if a const int X is 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?
  2. Modify Listing 2 to also print "X is not even!" when X is odd using an else clause. Test with both even and odd values. Why is this preferable to writing a second if statement with the condition X % 2 != 0?
  3. Write a C program that uses if, else if, and else to classify a const int X into one of the following categories and prints the result:
    • Negative
    • Zero
    • Between 1 and 100 inclusive
    • Greater than 100
  4. The lecture warns that omitting {} from an if statement only attaches the first statement to the conditional. Write a program that demonstrates this bug by writing an if without braces followed by two printf calls. Which printf is controlled by the if? Verify by testing with a condition that evaluates to false.
  5. Write a C program using a switch statement that takes a const int day between 1 and 7 and prints the corresponding day of the week. Use a default case for any value outside that range.
  6. The lecture states that without a break statement, control "falls through" to the next case in a switch. Write a program that demonstrates this by writing a switch with three cases and no break statements. What gets printed when the first case matches? When might fall-through behavior actually be useful?
  7. Rewrite the following if-else chain as a switch statement:
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?

  1. Write a C program that uses nested if statements (an if inside another if) to check whether a const int X is both positive and even. Print an appropriate message for each of the four possible combinations: positive even, positive odd, negative even, negative odd.
  2. The assembly output in Listing 5 shows that the compiler uses a jne (jump if not equal) instruction to implement the if statement. Looking at the assembly, identify which line performs the conditional jump and where control is transferred when the condition is false. What does testl %eax, %eax do and why is it needed?
  3. Further modify the ternary example in Listing 19 to include a check for a negative number all in the same line.

Footnotes:

1

gcc will optimize a call to printf by replacing it with puts if there is only a single string provided as an argument.

2

Again, we see here that gcc has optimized the instructions. Checking evenness can be done by seeing if the least significant bit is set to 0.

Contact: [email protected] | rss feed | Compiled with org-mode | Licensed under CC BY-NC-SA 4.0