Skip to main content

Flow and Structured Programming

This chapter covers flow control and structured programming in C, which are essential concepts for writing well-organized code.

Introduction to Structured Programmingā€‹

In C, there are two classes of instructions:

  1. Simple instructions (the ones we've seen so far)
  2. Control instructions (which aggregate simple instructions)

Structured programming avoids unconditional jumps (like goto statements) in favor of clear, predictable control flow. According to computer scientist Edsger W. Dijkstra (1969), any algorithm can be expressed using just three fundamental control structures:

  1. Sequence - Instructions executed one after another in order
  2. Selection - Choosing between different code blocks based on conditions (if-else, switch)
  3. Iteration - Repeating blocks of instructions (loops like while, do-while, for)

These three structures form the foundation of structured programming and are sufficient to express any algorithm without resorting to arbitrary jumps.

Using these structures makes code more readable, maintainable, and less prone to bugs compared to "spaghetti code" that relies on uncontrolled jumps.

What is "Spaghetti Code"?

"Spaghetti code" is a pejorative term for code that has complex and tangled control flow, often due to excessive use of unconditional jumps like goto statements. Just as with actual spaghetti where it's hard to trace a single strand, in spaghetti code it's difficult to follow the program execution path due to its poor or inconsistent structure.

Spaghetti sketch

The code might function correctly, but remains difficult to debug when problems arise, challenging to modify without introducing new bugs, nearly impossible for new developers to understand, and resistant to extension or improvement.

Structured programming was developed specifically to combat these issues by enforcing clear, predictable control flow.

Blocks in Cā€‹

A block in C is a group of statements enclosed within curly braces {}. Blocks define a scope and can contain variable definitions, expressions, and other control structures.

Syntax:

{
statement1;
statement2;
// more statements...
}

Blocks are used in control structures to group multiple statements and treat them as a single unit.

Blocks and Variable Scopeā€‹

Variables defined within a block:

  1. Are only accessible within that block (their scope)
  2. Are destroyed when execution leaves the block (their lifetime)

For this reason, such variables are called local to the block.

block_scope.c
#include <stdio.h>

int main() {
int a = 3;
{
int b = 2;
// Both a and b are accessible
printf("Inside inner block: a = %d, b = %d\n", a, b);
}
// b is no longer accessible here
printf("Outside inner block: a = %d\n", a);

return 0;
}
Inside inner block: a = 3, b = 2
Outside inner block: a = 3

Variable Shadowingā€‹

Variable shadowing occurs when a local variable in an inner scope has the same name as a variable in an outer scope.

When this happens, the local variable in the inner scope temporarily obscures or "shadows" the variable from the outer scope. Within the inner scope, any reference to the variable name refers to the inner variable, not the outer one. The outer variable continues to exist unmodified and becomes accessible again once execution leaves the inner scope.

This concept directly relates to the declaration and definition principles covered in the Variables and Types chapter. When shadowing occurs, both the outer and inner variables are distinct entities with separate memory allocations.

Each variable has its own definition and occupies its own memory space. Unlike when using extern declarations, these are completely independent variables that merely share a common identifier.

Let's take a look at this example to see what happens:

variable_shadowing.c
#include <stdio.h>

int main() {
int a = 3; // Outer 'a' is defined here
{
int a = 2; // Inner 'a' is a completely separate variable
printf("Inner a = %d\n", a); // Refers to inner 'a' (2)
}
printf("Outer a = %d\n", a); // Refers to outer 'a' (3)

return 0;
}
Inner a = 2
Outer a = 3

Conditional Statementsā€‹

If-Else Statementā€‹

The if-else statement executes different code blocks based on a condition.

Syntax:

if (expression) {
// Code executed when expression is true
} else {
// Code executed when expression is false (optional)
}

The expression is interpreted as a logical value: 0 means false, any non-zero value means true.

Example:

if_else_example.c
#include <stdio.h>

int main() {
int a = 5, b = 3, max;

if (a > b) {
max = a;
} else {
max = b;
}

printf("The maximum value is %d\n", max);
return 0;
}
The maximum value is 5

Nested If Statementsā€‹

If statements can be nested inside each other:

if (y != 0) {
if (x > y) {
z = x / y;
} else {
z = y / x;
}
}
caution

In nested if statements, each else is associated with the closest if that doesn't already have an else. Using proper indentation makes these relationships clearer.

Example:

nested_if_example.c
#include <stdio.h>

int main() {
int score = 85;
int attendance = 1; // 1 for sufficient, 0 for insufficient

if (score >= 60) {
if (attendance) {
printf("Result: Pass with score %d", score);
} else {
printf("Result: Incomplete - attendance requirement not met");
}
} else {
if (attendance) {
printf("Result: Fail - eligible for retest");
} else {
printf("Result: Fail - must repeat course");
}
}

return 0;
}
Result: Pass with score 85

Switch Statementā€‹

The switch statement executes different code blocks based on the value of an expression.

Syntax:

switch (expression) {
case constant1:
// Code for constant1
break;
case constant2:
// Code for constant2
break;
// More cases...
default:
// Code when no case matches
}

Example:

switch_example.c
#include <stdio.h>

int main() {
int day = 3;

switch (day) {
case 1:
printf("Monday");
break;
case 2:
printf("Tuesday");
break;
case 3:
printf("Wednesday");
break;
case 4:
printf("Thursday");
break;
case 5:
printf("Friday");
break;
default:
printf("Weekend");
}

return 0;
}
Wednesday
danger

Without the break statement, execution "falls through" to the next case. This means that after a matching case is found, all subsequent cases are executed until a break is encountered or the switch statement ends.

Example of using fall-through behavior intentionally:

month_days.c
switch (month) {
case 2:
if (isLeapYear) {
days = 29;
} else {
days = 28;
}
break;
case 4:
case 6:
case 9:
case 11:
days = 30;
break;
default:
days = 31;
}

Iteration Statements (Loops)ā€‹

Loop

Loops enable repetitive execution of code blocks. C provides three types of loops:

  1. while loops
  2. do-while loops
  3. for loops

While Loopā€‹

The while loop executes a block of code repeatedly as long as a condition is true.

Syntax:

while (condition) {
// code to execute
}

The condition is evaluated before each iteration. If the condition is false initially, the loop body is never executed.

While loop flowchart

Fig.1. Flowchart of the while loop.

Example:

multiplication_as_sum.c
#include <stdio.h>

int main() {
int a = 4, b = 3;
int result = 0;

while (b > 0) {
result += a;
b--;
}

printf("%d\n", result); // Computes a * b as a sum
return 0;
}
12

Infinite Loops with Whileā€‹

An infinite loop is a loop that never terminates. With a while loop, this happens when the condition is always true:

infinite_while.c
while (1) {
// This code will run forever
printf("This is an infinite loop\n");
}
caution

Make sure the loop condition eventually becomes false, or you'll create an infinite loop. For example:

while (b > 0) {
result += a;
// ā€¼ļø We forgot to update b
}

This would run forever since b never changes.

Do-While Loopā€‹

The do-while loop is similar to the while loop, but the condition is evaluated after each iteration. This ensures that the loop body is executed at least once.

Do-while loop flowchart

Fig.2. Flowchart of the do-while loop.

Syntax:

do {
// code to execute
} while (condition);

Example:

sum_until_negative.c
#include <stdio.h>

int main() {
int num, sum = 0;

do {
printf("Enter a number (negative to stop): ");
scanf("%d", &num);

if (num >= 0) {
sum += num;
}
} while (num >= 0);

printf("Sum of all positive numbers: %d\n", sum);
return 0;
}

For this example, if the user enters: 5, 10, 15, -1, the output would be:

Sum of all positive numbers: 30

For Loopā€‹

The for loop provides a compact way to write loops when the number of iterations is known in advance or when there's a clear initialization, condition, and update pattern.

Syntax:

for (initialization; condition; update) {
// code to execute
}
  • The initialization is executed once at the beginning
  • The condition is checked before each iteration
  • The update is executed after each iteration

Why For Loops Were Introducedā€‹

For loops were introduced to address common issues with while loops, and have many good features:

  1. Improved Readability: all loop control elements (initialization, condition, update) are in one place
  2. Reduced Errors: helps prevent forgetting to update counter variables
  3. Scope Control: variables declared in the initialization part are local to the loop (C99 and later)
  4. Standardized Pattern: provides a consistent structure for the common pattern of "initialize, check, execute, update"

Example:

sum_numbers.c
#include <stdio.h>

int main() {
int sum = 0;

for (int i = 1; i <= 10; i++) {
sum += i;
}

printf("Sum of numbers 1 to 10: %d\n", sum);
return 0;
}
Sum of numbers 1 to 10: 55

Infinite Loops with Forā€‹

The for loop can create an infinite loop by omitting the condition part:

infinite_for.c
for (;;) {
printf("This is an infinite loop\n");
// This loop runs forever
}

Comparing infinite loops with for and while:

// Infinite loop with while
while (1) {
// Loop body
}

// Equivalent infinite loop with for
for (;;) {
// Loop body
}

Both achieve the same result, but the for (;;) version is slightly more concise and is recognized by experienced C programmers as an intentional infinite loop.

Evolution of a For Loopā€‹

Let's see how a while loop can be transformed into a for loop step-by-step, using factorial calculation as an example:

  1. Original while loop:
factorial_while.c
int n = 6, i = 2;
int y = 1;
while (i <= n) {
y *= i++;
}
  1. Basic for loop equivalent:
factorial_for1.c
int n = 6, i;
int y = 1;
for (i = 2; i <= n; i++) {
y *= i;
}
  1. With variable declaration in the for statement (C99 and later):
factorial_for2.c
int n = 6;
int y = 1;
for (int i = 2; i <= n; i++) {
y *= i;
}
note

Variables declared in the for loop initialization are local to the loop's scope and are not accessible after the loop ends.

Special Cases of For Loopsā€‹

A for loop can also have an empty body (just a semicolon): all the necessary operations are performed in the loop control expressions themselves:

compact_factorial.c
int factorial = 1;
for (int n = 5; n > 0; factorial *= n--); // Computes 5!

However, this code can be harder to read and understand, so it should be used sparingly.

Special Control Flow Statementsā€‹

Using Break and Continueā€‹

The break statement terminates the innermost loop or switch statement:

break_example.c
for (int i = 0; i < 100; i++) {
if (i * i > 500) {
break; // Exit when iĀ² exceeds 500
}
printf("%d ", i);
}

The continue statement skips the rest of the current iteration and proceeds to the next iteration:

continue_example.c
for (int i = 0; i < 10; i++) {
if (i % 3 == 0) {
continue; // Skip multiples of 3
}
printf("%d ", i);
}
1 2 4 5 7 8
caution

While break and continue can be useful, overusing them can make code harder to follow. In structured programming, their use should be minimized in favor of clearer conditional logic.

The goto Statementā€‹

The goto statement provides an unconditional jump from one point in a program to another. It's generally advised to avoid goto as it can lead to spaghetti code:

goto_example.c
#include <stdio.h>

int main() {
int i = 0;

start: // This is a label
printf("%d ", i);
i++;

if (i < 10) {
goto start; // Jump back to the label
}

printf("\nDone!\n");
return 0;
}
0 1 2 3 4 5 6 7 8 9
Done!
caution

The goto statement is often considered "harmful" because:

  1. It creates complex, non-linear flow that's hard to understand
  2. It makes code difficult to debug and maintain
  3. It can lead to spaghetti code with unpredictable behavior
  4. All control flow needs can already be better handled with the structured programming constructs (if-else, loops)

There are very few legitimate uses for goto in modern C programming. One rare acceptable use is for error handling with multiple cleanup steps:

#include <stdio.h>
#include <stdlib.h>

int main() {
int *arr1 = NULL;
int *arr2 = NULL;

arr1 = malloc(100 * sizeof(int));
if (arr1 == NULL) {
goto cleanup;
}

arr2 = malloc(200 * sizeof(int));
if (arr2 == NULL) {
goto cleanup;
}

// Do something with arr1 and arr2

cleanup:
if (arr1 != NULL) {
free(arr1);
}
if (arr2 != NULL) {
free(arr2);
}
return 0;
}

Even in this case, many programmers prefer to use other techniques that don't require goto.

Best Practices and Common Pitfallsā€‹

Equality Operator vs. Assignment Operatorā€‹

Warning

One of the most common bugs in C is using the assignment operator = when the equality operator == was intended:

if (x = 5) {  // āŒ This assigns 5 to x and then evaluates to true
// ...
}

if (x == 5) { // āœ… This checks if x equals 5
// ...
}

A possible solution: when comparing variables with constants, if you put the constant on the left side the program will not compile unless you use the right operator:

if (5 == x)  // If you accidentally type = instead of == you'll get a compiler error

Fun Fact: this problem is so common that some modern languages like Python use := (known as the "walrus operator") for assignment within expressions to avoid confusion with the equality operator ==.

Overflow and Underflow in Loopsā€‹

Be careful about the data type of your loop variables to avoid overflow/underflow:

overflow_underflow.c
// Potential infinite loop due to overflow
char i;
for (i = 0; i < 200; i++) {
// When i reaches 127, i++ overflows to -128
// which is still less than 200, so the loop continues forever
}

// Potential infinite loop due to underflow
unsigned int j;
for (j = 10; j >= 0; j--) {
// When j reaches 0, j-- underflows to a very large number
// which is still >= 0, so the loop continues forever
}