Programming in C

Chapter 3
Decision Making and Iterative Control

Bachelors Program in Electronics and Communication
Programming in C   •   Post Graduate

Decision Making and Iterative Control

Introduction

A C program is useful not merely because it stores data and performs arithmetic, but because it can select actions and repeat operations according to changing conditions. Decision-making constructs determine which statements execute; iterative-control constructs determine how often a statement or block executes. Together they provide the control-flow foundation of algorithms used in embedded systems, communication software, operating systems, signal-processing utilities, and scientific applications.

In C, control flow is built from expressions, statements, blocks, and well-defined rules for evaluating conditions. A condition is not a separate Boolean data type in traditional C: an expression evaluates to zero (false) or a nonzero value (true). The header <stdbool.h> provides bool, true, and false as convenient abstractions, but the underlying rule remains the same.

The quality of control flow is determined not only by whether a program produces the expected output, but also by its readability, termination properties, numerical robustness, portability, and resistance to invalid input. At postgraduate level, control structures should therefore be studied as implementation mechanisms as well as representations of algorithmic decisions.

Learning Objectives

By the end of this chapter, a learner should be able to:

  • distinguish expressions, statements, blocks, and control-flow paths in C;
  • construct reliable if, if...else, nested, chained, and switch decisions;
  • explain short-circuit evaluation and its role in safe expressions;
  • select appropriate for, while, and do...while loops;
  • analyse initialization, condition testing, update, termination, and loop invariants;
  • use break, continue, and carefully controlled goto statements;
  • identify common defects such as accidental assignment, off-by-one errors, overflow, and infinite loops;
  • design and evaluate a practical C solution involving decisions and iteration.

1. Control Flow as an Algorithmic Model

A sequential program executes statements in source order. A decision introduces alternative paths, while a loop introduces a path that may be traversed repeatedly. If a control-flow graph is used, statements can be represented as nodes and possible transfers as directed edges. A decision node has multiple outgoing edges; a loop contains a back edge to an earlier node.

Control flow with decision and iteration Start / initialize Condition? true / false true Execute branch false Skip / other branch A loop may return to the condition after an update
Control flow diagram showing a decision and a loop
Figure: Control flow diagram showing a decision and a loop

Figure: A simplified control-flow model combining selection and iteration.

A reliable control structure should make four properties clear: the initial state, the condition for continuation or selection, the state-changing action, and the eventual termination or exit path. These properties are especially important in real-time and embedded programs, where an unintended infinite loop can prevent a system from servicing sensors, communication channels, or safety functions.

2. Expressions, Relational Operators, and Logical Operators

Relational operators compare values: <, <=, >, >=, ==, and !=. Their result is zero or nonzero. Equality must be written ==; writing = performs assignment and may silently alter program state.

Logical operators are:

OperatorMeaningEvaluation property
!elogical NOTtrue if e is zero
e1 && e2logical ANDe2 is evaluated only if e1 is nonzero
`e1e2`

The && and || operators sequence evaluation from left to right and introduce short-circuit behaviour. This is valuable when the first operand protects the second:

if (index >= 0 && index < length && buffer[index] == target) {
    /* safe access when length and index are valid */
}

Short-circuiting is not interchangeable with bitwise & and |. Bitwise operators operate on individual bits and evaluate both operands. They are appropriate for masks and hardware registers, not as replacements for logical tests.

The conditional operator ?: is an expression, not a statement:

maximum = (a > b) ? a : b;

It is useful for a simple value selection, but nested conditional operators can reduce readability. A full if...else is preferable when branches perform multiple operations or have important side effects.

Floating-point equality deserves special care. Due to rounding, a computation that is mathematically equal may not have an identical binary representation. A tolerance test can be used when its scale is justified:

#include <math.h>

if (fabs(x - y) <= tolerance) {
    /* x and y are considered sufficiently close */
}

The tolerance is application-dependent; it is not a universal constant. For measurements, it should relate to sensor resolution, numerical conditioning, and required accuracy.

3. Selection Constructs

3.1 The if statement

The basic form is:

if (condition) {
    statement_block;
}

The block executes exactly when condition is nonzero. Braces are recommended even for one statement because they prevent maintenance errors when a block later expands.

if (temperature > limit) {
    raise_alarm();
}

3.2 Two-way and chained selection

if (status == 0) {
    report_ready();
} else {
    report_fault();
}

For mutually exclusive ranges, a chained form is usually clearer than several independent if statements:

