Completed: / exercises

Control Structures in C

Learning Objectives: This material explains how to implement and control the logical functionality of a program in C using control structures. Additionally, it emphasizes the importance of making the code visually clear.
Control structures in C are as follows:
This part of the lecture material is likely the most straightforward and easiest to grasp in the course, as there are no significant differences in the behavior of these structures in C compared to other programming languages.

Truth Values

First, it is essential to understand what is considered true in C, i.e., how the truth value of a program statement is determined. In C, it is defined numerically as follows:
Example. After the assignment, the value of the variable would be interpreted as true here int8_t is_warm = -25;.
int8_t temperature_sensor_reading = -25;

if (temperature_sensor_reading) {
    printf("The sensor is active, temperature is being measured.\n");
} else {
    printf("The sensor is inactive or malfunctioning.\n");
}

Logical Operators

The logical operators we can use to evaluate the truth values of expressions in C are as follows:
Examples.
uint8_t variable1 = 0;
uint8_t variable2 = 5;
int8_t variable3 = -27;

int8_t result1 = !variable1; // true
int8_t result2 = variable1 && variable2; // false
int8_t result3 = variable2 && variable3; // true
In C, logical operations && (logical AND) and || (logical OR) use short-circuit evaluation. This means that the evaluation of expressions stops as soon as the result is determined.
For && (logical AND), if the first operand evaluates to false (0), the entire expression is false, so the second operand is not evaluated.
For || (logical OR), if the first operand evaluates to true (non-zero), the entire expression is true, so the second operand is not evaluated.

Comparison Operators

C's comparison operators are numerical:
Remember from earlier material that in C, characters are also treated as numbers according to the ASCII encoding. So whenever a condition is numerically true, the code block of the statement will be executed.
In C, multiple conditions or comparisons cannot be combined into a single condition; instead, comparisons must be separated and combined with a logical operator. For example:
// Python: ('a' <= character <= 'z')
if ((character >= 'a') && (character <= 'z')) 
   {
   ...
   }
Note! To avoid bugs, remember to use parentheses around different expressions in comparison and conditional structures to ensure that your intention is directly reflected in the code. Otherwise, you must be very familiar with the order of operations of C operators!
Note! Another common programming mistake is using the assignment operator = instead of the comparison operator ==. Similarly, the bitwise operator & can be confused with the logical AND operator &&. The compiler will usually warn about these.

Conditional Structures

There are two conditional structures in C. The traditional if-else structure and the multi-part conditional structure switch.

If-else if-else Discussion

C's conditional statements include the same if, else if, and else branches found in other programming languages, and they function the same way. The branches are tested in the given order, and the first one that evaluates to true will execute its associated code block, and the structure will exit. Conditional expressions are placed inside parentheses ( ) .
if (number < 10) {
    printf("The number is less than 10");
}
else if (number < 100) {
    printf("The number is less than 100");
}
else if (number < 1000) {
    printf("The number is less than 1000");
}
else {
    printf("The number is equal to or greater than 1000");
}
Sometimes you might see C control structures implemented without code blocks. This is perfectly valid C code, in which case only the statement following the condition is executed if the condition is true.
if (number < 100)
    printf("The number is less than 100");
Note! If you want to execute multiple statements when a condition is true, you must use a block, so it's good practice to always use curly braces from the very beginning!

Switch Flips the Switch

C also has a multi-part conditional structure called switch, which tests whether a variable's value matches one of the given integer constants. The reasoning is that switch might be clearer when checking constant values compared to if-else structures.
The syntax of the switch statement is as follows. Note that, unlike other structures, this statement does not use block markers but instead relies on the break; statement to indicate where a block ends.
switch (variable) {
   case CONSTANT1:        
      statement;
      statement;
      break;
   case CONSTANT2:
      statement;
      statement;
      break;
   default:
      statement;
      statement;
      break;
}
The switch structure is processed from top to bottom, comparing the variable's value to the constants. If a match is found, the corresponding code block is executed until a break statement is encountered. After that, the structure exits. The default block is optional and is executed only if none of the other conditions match.
If the break is omitted, execution will fall through from one block to the next until a break is encountered or the structure ends. This can be useful if you know what you're doing, as it allows you to handle multiple conditions with the same block.
In fact, the switch statements could be implemented using an equivalent if-else structure:
switch(choice) {		if (choice == 'p') {  
    case 'p':				length();
        length();		} else if (choice == 'm') {
        break;				mass();
    case 'm':			} else if (choice == 't') {
        mass();			volume();
        break;			} else if (choice == 'l') {
    case 't':				temperature();
        volume();		} else {
        break;				error();
    case 'l':			}
        temperature();
        break;
    default:
        error();
        break;
}
Here, the evaluation of conditions flows downward in the same way as in the if-else structure. So, in a way, the switch structure is unnecessary, but it is an easy-to-read way to implement constant value checks with many parallel options. Additionally, you'll undoubtedly encounter this structure in code written by others, so it's good to know what it's all about.

Loop Structures

In C, we can of course perform both counted and conditional looping.

Counting loops

