Skip to content

Instantly share code, notes, and snippets.

@remzmike
Last active March 28, 2023 19:58
Show Gist options
  • Save remzmike/260f66ec107f8ed26011cf913babcfe6 to your computer and use it in GitHub Desktop.
Save remzmike/260f66ec107f8ed26011cf913babcfe6 to your computer and use it in GitHub Desktop.
Hello C : 04 : if statements and for loops

Part 4: if statements and for loops

We skipped a detailed explanation of if statements and for loops in part 3 because they deserve a lot of attention.

If statements and for loops the two most important flow control statements in most programming.

if statements

These are probably the first thing beginners realize about programming.

When code executes, if statements determine how the execution branches to other paths in the tree of code you are implicitly defining through the structure of your code.

There are two key things to understand in if statements:

  1. Boolean expressions.
  2. How the the statement executes.

First, boolean expressions.

This code:

void main() {
    int x = 42;
    printf("x < 10 evaluates to: %d\n", x < 10);
    printf("x > 10 evaluates to: %d\n", x > 10);
}

Prints:

x < 10 evaluates to: 0
x > 10 evaluates to: 1

The x < 10 and x > 10 expressions evaluate to a boolean value, 0 or 1, because that is the definition of the < and > operators. Those are boolean expressions, and if/else statements use boolean expressions in their syntax to determine how to branch.

In boolean logic, a boolean expression that evaluates to 0 is considerd to be 'false', and anything else is considered to be 'true'.

If you put a 2 in a boolean expression, it will evaluate to true, the same as 1 does. This quirk is deliberate, but for now it is not important.

This code:

if (0) {
    printf("0 is true"); // this will not print, because 0 is not true
}

if (1) {
    printf("1 is true");
}

if (2) {
    printf("2 is true");
}

if (-1) {
    printf("-1 is true");
}

Prints:

1 is true
2 is true
-1 is true

Everything but 0 is considered true.

How the if statement executes

When an if/else statement encounters an expression which evaluates to true it will execute that block and then exit the statement, skipping the remaining if expressions and their blocks.

However, if an if statement has an else block, then that is a catch-all which runs when none of the other if expressions evaluates to true.

So this code:

if (0) {
    printf("zero");
} else if (0) {
    printf("zero");
} else {
    printf("not zero");
}

Will print "not zero", because no expression evaluated to true.

An if statement will only ever execute one of its blocks each time it runs.

Putting that all together, you should be able to understand this code:

if (0) {
    printf("zero");
} else if (0) {
    printf("zero");
} else if (1) {
    printf("one");
} else if (0) {
    printf("zero");
} else {
    printf("no true expression encountered");
}       

The first and second 0 expressions evaluate to false, so those blocks do not execute, then the first 1 expression is encountered, so execution goes into that block where it prints one, then execution skips to the end of the if statement, after the final curly brace. The if statement has then finished executing.

And this code:

if (0) {
    printf("zero");
} else if (0) {
    printf("zero");
} else if (0) {
    printf("zero");
} else {
    printf("no true expression encountered");
}       

Will print no true expression encountered, after evaluating every expression in the statement, but not executing their blocks, since none of their expressions were true.

Short circuiting

The behavior of if statements which causes them to skip the remaining blocks after encountering a true expression is called short circuiting.

Interestingly, boolean expressions also support this short circuiting behavior, and it is sometimes useful, or sometimes mystifying, so it is important to understand.

As we have seen, when an if statement sees an expression which evaluates to true it will skip the evaluation of remaining expressions.

Logical boolean expressions do the same thing. They will stop evaluating the remaining parts of an expression if the code knows it will not change the result of the expression. This feature can lead to tricky code, but is also used with optimization in mind.

In this expression, the || operator performs a logical OR, which means it will evaluate to true when either operand is true. So when the expression is evaluated from left to right and encounters the first true expression, the executing code knows the expression will be true regardless of the remaining sub-expressions, so the remaining expressions are not evaluated.

0 || 0 || 1 || 0 || 0

If one of the expressions after the 1 was a function call that took a long time to run, then it would not get called.

This short-circuiting also happens with the logical AND operator, &&, which returns true only when both operands are true.

0 && 1 && 0 && 0

With && expressions, the code knows that the expression will be false as soon as it evaluates the 0 operand for the && operator, so the other parts are not evaluated.

Consider this code:

int SlowFunction() {
    Sleep(5000);
    return 1;
}

void main() {
    if (0 || 0 || 1 || 0 || SlowFunction()) {
        // SlowFunction() was not called because the expression
        // short-circuited when the 1 was encountered.
        printf("SlowFunction() not called for OR expression\n");
    } else {
        // this block does not run
    }

    if (0 && SlowFunction() && 1 && 0 && 0) {
        // this block does not run
    } else {
        // and when we get here SlowFunction() was still not called,
        // because the expression short-circuited to false after the first 0.        
        printf("SlowFunction() not called for AND expression\n");
    }

    if (SlowFunction() && 0) {
        // this block does not run
    } else {
        printf("SlowFunction() WAS called, but the expression was still false\n");
    }
}