if (score >= 90) {
    grade = 'A';
} else if (score >= 75) {
    grade = 'B';
} else if (score >= 60) {
    grade = 'C';
} else {
    grade = 'F';
}

The tests are evaluated from top to bottom, and only the first matching branch executes. Conditions should be ordered so that their ranges are complete and non-overlapping. Boundary values must be decided explicitly: >= 75 and < 75 have different meanings from > 75 and <= 75.

3.3 Nested decisions

Nested decisions represent a decision whose applicability depends on another decision:

if (packet_valid) {
    if (packet_length <= MAX_PACKET) {
        process_packet(packet);
    } else {
        reject_oversized_packet();
    }
} else {
    reject_corrupt_packet();
}

Deep nesting increases cognitive complexity. Guard clauses can reduce it by rejecting invalid cases early:

if (!packet_valid) {
    return ERROR_CORRUPT;
}
if (packet_length > MAX_PACKET) {
    return ERROR_TOO_LARGE;
}
process_packet(packet);
return SUCCESS;

3.4 The switch statement

switch is appropriate when one integral or enumeration expression is compared with discrete constant cases:

switch (mode) {
case MODE_IDLE:
    enter_idle();
    break;
case MODE_RUN:
    enter_run();
    break;
case MODE_ERROR:
    enter_error();
    break;
default:
    enter_safe_state();
    break;
}

Case labels must be integer constant expressions and must be unique within the statement. Execution falls through from one case to the next unless break, return, or another transfer statement intervenes. Fall-through can be intentional, but it should be documented. The default case is important when input may be outside the expected enumeration.

A switch is often clearer than a long equality chain, particularly for protocol states or finite-state machines. It is not a replacement for range tests such as value >= lower && value <= upper.

4. Iterative Control

Iteration repeats a statement or block while a condition remains satisfied. Three questions define a loop:

  1. What is initialized before the first test?
  2. What condition permits another iteration?
  3. What changes so that termination eventually becomes possible?

4.1 while loop

The syntax is:

while (condition) {
    body;
}

The condition is tested before every iteration, so the body may execute zero times. It is appropriate when the number of repetitions is not known in advance:

while (read_sensor(&sample) == SENSOR_OK) {
    filter_sample(sample);
}

4.2 do...while loop

do {
    body;
} while (condition);

The body executes at least once. The trailing semicolon is part of the syntax. This form is useful for menus and input validation, provided the body can make progress toward a valid or terminating state.

do {
    printf("Enter a value from 1 to 10: ");
    scanf("%d", &value);
} while (value < 1 || value > 10);

This simple example has an input-conversion weakness: if scanf fails, the invalid input may remain in the input stream and the loop may repeat indefinitely. Production code should check the return value and consume or replace invalid input.

4.3 for loop

The general form is:

for (initialization; condition; update) {
    body;
}

The execution order is initialization once, condition before each iteration, body, update, and then the next condition test. A counted loop is therefore commonly written:

for (size_t i = 0; i < length; ++i) {
    sum += samples[i];
}

The index values are 0 through length - 1 when length > 0. This is the standard half-open interval [0, length), which simplifies array traversal and composition of subranges.

C permits declarations in the initialization clause when the selected language standard supports them:

for (int i = 0; i < 10; ++i) {
    /* i is scoped to the loop */
}

The three clauses can be omitted, but an omitted condition creates an intentional infinite loop:

for (;;) {
    service_peripherals();
    if (shutdown_requested()) {
        break;
    }
}

In a real-time system, an infinite service loop may be correct, but it must still provide watchdog servicing, fault handling, and a defined shutdown or recovery strategy where required.

5. Loop Correctness and Analysis

A loop can be analysed using a loop invariant: a property that is true before the loop starts and remains true after every completed iteration. For summing an array, consider:

int sum = 0;
for (size_t i = 0; i < n; ++i) {
    sum += a[i];
}

A useful invariant is: before each iteration, sum equals the sum of elements a[0] through a[i-1]. Initially, i is zero and the range is empty, so the invariant holds. Each iteration adds a[i] and increments i, preserving it. When the loop ends, i == n, so sum is the sum of all elements.

This reasoning assumes that a points to at least n valid elements, n is within the representable range of size_t, and the additions do not overflow the chosen integer type. In C, signed integer overflow is undefined behaviour; unsigned arithmetic is modulo arithmetic but may still be semantically wrong. A robust implementation should select a sufficiently wide type or check before addition.

