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:
- Simple instructions (the ones we've seen so far)
- 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:
- Sequence - Instructions executed one after another in order
- Selection - Choosing between different code blocks based on conditions (if-else, switch)
- 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.
"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.
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:
- Are only accessible within that block (their scope)
- Are destroyed when execution leaves the block (their lifetime)
For this reason, such variables are called local to the block.
#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:
#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:
#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;
}
}
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:
#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:
#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
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:
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)ā
Loops enable repetitive execution of code blocks. C provides three types of loops:
while
loopsdo-while
loopsfor
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.
Example:
#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:
while (1) {
// This code will run forever
printf("This is an infinite loop\n");
}
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.
Syntax:
do {
// code to execute
} while (condition);
Example:
#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:
- Improved Readability: all loop control elements (initialization, condition, update) are in one place
- Reduced Errors: helps prevent forgetting to update counter variables
- Scope Control: variables declared in the initialization part are local to the loop (C99 and later)
- Standardized Pattern: provides a consistent structure for the common pattern of "initialize, check, execute, update"
Example:
#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:
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:
- Original while loop:
int n = 6, i = 2;
int y = 1;
while (i <= n) {
y *= i++;
}
- Basic for loop equivalent:
int n = 6, i;
int y = 1;
for (i = 2; i <= n; i++) {
y *= i;
}
- With variable declaration in the for statement (C99 and later):
int n = 6;
int y = 1;
for (int i = 2; i <= n; i++) {
y *= i;
}
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:
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:
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:
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
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:
#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!
The goto
statement is often considered "harmful" because:
- It creates complex, non-linear flow that's hard to understand
- It makes code difficult to debug and maintain
- It can lead to spaghetti code with unpredictable behavior
- 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ā
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:
// 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
}