It prints:

SlowFunction() not called for OR expression
SlowFunction() not called for AND expression
SlowFunction() WAS called, but the expression was still false

The last line isn't printed until SlowFunction() returns, which takes 5 seconds.

switch statements

The switch statement is a branching statement, like if statements, which provides increased functionality at the cost of more obscure syntax and an increased chance of bugs, since the programmer must skip remaining logical cases explicitly with a break statement.

We will use them later in Win32 GUI programming, but you can generally consider them a more arcane and dangerous form of if statements.

Example switch statement:

int expression = 1;

switch(expression) {
    case 0:
        printf("zero");
        break;

    case 1:
        printf("one");
        break;

    case 2:
        printf("two");
        break;

    default:
        printf("not 0, 1 or 2");
}

Equivalent if statement:

int expression = 1;

if (expression == 0) {
    printf("zero");
} else if (expression == 1) {
    printf("one");
} else if (expression == 2) {
    printf("two");
} else {
    printf("not 0, 1 or 2");
}

They both print "one".

SAMPLE CODE: 04a.c

for loops

For loops, and their cousins while and do...while, are the other main ways to change the flow of your code.

They work by repeating a block of code, for as long as their associated boolean expression remains true.

Consider this code:

void main() {
    int note_frequencies[6] = { 247, 330, 392, 494, 659, 784 };

    for (int i = 0; i <= 5; i++) {
        int note_frequency = note_frequencies[i];
        Beep(note_frequency, 200);
    }
}

When it executes, it is equivalent to some initialization code:

int i = 0;

Plus this code executing each loop, potentially forever, but only 5 times here because i is incremented after each loop:

if (i <= 5) {
    printf("a");
    int note_frequency;
    note_frequency = note_frequencies[i];
    Beep(note_frequency, 200);
} else {
    break; // break means exit the loop
}
i++;

The for loop statement has three parts in parenthesis separated by semicolons.

In the for loop equivalent above you can see all three of those parts represented:

int i = 0;

(i <= 5)

i++;

The grammar of a for loop can be described as:

for (<initialization-statement>; <test-expression>; <update-statement>) <block>

The initialization-statement is usually a declaration + assignment, int i = 0, or just an assignment statement, a = 0.

If it's a declaration-and-assignment statement, then that variable exists in the for loop block's scope. Otherwise, you are using a variable from a parent scope.

The test-expression is checked before each loop iteration. If it evaluates to true then the loop runs one time, then runs the update-statement, then repeats.

For loops are often used to loop over an array of values, so the usual form is one where the initialization-statement defines a counter variable to track your position in the loop, while the test-expression checks whether you have reached the final position, and the update-statement increments the position by one before the loop repeats.

while and do...while loops

The while and do..while statements also allow you to define loops, but they are less common.

First of all, a for loop's syntax often forces you to remember to increment the loop variable, in the update-statement. The while loops, similar to switch vs if, require you to write a statement inside the blocks that does the same thing, if you are using them similarly to for loops. As such, you are more likely to make mistakes.

If I want to write a while loop I often just write a for loop instead and leave out the initialization-statement and update-statement.

This while loop:

int i = 0;
while (i < 10) {
    i++;
    printf("%d\n", i);
}   

Is equivalent to this for loop:

int i = 0;
for ( ; i < 10; ) {
    i++;
    printf("%d\n", i);
}

You can even write a for loop without a test-expression and exit it manually with break.

So, this for loop is also equivalent to both of the above loops:

int i = 0;
for ( ; ; ) {
    if (i < 10) {
        i++;
        printf("%d\n", i);
    } else {
        break;
    }
}        

That explains the while loop, but the do...while loop is very slightly different, and does something the for loop does not do alone, which is execute the block one time before evaluating the test-expression. So, a while loop and for loop can execute 0 times, but a do...while loop always executes at least once.

This ends up being very useful is certain situations, and if you try to use a for loop to do the same thing you would end up having to duplicate the code in the for loop block once before the for loop in order for it to act like a do...while loop.

For example, this while loop:

int i = 5;
do {
    printf("%d", i);
    i++;
}
while (i < 5);

Would look like this as an equivalent for loop:

int i = 5;
printf("%d", i);
for ( ; i < 5; ) {
    printf("%d", i);
    i++;
}

This can be annoying if you don't want to duplicate the code executed in the loop.

However, generally you will find that you will mostly end up using for loops.

SAMPLE CODE: 04b.c