A terminating loop also needs a variant: a quantity that moves toward a termination condition. For for (i = 0; i < n; ++i), the distance n - i decreases under the assumptions that i increases and n is fixed. If the update is omitted or the index is modified incorrectly, termination may fail.

Nested loops require separate analysis of each index. If an outer loop executes m times and an inner loop executes n times for each outer iteration, the body executes approximately m × n times. This is a time-complexity observation, not a guarantee of exact machine time; memory access, branches, compiler optimization, and hardware also matter.

6. Loop Control Transfers

break terminates the nearest enclosing loop or switch statement. continue skips the remainder of the current loop body and begins the next iteration; in a for loop, the update clause executes before the next condition test.

for (size_t i = 0; i < count; ++i) {
    if (!valid(samples[i])) {
        continue;
    }
    process(samples[i]);
}

A break is suitable for an exceptional or discovered termination condition, but excessive use can obscure the loop’s normal logic. A flag, a function decomposition, or a clearer condition may be preferable.

goto transfers control to a label within the same function. It is generally discouraged for ordinary algorithmic flow, but it can simplify cleanup when several operations may fail:

resource = acquire();
if (resource == NULL) goto cleanup;
buffer = allocate();
if (buffer == NULL) goto cleanup;
/* normal work */
cleanup:
free(buffer);
release(resource);

This pattern is acceptable when labels follow a structured cleanup order and the function remains easy to audit. It should not be used to jump into the scope of an uninitialized object or to create tangled control flow.

7. Common Errors and Defensive Practices

Assignment in a condition. if (x = 1) assigns one and tests true. Compile with warnings enabled, for example -Wall -Wextra -Wpedantic, and use if (1 == x) only if it fits the project’s style; it does not replace proper review.

Dangling else. An else associates with the nearest unmatched if. Braces remove ambiguity.

Off-by-one errors. The conditions i <= n and i < n differ by one iteration. For an array of n elements, valid indices are 0 to n - 1, so i < n is normally correct.

Unsigned reverse loops. This loop is defective because i cannot become negative:

for (size_t i = n - 1; i >= 0; --i) { /* defective */ }

A safe reverse pattern uses a condition based on zero before decrementing:

for (size_t i = n; i > 0; --i) {
    process(a[i - 1]);
}

Unchanged loop state. If no variable affecting the condition changes, the loop may not terminate.

Overflow and conversion. Expressions may be evaluated in a type wider or narrower than expected, and signed overflow is undefined. Use explicit, appropriate types and validate bounds.

Input failure. Always check conversion functions. For robust text input, fgets followed by validated conversion is often preferable to unchecked scanf use.

Side effects in conditions. Expressions such as while (buffer[i++] != '\0') can be correct, but they combine testing and state change. They require careful review and may be less maintainable than an explicit update.

8. Worked Example: Robust Threshold Classification

Suppose a communication receiver collects N nonnegative signal-quality samples and must classify each as GOOD, MARGINAL, or POOR. Let the thresholds be T_good = 70 and T_marginal = 40, with the policy:

  • quality ≥ 70: good;
  • 40 ≤ quality < 70: marginal;
  • quality < 40: poor.

The following function counts categories. It assumes quality points to n valid integer samples and that the output pointers are valid.

#include <stddef.h>

void classify(const int quality[], size_t n,
              size_t *good, size_t *marginal, size_t *poor)
{
    *good = *marginal = *poor = 0;

    for (size_t i = 0; i < n; ++i) {
        if (quality[i] >= 70) {
            ++*good;
        } else if (quality[i] >= 40) {
            ++*marginal;
        } else {
            ++*poor;
        }
    }
}

For samples 75, 39, 40, 69, 70, the counts are good = 2 (75, 70), marginal = 2 (40, 69), and poor = 1 (39). The chained structure is correct because the first branch removes all values at least 70; the second therefore covers 40 through 69.

The function’s complexity is O(n) time and O(1) additional space, excluding the input and output storage. Its boundary behaviour is explicit and testable. A production version might additionally validate pointers, reject physically impossible sensor values, and record an error status rather than silently classifying corrupt data.

9. Case Study: Embedded Temperature Protection

Background

The following is an illustrative case. A battery-management controller samples temperature periodically. It must control a cooling fan, issue an over-temperature alarm, and enter a protective state if the sensor is invalid.

Problem

