CS211: Loops
Table of Contents
Introduction
Oftentimes we run into situations where we would like to repeat the same computation multiple times. As an example, consider a program which computes the power of one number raised to another. We know that 3^8 is equivalent to 3 × 3 × 3 × 3 × 3 × 3 × 3 × 3. This would be easy enough to hard code1. We could write the following C program:
1: #include <stdio.h> 2: int main(){ 3: int val = 3 * 3 * 3 * 3 * 3 * 3 * 3 * 3; 4: printf("3^8 = %d\n", val); 5: }
And it works well enough, printing the expected 3^8 = 6561. But what if 3 and 8 weren't constant values, but variables? If I replace 3 with x:
1: #include <stdio.h> 2: int main(){ 3: int x = 3; 4: int y = 10; 5: int val = x * x * x * x * x * x * x * x; 6: printf("%d^%d = %d\n", x, y, val); 7: }
If we run the above, we don't get the expected 3^10 = 59049, but 3^10 = 6561! It's clear that because Line 5 does not adapt to the fact that the target power has changed there can be no alteration in the computational task. One way we can fix this is with looping.
while Loops
There are three looping constructs provided by the C programming language, two of which are slight variatios on the foundational while loop. The syntax of a while loop is as follows:
1: while(condition) 2: block; // Do this
Pretty simple! While a certain condition is met, i.e. the expression resolves to true, the instructions in the block will be executed. Once the instructions in the block have been exhausted the condition is checked again. If it is met the instructions start over (hence the "loop"); if the condition is not met the next set of instructions outside of the loop will be executed.
Here's how we can modify the power example to work with a while loop:
1: #include <stdio.h> 2: int main(){ 3: int x = 3; // Base 4: int y = 10; // Target power 5: int pow = 1; // Starting accumulator value 6: int i = 0; // Iterator 7: while(i < y){ // Conditional check 8: pow = pow * x; // Computation 9: i = i + 1; // Incrementing i to eventually satisfy looping condition 10: } 11: printf("%d^%d = %d\n", x, y, pow); 12: }
There's a lot going on here. First, you'll notice there are two more variables: pow and i. pow is an accumulator which stores the partial result of each computational step needed to get the final power value. i acts as a tally that keeps track of how many loops have been performed. Additionally, you can see that there's some new logic: a conditional operator i < y and a code block. The conditional is used to limit the number of loops performed (more on this later), and the code block is for defining a set of computations to be performed each loop. Here, the code block is multiplying the pow accumulator with the base x and incrementing the i loop tally. We can trace the value at the end of every loop for each variable in a table to keep track of what's happening:
| Loop # | i | pow |
|---|---|---|
| 1 | 1 | 3 |
| 2 | 2 | 9 |
| 3 | 3 | 27 |
| 4 | 4 | 81 |
| 5 | 5 | 243 |
| 6 | 6 | 729 |
| 7 | 7 | 2187 |
| 8 | 8 | 6561 |
| 9 | 9 | 19683 |
| 10 | 10 | 59049 |
After the 10th loop completes the conditional is checked on final time. Since i is now equal to 10 the expression i < 10 resolves to false and control is transferred to the first block outside of the loop, which is the print function call.
Infinite Loops
It is possible to accidentally create loops that never terminate. These are often referred to as infinite loops. Here's an example of how one could occur through a little bug in our program:
1: #include <stdio.h> 2: int main(){ 3: int x = 3; // Base 4: int y = 10; // Target power 5: int pow = 1; // Starting accumulator value 6: int i = 0; // Iterator 7: while(i < y){ // Conditional check 8: pow = pow * x; // Computation 9: // Something's missing... 10: } 11: printf("%d^%d = %d\n", x, y, pow); 12: }
We can see here that the body of the while loop has had the tallying statement removed. This will cause i to never be incremented, thus keeping it at 0, and therefore never allowing for the conditional check to change values.
Try compiling the code from Listing 4 yourself. You'll find that the compiler does not warn you of the infinite loop!
Infinite loops are not always a bad thing. Sometimes it makes sense for a program to include an infinite loop. For example, a chat room program might want to constantly poll a collection of network socket connections to see whether or not a new message has come through. Or, maybe a video game program might want to have a thread dedicated to always painting the screen for graphics content. What's important is to recognize when an infinite loop is happening and how to stop it if it's not what you intended.
How Are Loops Implemented?
In order to get a better intuition for how loops work we can take a look at how the assembly code is generated for them. Here's what it looks like when I run gcc -O0 -fno-omit-frame-pointer -fno-stack-protector -S power_while.c on Listing 4:
1: .file "power_while.c" 2: .text 3: .section .rodata 4: .LC0: 5: .string "%d^%d = %d\n" 6: .text 7: .globl main 8: .type main, @function 9: main: 10: .LFB0: 11: .cfi_startproc 12: endbr64 13: pushq %rbp 14: .cfi_def_cfa_offset 16 15: .cfi_offset 6, -16 16: movq %rsp, %rbp 17: .cfi_def_cfa_register 6 18: subq $16, %rsp 19: movl $3, -12(%rbp) 20: movl $10, -16(%rbp) 21: movl $1, -4(%rbp) 22: movl $0, -8(%rbp) 23: jmp .L2 24: .L3: 25: movl -4(%rbp), %eax 26: imull -12(%rbp), %eax 27: movl %eax, -4(%rbp) 28: addl $1, -8(%rbp) 29: .L2: 30: movl -8(%rbp), %eax 31: cmpl -16(%rbp), %eax 32: jl .L3 33: movl -4(%rbp), %ecx 34: movl -16(%rbp), %edx 35: movl -12(%rbp), %eax 36: movl %eax, %esi 37: leaq .LC0(%rip), %rdi 38: movl $0, %eax 39: call printf@PLT 40: movl $0, %eax 41: leave 42: .cfi_def_cfa 7, 8 43: ret 44: .cfi_endproc 45: .LFE0: 46: .size main, .-main 47: .ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0" 48: .section .note.GNU-stack,"",@progbits 49: .section .note.gnu.property,"a" 50: .align 8 51: .long 1f - 0f 52: .long 4f - 1f 53: .long 5 54: 0: 55: .string "GNU" 56: 1: 57: .align 8 58: .long 0xc0000002 59: .long 3f - 2f 60: 2: 61: .long 0x3 62: 3: 63: .align 8 64: 4:
Again, there's a lot going on here so we'll focus on the major points. There are three labels to be aware of here .LFB0:, the label for the start of main, .L2:, the label for the conditional check, and .L3:, the label for the code block inside of the loop. Here is a table representing the variables to stack locations:
| Variable | Location |
|---|---|
| x | -12(%rbp) |
| y | -16(%rbp) |
| pow | -4(%rbp) |
| i | -8(%rbp) |
There's nothing special about the order in which the variables were assigned locations on the stack, this is just how gcc decided to do so.
Line 23 starts us on our loop with jmp .L2 which says there should be an unconditional jump to the label .L2. The jump brings us to Lines 30 and 31 which set up and perform the i < y comparison. i is moved into %eax (Line 30), y is then compared with %eax—y - i—(Line 31), and if the result is less than 0 control is transferred to .L3. Lines 25-28 represent the body of the loop (multiplying by pow by x and incrementing i). We can see that once the jl instruction fails on line 32 the instructions making up the printf call are followed one by one.
do while Loops
C provides a slight variation on the while loop with the do while loop. The syntax is as follows:
1: do{ 2: block; // Do this 3: }while(condition); // Needs a ; to terminate statement
The biggest difference here is that the block is processed first before the first conditional check. The do while construct does not add anything new, it just makes certain kinds of looping logic a bit easier to write. For example, let's say I wanted to write a program that prompted the user to enter a positive number. I could write:
1: #include <stdio.h> 2: int main(){ 3: int x; 4: printf("Enter a positive number: "); 5: scanf("%d", &x); 6: while(x <= 0){ 7: printf("Enter a positive number: "); 8: scanf("%d", &x); 9: } 10: printf("You entered: %d\n", x); 11: }
I could cut down on the repeat code with a do while statement instead:
1: #include <stdio.h> 2: int main(){ 3: int x; 4: do { 5: printf("Enter a positive number: "); 6: scanf("%d", &x); 7: } while(x <= 0); 8: printf("You entered: %d\n", x); 9: }
for Loops
The last looping structure provided by C is likely also it's most famous: the for loop. The syntax for the for loop is as follows:
1: for(expr1 ; expr2 ; expr3) 2: block; // Do something
expr1 is the declaration expression. It is where you can define or set a variable at the entrance of the for loop. expr2 is the conditional expression. It is where you can place the conditional check that will be used to determine if another loop will be completed. expr3 is the increment expression. It is used for performing an additional operation at the end of the loop that is not included in the block body. expr1, expr2, and expr3 are all optional.
When expr2 is not present it will be treated as a true value. This means that for(;;) is equivalent to a while(1) statement.
Here is an example of the power program with a for loop:
1: #include <stdio.h> 2: int main(){ 3: int x = 3; // Base 4: int y = 10; // Target power 5: int pow = 1; // Starting accumulator value 6: for(int i = 0; i < y; i = i + 1){ // Declaration, conditional check, and increment expression 7: pow = pow * x; // Computation 8: } 9: printf("%d^%d = %d\n", x, y, pow); 10: }
Both Listing 4 and Listing 11 are functionally equivalent2. The obvious change is that the loop tallying variable, i, has been declared in expr1 and is incremented in expr3. Everything else is identical.
Many people prefer to write for loops over while loops due to convenience. From the compiler's perspective, it often does not matter.
Additions like the for loop and the do while loop can be classified as syntactic sugar. They do not provide additional functionality for the language, but they provide stylisitic benefits.
Nested Loops
Loops can be nested within one another. For example, if I wanted to print out a 4x3 grid of asterixes I could write:
1: #include <stdio.h> 2: int main(){ 3: for(int i = 0; i < 4; i++){ 4: for(int j = 0; j < 3; j++){ 5: printf("*"); 6: } 7: printf("\n"); 8: } 9: }
There are many situations where nested for loops are desirable, particularly with complex data structures.
break Keyword
We can use the break statement that we learned about with the switch statement. break will immediately transfer control out of the loop to the next block available as if the condition failed. If we did not want our power function to iterate anymore if pow was greater than 100:
1: #include <stdio.h> 2: int main(){ 3: int x = 3; // Base 4: int y = 10; // Target power 5: int pow = 1; // Starting accumulator value 6: int i = 0; // Iterator 7: while(i < y){ // Conditional check 8: pow = pow * x; // Computation 9: if(pow > 100) 10: break; // Exists the loop 11: i = i + 1; // Incrementing i to eventually satisfy looping condition 12: } 13: printf("%d^%d = %d\n", x, y, pow); 14: }
I leave it as an exercise to you to see what happens when the above is ran.
continue Keyword
Similarly to the break keyword, the continue keyword alters the control flow of a loop. When a continue is reached control flow will be transferred to the "end" of the loop. For a while loop that means the conditional will immediately be rechecked. For a for loop that means the increment expression will be performed then the conditional will be checked. Here is a for loop that sums up all even numbers from 1 to 100:
1: #include <stdio.h> 2: int main(){ 3: int sum = 0; 4: for(int i = 0; i < 100; i++){ 5: if(i % 2 == 1) 6: continue; 7: sum = sum + i; 8: } 9: printf("sum = %d\n", pow); 10: }
Exercises
- Rewrite the power program from Listing 4 using a
forloop instead of awhileloop. Then rewrite it again using ado whileloop. Which version do you find most readable and why? The lecture provides a trace table for Listing 4 showing the value of
iandpowat the end of each loop. Construct a similar trace table by hand for the following program, then verify by running it:int x = 2; int y = 8; int pow = 1; int i = 0; while(i < y){ pow = pow * x; i = i + 1; }
- Write a
forloop that computes the sum of all integers from 1 to 100. Then write the equivalent using awhileloop. Verify both produce the same result. - The lecture states that
for(;;)is equivalent towhile(1). Write a program using each form that prints "looping…" exactly 5 times using abreakstatement to exit. Verify both behave identically. - Modify the even sum program from Listing 14 to instead sum all odd numbers from 1 to 100 using
continue. What change do you need to make to the conditional? Verify your result. - The lecture notes that the compiler does not warn about infinite loops. Write a program containing an infinite loop as shown in Listing 5, compile it with
gcc, and confirm no warning is produced. Then useCtrl+Cin the terminal to kill it. What doesCtrl+Cdo at the OS level? - Write a
forloop using thebreakkeyword that finds the first integer greater than 1 whose square exceeds 500. Print both the integer and its square. - The lecture shows the assembly output for the
whileloop in Listing 6. Compile theforloop version of the power program from Listing 11 withgcc -O0 -fno-omit-frame-pointer -fno-stack-protector -Sand compare the assembly output. Are they identical? What does this tell you about the relationship betweenforandwhileloops at the machine level? Using nested loops as shown in Listing 12, write a program that prints the following multiplication table:
1 2 3 4 5 2 4 6 8 10 3 6 9 12 15 4 8 12 16 20 5 10 15 20 25
- The lecture states that all three expressions in a
forloop are optional, as shown in Listing 10. Write three separate programs demonstrating this: one withexpr1omitted, one withexpr3omitted, and one with both omitted. In each case, what change must you make elsewhere to keep the program correct?
Footnotes:
"Hard coding" refers to writing logic directly into source code with little to no abstractions.
Although, they are not semantically equivalent. In the while loop version, i is declared outside of the loop block and therefore is visible after the loop finishes. In the for loop version, i is declared in the declaration expression and therefore is only visible within the for loop. From the perspective of the program output, this is splitting hairs. From the perspective of understanding the language runtime, this is an important distinction.