Recommended resources
The freely available resources this lesson will direct you to.
- Function Basics | C Programming Tutorialwww.youtube.com
- C_27 If Statement in C | C Programming Tutorialswww.youtube.com
- Order of evaluation - cppreference.comen.cppreference.com
- EXP10-C. Do not depend on the order of evaluation of subexpressions or the order in which side effects take place | CERT Secure Codingwiki.sei.cmu.edu
Hello! Last lesson established how to evaluate a C expression: determine its type and value, check whether short-circuiting skips an operand, and distinguish precedence from execution order. Now those expression results will control which statements run and how often.
This lesson develops a reliable method for tracing conditionals, loops, and function calls, including changes to variables along the way. The goal for GATE-style output questions is to produce a justified trace—not merely a plausible-looking final number.
Conditionals: evaluate, select, then continue
An if statement evaluates its condition. If the result is nonzero, its body executes; if it is zero, the body is skipped. In an if–else if–else chain, conditions are checked in order until one succeeds. Once a branch is selected, the remaining conditions and branches in that chain are skipped.
C_27 If Statement in C | C Programming Tutorials
Watch “C_27 If Statement in C” from Jenny’s Lectures CS IT for its concrete traces of conditional execution and two common syntax traps.
Watch the basic if to see how zero and nonzero conditions affect the following statements. Then watch braces and bodies: pay attention to what remains unconditional when braces are absent. Finally, assignment conditions shows why = and == produce different traces.
The three panels below show where execution goes for a positive number, zero, and a negative number. Notice that code after the entire conditional chain runs in all three cases, provided no earlier statement exits the function.