The controller has three operational states:

  • NORMAL: temperature below 45 °C;
  • COOLING: temperature from 45 °C up to but not including 60 °C;
  • PROTECT: temperature at least 60 °C or sensor failure.

A naive implementation may repeatedly read the sensor, use floating-point equality, or omit a recovery policy. Such defects can cause oscillation, unsafe operation, or failure to respond.

Analysis and approach

The state machine should make state transitions explicit. Sensor validity must be checked before numerical comparison. Hysteresis is useful to prevent rapid switching: for example, cooling may remain active until temperature falls below 43 °C. The exact values are engineering requirements, not universal programming constants.

typedef enum {
    NORMAL,
    COOLING,
    PROTECT
} State;

State update_state(State current, int temperature, int sensor_ok)
{
    if (!sensor_ok) {
        return PROTECT;
    }

    switch (current) {
    case NORMAL:
        return (temperature >= 45) ? COOLING : NORMAL;
    case COOLING:
        if (temperature >= 60) return PROTECT;
        return (temperature < 43) ? NORMAL : COOLING;
    case PROTECT:
        /* Recovery should require an explicit safety policy. */
        return PROTECT;
    default:
        return PROTECT;
    }
}

A service loop can call update_state at a fixed sampling event rather than use an uncontrolled delay loop:

for (;;) {
    int temperature;
    int ok = read_temperature(&temperature);
    state = update_state(state, temperature, ok);
    apply_outputs(state);
    wait_for_next_sample();
}

Outcome and learning points

The switch expresses discrete states, the if statements express thresholds, and the infinite for loop represents a continuously operating controller. The default branch fails safely. Hysteresis prevents unnecessary toggling. However, software structure alone cannot establish safety: sensor redundancy, watchdog design, fault analysis, timing validation, and applicable functional-safety standards must also be considered.

10. Advanced Design and Professional Practice

Control flow influences testing. Branch coverage asks whether each decision outcome has executed; condition coverage examines individual Boolean components; modified condition/decision coverage is used in some safety-critical development contexts. Coverage does not prove correctness, but it reveals untested paths.

Finite-state machines are a powerful abstraction for communication protocols and embedded controllers. Each state defines permitted inputs and actions, while transitions define the next state. A switch implementation is direct and efficient, but a table-driven design may be easier to extend when states and events grow large.

Compiler diagnostics, static analysis, sanitizers, unit tests, property-based tests, and code review should complement one another. Tests should include zero-length input, minimum and maximum values, threshold boundaries, invalid states, sensor failure, and termination conditions. Coding standards may restrict implicit conversions, mandatory braces, unrestricted goto, or unbounded loops.

AI-assisted code generation can suggest loop structures, but it may introduce incorrect bounds, unchecked input, or unsafe assumptions. Generated control logic must be compiled with strict warnings, reviewed against requirements, tested at boundaries, and examined for undefined behaviour. In regulated or safety-critical contexts, traceability and human accountability remain essential.

Challenges and Limitations

Control structures cannot compensate for ambiguous requirements, poor data validation, incorrect timing assumptions, or inadequate hardware design. Thresholds may be sensitive to noise; integer limits may be exceeded; floating-point calculations may be unstable; and a loop that is logically terminating may violate a real-time deadline. Concurrency introduces additional concerns: a condition may change asynchronously, requiring synchronization or atomic access rather than an ordinary variable read. These issues must be addressed at system level, not hidden inside syntactic constructs.

Chapter Summary

Decision-making constructs select among alternative paths. In C, zero represents false and nonzero represents true; relational, logical, and conditional operators support expression-level decisions. if structures are suitable for general Boolean conditions, while switch is effective for discrete integral or enumeration values. Braces, complete boundary analysis, and a default case improve reliability.

while, do...while, and for loops differ mainly in when they test conditions and how naturally they express unknown, at-least-once, and counted repetition. Correct loops require initialization, a meaningful continuation condition, progress toward termination, and valid memory and arithmetic assumptions. Loop invariants and variants provide a formal basis for reasoning about correctness.

break, continue, and limited, disciplined goto use can support clear control flow, but uncontrolled transfers reduce maintainability. Common defects include assignment in conditions, off-by-one errors, unsigned reverse-loop errors, unchecked input, overflow, and infinite loops. Professional implementations combine clear structure with compiler warnings, static analysis, testing, defensive validation, and system-level safety analysis.

Key Terms

Control flow: The order in which program statements execute.