Counting looping in C is implemented using the for statement. The statement in C is slightly more demanding for the coder than in other languages.
The syntax is as follows.
for (initialization statement; condition statement; increment statement) {
    statement;
    statement;
}
Now, to help with the for loop, we need a loop variable that indicates the iteration in the condition statement.
  1. In the initialization statement, we give the loop variable its starting value.
  2. In the condition statement, we test the value of the loop variable.
    1. If the condition is met, the code block is executed.
    2. If the condition is not met, the loop is exited.
  3. The increment statement defines how the loop variable changes for the next iteration, i.e., for the next condition test.
Examples.
uint8_t i;                  uint8_t i;                  uint8_t i;

for (i=0; i < 10; i++) {    for (i=10; i > 0; i--) {    for (i=0; i <= 20; i+=2) {
   do_something();             do_something();             do_something();
}                           }                           }
In the first case, the value of the loop variable increases by one, in the second case it decreases by one, and in the third case it increases by two.
As can be seen, various comparisons can be made in the condition statement of the for loop. And the increment statement can take different forms... in fact, (almost) all C operators and even function calls whose return values are tested can be used there!
And to make the for loop not too simple, we can initialize multiple loop variables within it, separated by commas. This allows us to conveniently have multiple auxiliary variables within the code block...
int i,j;

for (i=0, j=10; i < 10; i++, j--) {
   do_something(i,j); 
}							

Conditional Looping

Conditional looping in C can be implemented in two ways.

Test First, Then Execute

In the while statement, the code block is executed in the loop as long as the condition is true. Therefore, the while loop is best suited for situations where the number of repetitions is not known in advance.
The syntax is very straightforward.
while (condition) {
    statement;
    statement;
}
A simple example. On the side, the equivalent for statement for the C compiler.
uint8_t i=0;            uint8_t i=0;

while (i < 10) {        for (i=0; i < 10; i++) { 
   do_something();         do_something();
   i++;                 }
}
Sometimes the for statement may be stylistically better because it keeps the loop control statements close to each other structurally.

Execute First, Then Test

Again, the do-while structure executes its code block first and then tests the condition statement afterward. If the condition is false, the loop is exited. This structure can sometimes be a stylistically better solution than the while structure, for example, if a repetitive operation on the variables in the condition needs to be performed before testing them.
The syntax is not particularly special.
do {
   statement;
   statement;
} while (condition);

Let's Take a Break

Related to control structures, we can also control the flow of code within a block using the commands break and continue.
Now, with the break command, we can jump out of any block even in the middle of its execution. For example:
while (x > 0) {
    if (x == SPECIAL_CONSTANT) {       
        break;
    }
    x = do_something();
} 
Then continue jumps to the next iteration of the loop, but of course, it executes the increment statement first. For example, the function do_something is not executed if the equality condition in the if statement is met.
for (x=0; x < 100; x++) {
    if (x == SPECIAL_CONSTANT) {
       continue;
    }    
    do_something();
}

Infinite Loop

One special case in the use of loop structures is very common in embedded systems programming.
Sometimes, the functionality of an embedded program is implemented inside a single infinite loop, which is part of the program's main function. As noted earlier, a C program runs until this function ends. So, conveniently (well, not really...), in an embedded program, waiting for input is done by running an infinite loop. This type of solution is called a superloop structure and might be familiar from Arduino programming, for example.
int main() {         
   
   while (1) {    
      if (check_input()) {
         do_something();
      }
    }                  
} 
Input to the program can be obtained by polling, as in the example above with the check_input function, or by interrupts. Well, more on that in future material...
Although this solution is simple, it is not technically good from a programming perspective. For example, because the condition testing is sequential and not parallel. Secondly, most of the execution time, the program tests statements that do not actually occur from the device's perspective. If the device has the resources to run an operating system, it is a better solution to let it decide when each functionality of the program is executed. The operating system often also manages the device's power consumption programmatically, which can be more difficult to achieve in a superloop structure.
We will return to this when learning to program our Hardware device.

A Word on Coding Style

Let's further justify why it's a good idea to use curly braces. The following code snippet is valid C, but not very readable. For example, now if the programmer succumbs to Python-style indentation, which if statement does the else structure belong to?
if (n > 0) 
   m = n + 10;
   if (n > 10)
      m = n;
else 
   n += 10;
From the example, we also notice how difficult it can be to read nested if-else structures. Sometimes a beginner programmer creates overly long if-else structures (which on paper will max out the print quota). Later, finding a bug in such a program is a really thankless task and can actually take hours of time.
In this course, we do not write such code, but we know how to divide our program into clear blocks and functions that are at most one screen in length.

In Conclusion

With the looping skills learned from Python, you can handle everything in this course, as long as you learn the syntax and special cases. New concepts, such as do-while loops and switch structures, are useful at times.
Different C language standards support slightly different C code regarding code blocks and control structures. For example, in the old C89 standard, variables could not be declared in the initialization structure of a for loop but had to be declared earlier in the code. However, in current standards, this is possible.
for (int a = 0; a < 10; a++) {
}
Since in C, the appearance of code has little significance, we can write for loops, for example, like this. The functionality of the program could not really be easily seen from the code...
Final greetings from the assistants: In this course, we save everyone's time and effort by writing our own code in a modular fashion using functions and code blocks that are clearly readable with proper indentation, parentheses, and curly braces.
?