While loops, do/while loops, and filter programs
In this lesson I will cover the following topics.
The while loop and the do/while loop.
The break and continue statements.
Filter programs.
In the last lesson we looked at the for loop. The other common type of loop is the while loop. The while loop is actually simpler than the for loop. It looks like this
while (x < y) { // Do stuff. }
Unlike the for loop, the while loop contains only the loop condition. If that condition is true, the body of the loop executes. It is important to note that, like the for loop, the condition is tested first. If the condition is false right away, the body of the loop will never execute. This is normally what you want. If your program is given no data to process, it should correctly do nothing. This situation happens quite often so it's important to handle it right.
Since the loop itself contains nothing that would change the condition, while loops normally contain statements that update the variables used in the condition. Actually you don't really need the for loop at all. You can get the same effect with the while loop.
for (i = 0; i < 10; i++) { printf("%d\n", i); }
or
i = 0; while (i < 10) { printf("%d\n", i); i++; }
The main advantage with the for loop is that it brings all the manipulating of the loop index variable to one place. Initializing, testing, and advancing the value of i is all in the for loop's header. However, the while loop above works exactly the same way.
So when would you use one rather than the other? Typically a while loop is the best choice when you aren't sure how many times the loop will execute. A for loop is more natural when there is a definite count to the number of passes. However, the distinction is largely a matter of style.
The final looping statement in C is a bit different than the other two. It looks like this
do { // etc... } while (x < y);
This differs from the for and while loops in that the condition is checked at the end of the loop rather than at the beginning. Consequently the loop body will execute at least once no matter what. This is a very important difference between the do/while loop and both of the other two looping statements.
It turns out that the do/while loop is normally not what you want. It is used far less frequently than the other two looping statements. Why is this? As I've said, most of the time you want your program to gracefully do nothing when there is nothing to do. The do/while loop won't discover that there is nothing to do until it has already committed itself to doing something. That causes problems. If there is no data to work on, you end up working on something that doesn't exist before you realize it.
However, the do/while loop can be useful when interacting with a human. That is because you normally want to interact at least once. With the while loop or the for loop things are a bit more awkward. Consider the situation where you are trying to get an age from the user and you won't accept anything that looks invalid. Instead of just terminating the program when you get a bad age, you want to ask the user again. Here is how it might look
#include <stdio.h> int main(void) { int age; int age_okay; // Get the user's age. Don't let them enter something unacceptable. do { printf("Enter your age: "); scanf("%d", &age); // Do I like it? age_okay = (age >= 0 && age <= 125); if (!age_okay) { printf("Error: Age outside of acceptable bounds (0 to 125)\n"); } } while (!age_okay); // Print out the age I got. printf("I understand your age to be %d\n", age); return 0; }
Here a do/while loop is natural because we want to interact with the user at least once. If, afterwards, we don't like what we get, we can loop back and try again. Notice that in this program I used an ordinary integer variable (named age_okay) to hold the result of a condition. I did that because I think it's a bit clearer to put !age_okay into the two tests that follow rather than spelling out the condition all the time. You should read !age_okay as "NOT age okay." Thus the do/while loop reads as "do ...(stuff)... while NOT age okay."
This same program could have been written with a while loop. Here's how.
#include <stdio.h> int main(void) { int age; int age_okay = 0; // Keep looping until the user gets it right! while (!age_okay) { // Get the user's age. printf("Enter your age: "); scanf("%d", &age); // Check it. if (age < 0 || age > 125) { printf("Error: Age outside of acceptable bounds (0 to 125)\n"); } else { age_okay = 1; } } // Print out the age I got. printf("I understand your age to be %d\n", age); return 0; }
This version is really quite acceptable. Here I had to be sure to initialize the age_okay flag to "false" so that the loop would be sure to execute the first time. Inside the loop, I check the age and, if I like it, I set the age_okay flag to "true". That causes the loop to end. By setting up and using flag variables like age_okay you can often get what you want done with a while loop anyway. Nevertheless, the do/while loop is occasionally handy.
A quick note about my error messages. In general when you print an error message, you should give the user as much information as possible. Take a look at these possible error messages for the program above.
printf("Error\n"); printf("Error 0x9CF13B50. Please call technical support.\n"); printf("Error in age verification module\n"); printf("Error: Age unacceptable\n"); printf("Error: Age outside of acceptable bounds (0 to 125)\n"); printf( "Error: Age, %d, is outside of acceptable bounds. Expecting 0 to 125\n", age );
The last message is the best. It tells the user what went wrong and implies how it should be fixed. It also prints back the user's input so the user knows what the program thinks is going on. For example, if the program did the following the user would know that something was very wrong and that it was probably not the user's fault.
Enter your age: 39
Error: Age, -345918, is outside of acceptable bounds. Expecting 0 to 125
High quality programs produce good messages that help the user figure out what is wrong. Programs that produce cryptic messages that baffle the user are inferior programs.
Before I can call my presentation of the looping statements complete, I need to say a few words about the break and continue statements. The break statement causes a loop to end immediately. It is useful for bailing out of a loop when a special condition is encountered in the middle of the loop. It is also useful for getting out of infinite loops. Here is a loop that tries to test if number is prime.
for (i = 2; i < number; i++) { if (number % i == 0) { is_prime = 0; break; } }
The idea is to just scan over all values from 2 that are less than number and try to divide number by each of them. If I find a value that divides into number evenly, I set a flag to indicate that number is not prime and then break out of the loop at once. The next line executed would be the one immediately following the closing brace of the loop.
On the other hand, the break statement causes your program to jump around in unexpected ways. That can make your program much harder to read and modify. It is generally accepted that each block in your program should, ideally, be entered only from the top and be exited only from the bottom. The break statement violates this and can lead to programs that are harder to understand and maintain.
The continue statement is similar to break except that instead of the loop ending, it jumps back to the top right away. The continue statement is useful for dispensing with "uninteresting" cases immediately without the bother of creating an else clause. I'll show an example of continue in action a bit later.
Every language has its strengths and weaknesses. One of the strengths of C is in its ability to churn through a lot of data in an easy and natural way. Under Unix (as well as some other operating systems) each program that you run has three "files" available to it automatically. These files are called the standard input file, the standard output file, and the standard error file. Normally the standard input is "connected" to the keyboard of your terminal. When a program tries to read from standard input it reads the characters you type. Normally the standard output is "connected" to the display of your terminal. When a program tries to write to standard output it writes material on the screen.
The printf function that we have been using writes to standard output. The scanf function reads from standard input. That is why they work the way they do. However, the user can "redirect" these files to normal disk files without the program knowing anything about it.
Do you still have your "Hello, World" program on hand? If not, create hello.c again like this
#include <stdio.h> int main(void) { printf("Hello, World!\n"); return 0; }
Compile it and run it.
$ hello
Hello, World!
Now do this:
$ hello > output.txt
The ">" symbol on the command line causes the standard output of the program to be connected to the file output.txt. The output goes into that file instead of on your display. Edit output.txt using pico (or any other text editor) and you will see just what I mean.
Now try this program out...
#include <stdio.h> int main(void) { int ch; while ((ch = getchar()) != EOF) { putchar(ch); } return 0; }
This program doesn't look like much, but it is actually surprisingly powerful. But first it requires some explaining. The condition in the while loop looks pretty nasty, but it's not that bad. Let's take a closer look.
(ch = getchar()) != EOF
The condition is a test for inequality. The right operand, EOF, is a special value that is used to signal the end of a file. Read "End Of File" for EOF. The actual numeric value used for EOF is defined in stdio.h. It doesn't concern us.
The left operand of != is an assignment expression. The function getchar from the standard library is called and what it returns is put into the integer ch. The result of the assignment operation is the new value of ch---the character read by getchar. What does getchar do? It reads a single character from the standard input. The parentheses around the assignment are necessary because != has higher precedence. If I wrote
ch = getchar() != EOF
It would evaluate as
ch = (getchar() != EOF)
and I would be putting the value of 1 or 0 into ch depending on if getchar returns EOF or not.
So now lets look at that while loop again
while ((ch = getchar()) != EOF) { // etc... }
This loop begins by reading a single character from standard input and putting that character into ch. It then tests to see if that character is EOF. If not, it processes that character. Then it goes back and reads the next character from standard input and puts it into ch. If that next character is still not EOF it processes it as well. In short, this loop reads every character from standard input, one character at a time, processing each as it goes. When we run out of input, the loop ends.
This loop is very common. It looks a bit odd because there is an assignment in the loop condition. However, this is a standard C idiom. It is something that C programers recognize and understand at once. You will see it frequently.
Notice that using a while loop is just what we want here. If the input file is empty, getchar will return EOF at once and the loop doesn't do anything. That is exactly the desired response. Also a for loop is not necessary (or desirable) here because the act of getting, storing, and testing the next input character all happens in the loop condition. There is no need to advance a loop index because getchar will automatically get the next character the next time it is called.
Now what about the body of the loop? In my sample program there is just a single line
putchar(ch);
The library function putchar just prints a single character onto the standard output. Thus my program reads standard input and copies what it finds there to standard output. It is a simple file copying program!
Here's how you might use it
$ prog < afile.txt > bfile.txt
Here I'm assuming you called this little program prog. The "< afile.txt" on the command line tells the Unix shell to arrange things so that standard input is connected to the file afile.txt. The "> bfile.txt" tells the shell to connect standard output to bfile.txt. When the program runs it reads afile.txt one character at a time and prints those characters to bfile.txt. Yet the program knows nothing about files! It just reads standard input and writes to standard output. What could be simpler?
If you want to see this program in action, just run it like this
$ prog
Now it reads characters from the terminal keyboard and prints them to the terminal screen. It will read an entire line before it prints anything because terminal devices are "line buffered" by default. This is handled outside the domain of the program and won't make any difference to it. Type a ^D to send and end-of-file indication to the program and cause it to terminate.
Now check this out
$ prog < afile.txt
This prints afile.txt to the screen (the way the standard Unix program cat does).
$ prog > afile.txt
This allows you to type things at your terminal and it puts those things into the file afile.txt. Type a ^D to send and end-of-file indication to the program.
You see how flexible this is?
But it gets better. Suppose you decided that you didn't like the letter 'x' and you wanted to remove it from all of your files. Here is a program that would help you.
#include <stdio.h> int main(void) { int ch; while ((ch = getchar()) != EOF) { if (ch != 'x') { putchar(ch); } } return 0; }
Like before, this program reads the file at stdin and copies it to stdout. However, if it sees an 'x' character it just ignores it. In particular, it only outputs characters that are not 'x'. This program thus filters out 'x' characters from its input. If you want to test it before you use it, try this
$ prog Hello, World! Hello, World! I hate x! I hate ! xxyxx y Cool! It works! Cool! It works! ^D
Notice that the program prints out newline characters normally. That's why the output has the same line structure as the input. Of course if you wanted to change that...
#include <stdio.h> int main(void) { int ch; while ((ch = getchar()) != EOF) { if (ch == '\n') { printf("\n\n"); } else { putchar(ch); } } return 0; }
This program prints out two newline characters every time it finds one in the input file. It thus has the effect of double spacing the input. With a little imagination you can write programs that perform all sorts of interesting operations on the text in your files. Such programs are called "filter programs" because they filter their input.
Keep in mind that filter programs read and write simple minded streams of characters. When you use getchar you will see every space, tab, and newline in the input file. Check out this program that counts the number of lines in the input.
#include <stdio.h> int main(void) { int ch; int line_count = 0; while ((ch = getchar()) != EOF) { if (ch == '\n') { line_count++; } } printf("Found %d lines.\n", line_count); return 0; }
I wonder how many lines of code are in the "Hello, World" program.
$ prog < hello.c
Found 12 lines.
I wonder how many lines are in my login script.
$ prog < .profile
Found 47 lines.
Cool.
The other nice thing about filter programs is that they (typically) can process files of any size. Since they read their input files one character at a time, the amount of memory they require is independent of the file's size. Thus my line counting program above could process a 100 MByte file containing hundreds of thousands of lines, even if some of those lines were millions of characters long. It is a serious, useful program. And it is only a few short lines of C.
The while loop is a simplified version of the for loop. It just tests a condition at the top of the loop and executes the loop body if the condition is true. The do/while loop puts the condition at the end of the loop so that the loop body executes at least once. Normally you want to test loop conditions at the top of the loop, but the do/while loop is useful sometimes anyway.
The break statement causes the nearest enclosing loop to end immediately. It provides you with a quick way to bail out of a loop (for example if an error condition is noticed). The continue statement causes the nearest enclosing loop to repeat immediately. Both of these statements are sometimes useful but they can be abused. You should use them carefully.
You can easily process the file at your program's standard input using the classic loop
int main(void) { int ch; while ((ch = getchar()) != EOF) { // Process ch here } return 0; }
Using this skeleton you can write a wide variety of useful programs that filter their input in some way.