Condition: An expression whose zero or nonzero value selects behaviour.

Selection: Choosing one execution path from alternatives.

Iteration: Repeated execution of a statement or block.

Short-circuit evaluation: Evaluation in which && or || may omit the second operand.

Relational operator: An operator that compares two values.

Logical operator: An operator that combines or negates truth values.

Conditional operator: The three-operand ?: expression.

Fall-through: Continuing into the next switch case without a transfer statement.

Loop invariant: A property preserved across loop iterations.

Loop variant: A quantity used to reason about progress toward termination.

Off-by-one error: A boundary error causing one excessive or missing iteration.

Infinite loop: A loop with no reachable terminating condition or exit.

Guard clause: An early return or rejection for an invalid or exceptional case.

Finite-state machine: A model consisting of states and input-driven transitions.

Hysteresis: Different activation and deactivation thresholds used to prevent oscillation.

Undefined behaviour: C behaviour for which the standard imposes no requirements.

Branch coverage: A measure of whether decision outcomes have been executed in tests.

Review Questions

Short Answer Questions

  1. How does C represent true and false values in a condition?
  2. What is the difference between == and =?
  3. When is a do...while loop preferable to a while loop?
  4. Why is break normally required in a switch case?
  5. Define a loop invariant.

Long Answer Questions

  1. Explain the syntax, execution order, and appropriate uses of for, while, and do...while loops.
  2. Discuss short-circuit evaluation and show how it can protect an array access or division operation.
  3. Compare chained if...else statements with switch statements, including their limitations.
  4. Explain common causes of nontermination and off-by-one errors, with corrected C examples.
  5. Describe how finite-state-machine design improves an embedded controller implemented with C control structures.

Analytical / Application Questions

  1. Design a C function that scans an integer array and reports the first index containing a value outside a specified inclusive range. State assumptions and analyse its termination.
  2. A protocol receiver has states IDLE, RECEIVING, and ERROR. Develop a transition strategy using switch, including handling of unknown events and invalid states.
  3. Analyse a loop that processes samples until either 100 valid samples are collected or five invalid samples occur. Specify suitable counters, termination conditions, and tests for boundary cases.

Multiple Choice Questions with Answers

Question 1

Which operator performs logical AND with short-circuit evaluation?

A. &

B. &&

C. |

D. ||

Correct Answer: B. &&

Question 2

For an array containing n elements indexed from zero, which condition is normally correct for forward traversal?

A. i <= n

B. i < n

C. i > n

D. i == n

Correct Answer: B. i < n

Question 3

Which statement about a switch case is correct?

A. Case labels may contain arbitrary floating-point expressions

B. Every case automatically terminates with break

C. Case labels must be unique integral constant expressions

D. A switch directly tests numerical ranges

Correct Answer: C. Case labels must be unique integral constant expressions

Question 4

Which loop always executes its body at least once?

A. while

B. for

C. do...while

D. switch

Correct Answer: C. do...while

Question 5

What is the result of the expression 5 && 0 in C?

A. 0

B. 1

C. 5

D. Undefined in all implementations

Correct Answer: A. 0

Question 6

Which defect occurs in if (x = 10) when the programmer intended a comparison?

A. Integer underflow

B. Assignment instead of equality testing

C. Short-circuit failure

D. Unreachable fall-through

Correct Answer: B. Assignment instead of equality testing

Question 7

In a for loop, after the loop body completes, which component is evaluated before the next condition test?

A. Initialization

B. Case label

C. Update expression

D. Function prototype

Correct Answer: C. Update expression

Question 8

Why is for (size_t i = n - 1; i >= 0; --i) defective as a reverse loop?

A. size_t is always floating point

B. The condition is always false initially

C. An unsigned index cannot become less than zero

D. The decrement operator cannot be used in for loops

Correct Answer: C. An unsigned index cannot become less than zero

Question 9

What is the principal purpose of a loop invariant?

A. To increase processor clock speed

B. To prove or reason about a property preserved during iteration

C. To force every loop to execute once

D. To replace all testing

Correct Answer: B. To prove or reason about a property preserved during iteration

Question 10

In the illustrative temperature controller, why are different activation and deactivation thresholds useful?

A. They eliminate the need for sensor validation

B. They prevent rapid switching near a threshold

C. They guarantee hardware safety independently of software

D. They convert integers into floating-point values

Correct Answer: B. They prevent rapid switching near a threshold

Programming in C
Chapter 3 of 3