A condition can also change the state you are tracing:
int x = 7;
if (x = 0) {
x = 9;
} else {
x = 3;
}
x = 0 is an assignment expression. It stores 0 in x and has the value 0, so the else branch runs. The final value is 3. By contrast, if (x == 0) would compare the original 7 with zero, leave x unchanged during the test, and still select the else branch. Both versions reach the same final value here, but for different reasons.
Identify the statement that belongs to the condition. Without braces, only the next statement is the if body:
if (x > 0)
x++;
x += 10; /* Runs regardless of the condition. */
A semicolon can itself be that next statement. In if (x > 0);, the conditional body is empty; a subsequent block is not made conditional by its indentation. Similarly, an else associates with the nearest preceding unmatched if. Braces make the intended structure explicit and make tracing less error-prone.
A switch is another conditional selection mechanism. Its controlling integer expression is evaluated once; execution begins at the matching case, or at default if no case matches. Labels do not stop execution:
int code = 1;
switch (code) {
case 1:
printf("A");
/* Fall through. */
case 2:
printf("B");
break;
default:
printf("D");
}
This prints AB. With code = 2, it prints B; with a value matching neither case, it prints D. The break exits the switch, not the enclosing function or an enclosing loop.
Loops: the failed test is part of the trace
To trace a loop, record not only its successful iterations but also the condition check that ends it. That last check can have a side effect.
The three common forms place the test differently:
while (condition)tests before each possible iteration; its body may run zero times.do { ... } while (condition);tests after each iteration; its body runs at least once.for (initialization; condition; update)performs initialization once, then tests before each iteration. After a completed body, it performs the update before testing again.
For example:
int i = 0;
while (i++ < 3) {
printf("%d ", i);
}
printf("final=%d\n", i);
Post-increment supplies the old value to < 3, then increments i. The trace is:
| Value before test | Value compared with 3 |
i after test |
Body runs? |
|---|---|---|---|
| 0 | 0 | 1 | Yes; prints 1 |
| 1 | 1 | 2 | Yes; prints 2 |
| 2 | 2 | 3 | Yes; prints 3 |
| 3 | 3 | 4 | No |
The output is 1 2 3 final=4. Stopping your trace after the third printed number would miss the increment on the false test.
break exits the innermost enclosing loop or switch. continue skips the rest of the current loop body, but it does not end the loop. In a for loop, execution proceeds to the update expression before the next test; in a while or do loop, it proceeds to the next condition check. This distinction matters if the only increment in a while body appears after a continue: that increment will be skipped, possibly leaving the loop unable to terminate.
For nested loops, keep a separate row or column for each loop counter. A break in the inner loop leaves the outer loop in place; it does not exit both.
Functions: trace the caller and the called function separately
At a function call, evaluate the arguments, give the called function’s parameters their corresponding values, trace its body, and then use its return value in the caller’s expression. For ordinary int parameters, changing a parameter does not change the caller’s variable.
Function Basics | C Programming Tutorial
Watch “Function Basics | C Programming Tutorial” by Portfolio Courses for the mechanics of passing values into a call and bringing a result back.
Watch definition and call, focusing on parameters, the body, and how a returned value is used by the caller. Then watch arguments and parameters to distinguish a caller’s variable from the called function’s separate parameter.
Here is a call that combines a return value with a visible side effect:
int change(int x) {
x += 2;
return x;
}
int main(void) {
int n = 4;
int result = change(n);
printf("%d %d\n", n, result);
return 0;
}
Inside change, the parameter x starts at 4, becomes 6, and is returned. Back in main, result receives 6, but n remains 4. The output is 4 6. Treat the caller’s n and the called function’s x as distinct entries in your trace.
A function may also have a side effect beyond its return value—for example, printing output or modifying a variable it is allowed to access outside the function. Record such effects when the function runs; do not infer them merely from the value it returns. The exact scope and lifetime rules for different kinds of variables are the subject of the next lesson.
Side effects: when C does—and does not—fix the order
The previous lesson warned that precedence determines grouping, not general evaluation order. This becomes especially important when function calls or increments occur within larger expressions. In C, arguments to the same function call need not be evaluated left to right. All argument evaluations finish before the called function’s body begins, but their order relative to one another is generally unspecified.
Order of evaluation - cppreference.com
Read cppreference’s “Order of evaluation” as a reference for deciding whether a proposed trace is guaranteed, merely one possible outcome, or invalid because of undefined behavior.
Before the Contents list, read the opening warning about associativity versus evaluation order. Under “Definitions,” read “Evaluations” from value and side effects, and in “Ordering,” read sequenced-before. Under “Rules,” focus on numbered items 1–4, from calls and boundaries; then items 10–12, particularly assignment and increment and function-call ordering. In “Undefined behavior,” read numbered items 1–2, from conflicting accesses. Focus on the distinction between an unspecified choice of order and an unsequenced conflict.
Consider two calls with an effect on an outside variable:
int g = 0;
int mark(int x) {
g = x;
return x;
}
/* Later, inside a function: */
int sum = mark(1) + mark(2);
sum is 3, but g may end as 1 or 2: C does not specify which call occurs last. This is not permission to choose one final value and declare it the answer. The correct trace records both permitted possibilities. In C11 and later, the two function calls themselves are indeterminately sequenced: either call completes before the other, but their order is not fixed.
Now compare an unsequenced conflict:
int i = 1;
int result = i++ + i;
This has undefined behavior. The modification of i by i++ is unsequenced relative to the other read of i; there is no valid numerical answer to calculate. Likewise, f(i++, i++) has undefined behavior because the two argument expressions modify the same i without a sequencing guarantee. Adding parentheses around either argument would change grouping, not establish an evaluation order.
A useful boundary is the end of a full expression, typically a statement ending in a semicolon or a loop’s controlling expression. Thus, breaking work into statements often makes a trace unambiguous:
int first = i++;
int second = i;
int result = first + second;
With i initially 1, first becomes 1, i becomes 2, second becomes 2, and result becomes 3. This rewritten program is defined; it is not an assumed interpretation of the earlier undefined expression.
An end-to-end tracing method
For a longer snippet, maintain a small state table. At each step, identify the next condition or statement, record its side effects, and only then choose the next control-flow location. Apply that method here:
#include <stdio.h>
int add_two(int x) {
x += 2;
return x;
}
int main(void) {
int total = 0;
int hits = 0;
for (int k = 0; k < 5; ++k) {
if (k == 1)
continue;
if (k == 4)
break;
total += add_two(k);
if (total > 4)
++hits;
}
printf("%d %d\n", total, hits);
return 0;
}
Keep k, total, and hits in the caller’s state. The parameter x exists only while a particular call to add_two is running.
k at test |
Path through the body | total afterward |
hits afterward |
|---|---|---|---|
| 0 | add_two(0) returns 2 |
2 | 0 |
| 1 | continue; the for update still runs |
2 | 0 |
| 2 | add_two(2) returns 4; total exceeds 4 |
6 | 1 |
| 3 | add_two(3) returns 5; total exceeds 4 |
11 | 2 |
| 4 | break; no function call or for update |
11 | 2 |
The program prints 11 2. Notice the distinct reasons for skipped work: continue bypasses the remaining body but preserves the loop’s update-and-test cycle; break exits the loop immediately. Neither skipped iteration calls add_two.
For GATE tracing, a compact checklist is more dependable than mental simulation: parse each expression; track current values; mark each condition’s result; record every executed side effect; and stop if the code has undefined behavior.
Key takeaways
A precise trace follows control flow and changing state. Conditions select branches, loop tests may alter variables even when they fail, and a function’s parameter is not the caller’s variable. break and continue have different effects on the next loop action. Finally, when side effects share an expression, distinguish a guaranteed order, an unspecified choice between valid outcomes, and undefined behavior.
Next, you will examine scope, lifetime, and visibility in more detail, so that every variable in a trace can be identified as well as valued.