#include <stdio.h>
#include <Windows.h>
int SlowFunction() {
Sleep(5000);
return 1;
}
void main() {
int x = 42;
printf("x < 10 evaluates to: %d\n", x < 10);
printf("x > 10 evaluates to: %d\n", x > 10);
printf("\n");
if (0) {
printf("0 is true");
}
if (1) {
printf("1 is true");
}
if (2) {
printf("2 is true");
}
if (-1) {
printf("-1 is true");
}
printf("\n\n");
if (0) {
printf("zero");
} else if (0) {
printf("zero");
} else {
printf("not zero");
}
printf("\n\n");
if (0) {
printf("zero");
} else if (0) {
printf("zero");
} else if (1) {
printf("one");
} else if (0) {
printf("zero");
} else {
printf("no true expression encountered");
}
printf("\n\n");
if (0) {
printf("zero");
} else if (0) {
printf("zero");
} else if (0) {
printf("zero");
} else {
printf("no true expression encountered");
}
printf("\n\n");
if (0 || 0 || 1 || 0 || SlowFunction()) {
// SlowFunction() was not called, because the expression short-circuited when the 1 was encountered.
printf("SlowFunction() not called for OR expression\n");
} else {
// this block does not run
}
if (0 && SlowFunction() && 1 && 0 && 0) {
// this block does not run
} else {
// and when we get here SlowFunction() was still not called, because the expression short-circuited to false after the first 0.
printf("SlowFunction() not called for AND expression\n");
}
if (SlowFunction() && 0) {
// this block does not run
} else {
printf("SlowFunction() WAS called for AND expression, but it still evaluated to false altogether\n");
}
printf("\n\n");
{
int expression = 1;
switch(expression) {
case 0:
printf("zero");
break;
case 1:
printf("one");
break;
case 2:
printf("two");
break;
default:
printf("not 0, 1 or 2");
}
}
printf("\n\n");
{
int expression = 1;
if (expression == 0) {
printf("zero");
} else if (expression == 1) {
printf("one");
} else if (expression == 2) {
printf("two");
} else {
printf("not 0, 1 or 2");
}
}
}
#include <stdio.h>
#include <Windows.h>
void main() {
int note_frequencies[6] = { 247, 330, 392, 494, 659, 784 };
/*for (int i = 0; i <= 5; i++) {
int note_frequency = note_frequencies[i];
Beep(note_frequency, 200);
}*/
// rough equivalent of the above for loop, with prints for better understanding
{
int i = 0;
if (i <= 5) {
printf("a");
int note_frequency;
note_frequency = note_frequencies[i];
Beep(note_frequency, 200);
} else {
goto END_LOOP;
}
i++;
if (i <= 5) {
printf("b");
int note_frequency;
note_frequency = note_frequencies[i];
Beep(note_frequency, 200);
} else {
goto END_LOOP;
}
i++;
if (i <= 5) {
printf("c");
int note_frequency;
note_frequency = note_frequencies[i];
Beep(note_frequency, 200);
} else {
goto END_LOOP;
}
i++;
if (i <= 5) {
printf("d");
int note_frequency;
note_frequency = note_frequencies[i];
Beep(note_frequency, 200);
} else {
goto END_LOOP;
}
i++;
if (i <= 5) {
printf("e");
int note_frequency;
note_frequency = note_frequencies[i];
Beep(note_frequency, 200);
} else {
goto END_LOOP;
}
i++;
if (i <= 5) {
printf("f");
int note_frequency;
note_frequency = note_frequencies[i];
Beep(note_frequency, 200);
} else {
goto END_LOOP;
}
i++;
if (i <= 5) {
printf("g");
int note_frequency;
note_frequency = note_frequencies[i];
Beep(note_frequency, 200);
} else {
goto END_LOOP;
}
i++;
printf("h");
}
printf("i");
END_LOOP:
printf("\nLoop ended...");
// g, h and i are not printed, which more accurately shows what a real for loop does internally
// also, the loop can technically continue forever, but i know how this one will run, so i only pasted as many repeats as I needed, plus 1 more to show g is not printed
printf("\n\n");
{
int i = 0;
while (i < 10) {
i++;
printf("%d\n", i);
}
}
printf("\n\n");
{
int i = 0;
for ( ; i < 10; ) {
i++;
printf("%d\n", i);
}
}
printf("\n\n");
{
int i = 0;
for ( ; ; ) {
if (i < 10) {
i++;
printf("%d\n", i);
} else {
break;
}
}
}
printf("\n\n");
{
int i = 5;
do {
printf("%d", i);
i++;
}
while (i < 5);
}
printf("\n\n");
{
int i = 5;
printf("%d", i);
for ( ; i < 5; ) {
printf("%d", i);
i++;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment