CS U111 · Lab 4 · Loops and iterative problem solving
Lab 4, every problem worked
All seven practice problems, all five debugging exercises, all four dry runs, and the calendar challenge — each with the thinking that produced it, the code, and the output it actually printed. Every program on this page was compiled and run before it was published.
From the CS U111 handout: continuous lab evaluations are open book, are worth 10% of the course in total, and are scored best k of n across the whole semester. So one lab evaluation is worth on the order of one to two percent of your CS grade, and a bad one is designed to be droppable. Your calculus quiz tomorrow is worth many times that. Spend today's hours on calculus and give this page 45–60 minutes. That is the correct allocation, not a compromise.
And the part that matters more than any of this: loops in C are new to everyone in the room. No school board teaches while and do-while in C. This is week-4 material that started from zero for the whole batch four weeks ago.
If you do nothing else tonight, do these four things in this order. They are ranked by marks-per-minute.
- 10 min — The digit-extraction pattern. It alone solves Problems 1 and 3, and the sheet's Armstrong and palindrome examples. It is the single most reused six lines in the whole lab.
- 10 min — The four dry runs and the five debugging exercises. These are the cheapest marks on any loops paper, and they are the questions most likely to be asked verbatim.
- 15 min — Problem 1, Problem 4 (do-while), Problem 7 (patterns). Type each one out yourself once. Typing, not reading.
- 10 min — the notes page's pattern cookbook, skimmed. That is the page to have open during the evaluation, since it is open book.
Problems 2, 5, 6 and the calendar challenge are here in full, but they are the ones to read rather than drill if time runs out.
- The one pattern that unlocks half the sheet
- The three questions to ask before writing any loop
- Problem 1 · number classifier · 2 · binary · 3 · digital root · 4 · PIN validator · 5 · smart guesser · 6 · approximating π · 7 · pattern menu
- Debugging exercises 1–5
- Dry runs 1–4
- Challenge · days between two dates
- Bonus problems, brieflyExtra · marks → grades, one line
- If they ask something you've never seen
1 · The one pattern that unlocks half the sheet
Almost every "do something with the digits of a number" problem in this lab is the same six lines. Learn these six lines and Problems 1 and 3 stop being problems:
int temp = n;
while (temp != 0) {
int d = temp % 10; // d = the LAST digit
/* ... do something with d ... */
temp /= 10; // chop that digit off
}
Why it works comes from two facts you already use in arithmetic. % is the remainder after division, so 452 % 10 is 2 — the last digit. And integer division throws away the remainder, so 452 / 10 is 45, not 45.2 — the number with its last digit removed. Repeat, and the number is eaten one digit at a time, right to left, until nothing is left.
Unpack this step — why 452 / 10 is 45 and not 45.2
In C, dividing an int by an int gives an int: the fractional part is discarded (not rounded — chopped). 7/2 is 3, 452/10 is 45, 4/10 is 0. That last one is why the loop ends: after the final digit, temp becomes 0 and the condition temp != 0 turns false.
Trace it once, by hand, for n = 452 — this is exactly the table you should draw in the margin during an exam:
| pass | temp (before) | d = temp % 10 | temp /= 10 (after) |
|---|---|---|---|
| 1 | 452 | 2 | 45 |
| 2 | 45 | 5 | 4 |
| 3 | 4 | 4 | 0 |
| check | 0 | — | condition false → stop |
Three passes, three digits, digits seen in the order 2, 5, 4 — backwards. That backwards order is not a flaw; it is exactly what makes reversing a number free (Problem 1) and exactly what makes printing binary hard (Problem 2, which is why that one needs a different trick).
Feed 0 to this loop and the body never runs even once — temp != 0 is false immediately — so a digit counter reports 0 digits for the number 0. That is wrong in English (0 has one digit) and it is precisely the "zero times" boundary case §19 of the lab sheet tells you to test. If the problem says "positive integer" you are safe; if it doesn't, handle n == 0 separately with an if before the loop. Examiners love this one because it costs nothing to ask.
2 · The three questions to ask before writing any loop
The lab sheet gives you this in §10 and §18, and it is worth more than any individual solution. Before writing a single line, answer these on paper:
| Question | What it decides | Example (Problem 1) |
|---|---|---|
| 1. What is the state? | Which variables change as you go, and what they start at. Every accumulator must be initialised before the loop. | temp = n, digits = 0, sum = 0, reversed = 0 |
| 2. What makes it stop? | The condition. Write it as "keep going while…", not "stop when…". | keep going while temp != 0 |
| 3. How does it advance? | The update. If nothing in the condition changes inside the body, you have written an infinite loop. | temp /= 10 |
Then pick the loop shape from the answer to question 2, which is a mechanical choice:
| If… | use | because |
|---|---|---|
| you know the count in advance ("10 times", "N terms", "rows 1 to n") | for | start, stop and step sit on one line where a reader can check them |
| you stop on a condition, not a count ("until the number becomes 0") | while | the count isn't known when the loop starts |
| the body must run at least once ("ask for input, then check it") | do-while | the test happens after the first pass, so the prompt always appears |
3 · The seven practice problems
Problem 1
Number classifier via loop
Write a program that takes a positive integer from the user. In a single pass (one loop), calculate and print:
- The total number of digits.
- The sum of the digits.
- The reverse of the number.
Sample Execution:
Enter a number: 452
Digits: 3
Sum: 11
Reversed: 254One pass over the digits: count them, sum them, and build the reverse.
temp (the number being eaten), plus three accumulators all starting at 0. Stop: temp != 0. Advance: temp /= 10. Shape: while — the digit count isn't known in advance."In a single pass" is the whole point of the exercise: the temptation is to write three separate loops, one per answer. Resist it — every quantity you want can be updated inside the one loop, because each depends only on the digit currently in your hand.
#include <stdio.h>
int main(void)
{
int n;
printf("Enter a number: ");
scanf("%d", &n);
int digits = 0, sum = 0, reversed = 0;
int temp = n;
while (temp != 0) {
int d = temp % 10; // last digit
digits++; // counter
sum += d; // accumulator
reversed = reversed * 10 + d; // builder — see below
temp /= 10; // advance
}
printf("Digits: %d\n", digits);
printf("Sum: %d\n", sum);
printf("Reversed: %d\n", reversed);
return 0;
}
% ./a.out
Enter a number: 452
Digits: 3
Sum: 11
Reversed: 254
The only line that isn't obvious is reversed = reversed * 10 + d. Multiplying by 10 shifts everything you have built so far one place to the left, leaving a hole in the units column; + d drops the new digit into that hole. Since digits arrive backwards, the number gets built backwards, which is exactly the reverse:
| pass | d | reversed × 10 | + d → reversed |
|---|---|---|---|
| 1 | 2 | 0 | 2 |
| 2 | 5 | 20 | 25 |
| 3 | 4 | 250 | 254 |
Check yourself: what does this program print for the input 1000?
Digits: 4, Sum: 1, Reversed: 1 — verified by running it. The reverse of 1000 "is" 0001, and leading zeros simply don't exist in an int, so it prints as 1. Nothing is broken; that's the honest answer and worth saying out loud if asked.
Problem 2
Decimal-to-binary converter and bit counter
Write a program that asks the user for a positive decimal integer. Use loops to compute its binary representation and print the digits in the correct order (most significant bit first). As a second part of the problem, count and print how many of those bits are 1.
Hint: Use a loop to first find the highest power of 2 that fits into the number, and then use a second loop to work your way downwards to 1.
Sample Execution:
Enter a positive integer: 23
Binary representation: 10111
Number of 1 bits: 4Print the bits most-significant-first, then count the 1s.
n % 2, n /= 2) produces bits from the right, and you are asked to print from the left. Without arrays (Lab 6) you cannot store them and reverse. So you walk the powers of 2 downwards instead — which is the hint the sheet gives you.Two loops. The first climbs: keep doubling while doubling still fits inside n, which leaves p holding the largest power of 2 that is ≤ n. The second descends from that power to 1, asking at each step "is this bit a 1?".
#include <stdio.h>
int main(void)
{
int n;
printf("Enter a positive integer: ");
scanf("%d", &n);
// 1. climb to the highest power of 2 that fits in n
int p = 1;
while (p * 2 <= n) {
p *= 2;
}
// 2. walk back down, printing one bit per power
int ones = 0;
printf("Binary representation: ");
while (p >= 1) {
int bit = (n / p) % 2;
printf("%d", bit);
if (bit == 1) ones++;
p /= 2;
}
printf("\n");
printf("Number of 1 bits: %d\n", ones);
return 0;
}
% ./a.out
Enter a positive integer: 23
Binary representation: 10111
Number of 1 bits: 4
p is the place value the loop is currently asking about — 16, then 8, 4, 2, 1 — not a bit number. On each pass, read (n / p) % 2 as: divide by the place value to bring that bit down into the units position (integer division throws away every bit below it), then take the units bit. Take the single pass where p = 4, for n = 23: 23/4 is 5 (not 5.75), 5 % 2 is 1 — so the 4s bit is set. The same pass for n = 22 gives 22/4 = 5, 5 % 2 = 1: the 4s bit of 22 (= 10110) is set too. For n = 19 (= 10011): 19/4 = 4, 4 % 2 = 0, not set. The full trace for 23, all five passes — the table you would sketch to convince an examiner (the p = 4 row is the example above):
| p | n / p | bit = (n/p) % 2 | ones |
|---|---|---|---|
| 16 | 1 | 1 | 1 |
| 8 | 2 | 0 | 1 |
| 4 | 5 | 1 | 2 |
| 2 | 11 | 1 | 3 |
| 1 | 23 | 1 | 4 |
| 0 | — | — | p ≥ 1 false → stop |
Verified against other inputs: 1 → 1 (one bit); 8 → 1000 (one 1-bit); 255 → 11111111 (eight); 1000 → 1111101000 (six).
Writing while (p <= n) p *= 2; overshoots — it leaves p one doubling past n, and you print a leading zero. The condition has to be p * 2 <= n ("would doubling still fit?"). If you can't remember which, test with n = 8: the answer must be exactly 1000, four characters, no leading zero.
Unpack this step — what about n = 0?
The climb leaves p = 1, the descent prints one bit, (0/1)%2 = 0, so you get 0 with zero 1-bits. Correct. The problem says "positive integer", so this is a bonus rather than a requirement — but it's a good thing to have checked if asked.
Problem 3
The digital root
The digital root of a positive integer is a single-digit value obtained by repeatedly summing its digits. If the sum of the digits results in a number with more than one digit, you must sum the digits of that new number, repeating this process until you arrive at a single-digit result.
Write a program that takes a positive integer from the user, calculates its digital root, and prints the intermediate sums along the way.
Sample Execution:
Enter a number: 987
Intermediate sum: 24
Intermediate sum: 6
Digital root: 6Sum the digits; if the result still has more than one digit, sum again; repeat.
#include <stdio.h>
int main(void)
{
int n;
printf("Enter a number: ");
scanf("%d", &n);
while (n > 9) { // more than one digit?
int sum = 0, temp = n;
while (temp != 0) { // the digit pattern again
sum += temp % 10;
temp /= 10;
}
printf("Intermediate sum: %d\n", sum);
n = sum; // the advance: shrink n
}
printf("Digital root: %d\n", n);
return 0;
}
% ./a.out
Enter a number: 987
Intermediate sum: 24
Intermediate sum: 6
Digital root: 6
n > 9 is the compact way of saying "has more than one digit", and it is worth pausing on: single-digit numbers are exactly 0–9, so anything above 9 needs another round. Note the two nice consequences — feed it a number that is already a single digit (say 5) and the outer loop runs zero times, printing no intermediate sums and going straight to Digital root: 5. That is the zero-iteration boundary case, handled for free by putting the test at the top.
Verified: 987 → 24 → 6. 9999999 → 63 → 9. 5 → 5 with no intermediate line.
int sum = 0; must be inside the outer loop. Declare it outside and the second round adds to the first round's total, and 987 gives you nonsense. Rule of thumb: an accumulator is initialised immediately before the loop that fills it — no earlier.
Check yourself: there is a one-line formula for the digital root. Should you use it?
The digital root of a positive n is 1 + (n - 1) % 9 — for 987 that is 1 + 986 % 9 = 1 + 5 = 6. ✓ But the problem explicitly asks you to print the intermediate sums, which the formula can't do. Use the loop. Mentioning the formula in a comment shows you noticed, and costs nothing.
Problem 4
PIN validator with limited attempts
Write a program that simulates a locked phone or ATM. The program should store a fixed correct PIN (e.g., 1234). The user is repeatedly prompted to enter the PIN. Because the user must always be asked at least once before they can be told if they are right or wrong, this is a perfect fit for a do-while loop.
If they enter the wrong PIN, tell them to try again. After 3 failed attempts, the loop should stop and lock them out with an appropriate message. If they get it right before running out of attempts, it should exit immediately with a success message.
Sample Execution (Lockout):
Enter PIN: 1111
Incorrect. Try again.
Enter PIN: 9999
Incorrect. Try again.
Enter PIN: 0000
Account locked. Too many failed attempts.Sample Execution (Success):
Enter PIN: 1111
Incorrect. Try again.
Enter PIN: 1234
Access granted!Three attempts, then lock out; success exits immediately.
#include <stdio.h>
#include <stdbool.h>
int main(void)
{
const int CORRECT = 1234;
int attempts = 0;
bool unlocked = false;
int entered;
do {
printf("Enter PIN: ");
scanf("%d", &entered);
attempts++;
if (entered == CORRECT) {
unlocked = true; // raise the flag
} else if (attempts < 3) {
printf("Incorrect. Try again.\n"); // don't say it on the last try
}
} while (!unlocked && attempts < 3);
if (unlocked) printf("Access granted!\n");
else printf("Account locked. Too many failed attempts.\n");
return 0;
}
% ./a.out % ./a.out
Enter PIN: 1111 Enter PIN: 1111
Incorrect. Try again. Incorrect. Try again.
Enter PIN: 9999 Enter PIN: 1234
Incorrect. Try again. Access granted!
Enter PIN: 0000
Account locked. Too many failed attempts.
Two details that separate a working program from a polished one, both visible in the sheet's own sample output. First, "Incorrect. Try again." does not appear after the third failure — "try again" would be a lie, since there is no again. That is what the else if (attempts < 3) buys you. Second, the loop condition carries both exit reasons, and the if after the loop asks which one fired. That flag-plus-check shape is the standard way to answer "why did the loop end?"
&& versus ||
while (!unlocked && attempts < 3) means "keep going while still locked and attempts remain". Writing || keeps looping while either holds — so a correct PIN on attempt 1 loops again. Say the condition out loud in English before you write it; the English word for continuing is almost always "and".
Unpack this step — could you use break instead of a flag?
Yes: if (entered == CORRECT) { printf("Access granted!\n"); break; }, with the lockout message after the loop. Both are correct and the lab sheet teaches both (§9.1, §12). The flag version is easier to defend in a viva because the loop condition alone tells the whole story of when it stops.
Problem 5
The smart guesser simulation
Imagine a game where someone picks a hidden number between 1 and 1000, and you have to guess it. Instead of guessing 1, then 2, then 3, the smartest strategy is to always guess the exact middle of the possible range. Depending on whether your guess is too high or too low, you can eliminate half of the remaining numbers at once.
Write a program that simulates this process. Set a hidden number in your code (e.g., 714). Using a while loop, keep track of a low bound and a high bound. In each iteration, calculate the midpoint guess, print it, and then update either the low or high bound based on whether the guess was too small or too large.
Keep looping until the guess matches the hidden number, and print the total number of guesses it took.
Sample Execution:
Target number is 714.
Guessing 500... Too low!
Guessing 750... Too high!
Guessing 625... Too low!
... [intermediate guesses omitted for brevity] ...
Guessing 714... Correct!
Found in 9 guesses.Halve the range each time; count the guesses.
low and high, that close in on each other. Stop: the guess equals the target. Advance: throw away the half that cannot contain the answer.This is binary search, and the reason it is fast is worth one sentence: each wrong guess eliminates half of what remains, so 1000 candidates collapse to about 10 guesses — because doubling 1 ten times passes 1000. Guessing 1, 2, 3, … would take up to 1000.
#include <stdio.h>
int main(void)
{
int target = 714;
int low = 1, high = 1000;
int guesses = 0;
printf("Target number is %d.\n", target);
while (low <= high) {
int guess = (low + high) / 2; // the midpoint
guesses++;
printf("Guessing %d... ", guess);
if (guess == target) {
printf("Correct!\n");
break;
} else if (guess < target) {
printf("Too low!\n");
low = guess + 1; // answer is above the guess
} else {
printf("Too high!\n");
high = guess - 1; // answer is below the guess
}
}
printf("\nFound in %d guesses.\n", guesses);
return 0;
}
% ./a.out
Target number is 714.
Guessing 500... Too low!
Guessing 750... Too high!
Guessing 625... Too low!
Guessing 687... Too low!
Guessing 718... Too high!
Guessing 702... Too low!
Guessing 710... Too low!
Guessing 714... Correct!
Found in 8 guesses.
The lab sheet's sample says "Found in 9 guesses", but its printed guess sequence (500, 750, 625, …) is exactly the one above, which reaches 714 on the 8th guess. This program prints 8, and 8 is what the sequence gives. The sheet's "9" looks like an illustrative number rather than a computed one — its own sample omits the middle guesses with "…". If your program prints 8, it is right; just be ready to say which midpoint rule you used ((low + high) / 2, rounding down).
low = guess instead of low = guess + 1
If the guess was too low, the answer is strictly above it, so the new bottom of the range is guess + 1. Setting low = guess can leave the range stuck at one or two values and loop forever — the classic binary-search infinite loop. Same reasoning gives high = guess - 1 on the other side.
Problem 6
Approximating π with the Gregory–Leibniz series
For centuries, mathematicians calculated the value of π by hand using infinite series. One of the most famous is the Gregory-Leibniz series, which states that if you alternate adding and subtracting fractions with odd denominators forever, you get exactly π/4:
π/4 = 1 − 1/3 + 1/5 − 1/7 + 1/9 − …
This means we can approximate π by multiplying this sum by 4. This series is perfect for a computer loop. Each term follows a predictable pattern, and the sign flips back and forth between positive and negative on every single iteration.
Task:
- Write a program that asks the user for a number of terms N.
- Use a loop to calculate the Leibniz series up to N terms and multiply the final sum by 4.
- The true value of π is roughly 3.14159265358979 (you can get this in C using
acos(-1.0)from<math.h>). Compute and print the percentage error of your approximation. - Run your program with N = 100, then N = 10000, and finally N = 1000000. Observe how the error changes. What does this tell you about the trade-off between accuracy and how long the loop takes to run?
Sample Execution:
Enter N: 10000
Approximate pi: 3.141493
Actual pi: 3.141593
Percentage error: 0.003183%π/4 = 1 − 1/3 + 1/5 − 1/7 + …, times 4, with the percentage error.
sum, and a sign that flips every pass. Stop: after N terms — a known count, so for. Advance: i++, and sign = -sign.The i-th term (counting from i = 0) has denominator 2i + 1 — that generates 1, 3, 5, 7, … exactly. The alternating sign is handled by a variable that multiplies by −1 each pass; that trick reappears constantly.
#include <stdio.h>
#include <math.h>
int main(void)
{
int N;
printf("Enter N: ");
scanf("%d", &N);
double sum = 0.0;
int sign = 1;
for (int i = 0; i < N; i++) {
sum += sign * 1.0 / (2 * i + 1); // note the 1.0 — see the trap
sign = -sign; // flip for the next term
}
double approx = 4 * sum;
double actual = acos(-1.0);
double error = fabs(approx - actual) / actual * 100;
printf("Approximate pi: %f\n", approx);
printf("Actual pi: %f\n", actual);
printf("Percentage error: %f%%\n", error);
return 0;
}
Compile with gcc -Wall -o pi pi.c -lm — the -lm links the maths library that acos and fabs live in. Forgetting it gives a confusing "undefined symbol" error at the link stage, not a compile error.
% ./pi % ./pi % ./pi
Enter N: 100 Enter N: 10000 Enter N: 1000000
Approximate pi: 3.131593 3.141493 3.141592
Actual pi: 3.141593 3.141593 3.141593
Percentage error: 0.318302% 0.003183% 0.000032%
And there is the answer to the question the sheet actually asks. Multiplying the work by 100 divides the error by about 100 — one extra correct decimal place for every hundredfold increase in effort. This series is beautiful and useless for computing π; real algorithms converge far faster. That trade-off, accuracy against running time, is the point of the exercise.
1 / (2*i + 1) is zero
Both operands are int, so C does integer division and throws the fraction away: 1/3 becomes 0, 1/5 becomes 0. The sum then comes out as exactly 1 and π "equals" 4. Writing 1.0 / (2*i + 1) makes the numerator a double and forces real division. Verified: with 1/(2*i+1) the five-term sum is 1.000000; with 1.0/(2*i+1) it is 1.787302. This is the single most common way this problem is failed.
Unpack this step — why %f%% at the end of the printf
%f prints the number; % is printf's escape character, so to print a literal percent sign you write %%. A lone % followed by a letter would be read as another format specifier and print garbage.
Problem 7
Star pattern menu
Write a program that uses a switch statement to ask the user which pattern they want to print, and then uses loops to generate it.
Menu:
1. Right Triangle
2. Inverted Right Triangle
3. Square
Select pattern: 2
Enter size: 4
****
***
**
*A switch chooses the pattern; nested loops draw it.
printf("\n") goes after the inner loop, inside the outer — that newline is what ends a row.#include <stdio.h>
int main(void)
{
int choice, n;
printf("Menu:\n1. Right Triangle\n2. Inverted Right Triangle\n3. Square\n");
printf("Select pattern: ");
scanf("%d", &choice);
printf("Enter size: ");
scanf("%d", &n);
printf("\n");
switch (choice) {
case 1: // growing triangle
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) printf("*");
printf("\n");
}
break;
case 2: // shrinking triangle
for (int i = n; i >= 1; i--) {
for (int j = 1; j <= i; j++) printf("*");
printf("\n");
}
break;
case 3: // square
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) printf("*");
printf("\n");
}
break;
default:
printf("Invalid choice.\n");
}
return 0;
}
% ./a.out
Select pattern: 2
Enter size: 4
****
***
**
*
Compare the three inner loops and nothing else changes: j <= i with i counting up gives the growing triangle; j <= i with i counting down gives the inverted one; j <= n gives the square. Every star-pattern question you will ever be asked is a variation on which expression sits in the inner loop's condition.
break
Leave out break at the end of a case and C "falls through" into the next case — choose 1 and you get the triangle and the inverted triangle and the square. It compiles silently. Every case gets a break (the last one doesn't need it, but write it anyway so nobody has to think about it).
Check yourself: how would you print a pyramid, centred, rather than a left-aligned triangle?
Add a second inner loop before the stars that prints spaces: for row i of n, print n − i spaces, then 2i − 1 stars, then the newline. Row 1 of 5: 4 spaces + 1 star; row 5: 0 spaces + 9 stars. Three loops, one row at a time — same skeleton.
4 · The five debugging exercises
These are the cheapest marks available, and the ones most likely to be reused word-for-word. Each answer is two sentences: what the loop actually does, then the one change.
Debugging exercise 1
The table that stops at 9
This program is supposed to print the multiplication table for 5, up to 5 × 10.
int n = 5;
for (int i = 1; i < 10; i++) {
printf("%d x %d = %d\n", n, i, n * i);
}Task: Explain why it doesn't produce the expected output and fix the condition.
for (int i = 1; i < 10; i++) // prints 5x1 … 5x9
Why: i < 10 is false as soon as i reaches 10, so the body never runs with i = 10 and the last line printed is 5 x 9 = 45 (confirmed by running it). Fix: i <= 10. This is the off-by-one error of §10.2.
Unpack this step — the rule that prevents this every time
Count the iterations before you run: for (i = a; i < b; i++) runs b − a times; i <= b runs b − a + 1 times. Here a = 1, b = 10: the first form gives 9 passes, the second 10. You want 10.
Debugging exercise 2
The digit counter that freezes
This program is supposed to count the number of digits in num.
int num = 459;
int count = 0;
while (num > 0) {
count++;
}
printf("Digits: %d\n", count);Task: What is missing? Why does this program freeze?
while (num > 0) {
count++;
num /= 10; // ← THIS LINE IS MISSING
}
Why it freezes: nothing inside the body changes num, so num > 0 is true forever — 459 stays 459 and the loop never ends. It is the infinite loop of §10.1: the update step is missing. Fix: add num /= 10; inside the loop, and it prints Digits: 3.
If you ever run this by accident: Ctrl+C in the terminal kills a runaway program.
Debugging exercise 3
The sum that prints garbage
This program tries to find the sum of numbers from 1 to 5.
int sum;
for (int i = 1; i <= 5; i++) {
sum = sum + i;
}
printf("Sum is %d\n", sum);Task: Explain why the output is the way it is and correct the bug.
int sum; // ← never given a starting value
for (int i = 1; i <= 5; i++) sum = sum + i;
Why: an uninitialised local variable in C contains whatever bytes happened to be sitting in that memory. The loop then correctly adds 15 to that garbage. Running the buggy version on this machine printed Sum is 221908503 — and it may print something different next run, or on a different machine, which is exactly what makes the bug nasty. Fix: int sum = 0;.
Sometimes the garbage happens to be 0 and the buggy program looks correct. That is the worst outcome, not the best: the bug survives to the exam machine. Compile with -Wall and the compiler warns you before you ever see the number.
Debugging exercise 4
The triangle that came out square
This program intends to print a right-angled triangle, but it prints a square instead.
int n = 4;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
printf("*");
}
printf("\n");
}Task: Change exactly one variable in one condition to fix the pattern.
for (int j = 1; j <= n; j++) // every row gets n stars
for (int j = 1; j <= i; j++) // row i gets i stars ← the fix
Why: the inner loop's limit is n, a constant, so every row is the same length. Fix: exactly one character — change n to i in the inner condition, tying the row's length to the row's number. (That is the change the task asks for: one variable, one condition.) The picture in Problem 7 is this fix, drawn.
Debugging exercise 5
The thermostat glitch — 0.1 + 0.2
A factory thermostat reads a current temperature offset, a calibration adjustment, and compares the result against a target value. The code compiles and runs, but something's off.
#include <stdio.h>
int main(void) {
float current = 0.1;
float adjustment = 0.2;
float target = 0.3;
if (current + adjustment == target) {
printf("Calibrated\n");
} else {
printf("Drift detected\n");
}
if (current + adjustment == 0.3) {
printf("Matches spec\n");
} else {
printf("Off spec\n");
}
return 0;
}Task: Predict the output of both checks. Then explain why the two checks — which look like they're testing the same thing — don't agree.
The output, run on a real compiler:
Calibrated
Off spec
The two checks look identical and disagree. Here is the whole story, in three facts:
- Binary fractions can't hold 0.1, 0.2 or 0.3 exactly, any more than decimal can hold 1/3 exactly. Each is stored as the nearest representable value, so tiny errors enter the moment you type the number.
- The first check compares a
floatwith afloat. The stored value ofcurrent + adjustmentand the stored value oftargetboth land on the samefloat:0.30000001192092895508. Equal — so it printsCalibrated. - The second check compares against the literal
0.3, which is adouble, not afloat. Thefloatsum is widened todoublefor the comparison, and the two disagree in the extra digits adoublecan see:
Not equal — so it printsfloat sum widened to double : 0.30000001192092895508 double literal 0.3 : 0.29999999999999998890Off spec.
The lesson: never compare floating-point values with ==. Compare with a tolerance:
#include <math.h>
if (fabs((current + adjustment) - target) < 1e-6) printf("Calibrated\n");
"Are they equal?" becomes "are they closer together than I care about?" — which is the only question that has a reliable answer in floating point. Integers are exact and may be compared with == freely; that is why loop counters are int.
Unpack this step — where the mismatch actually comes from
A float keeps about 7 significant decimal digits, a double about 16. When the float is widened, the missing precision is filled with the value it was already rounded to, not with the "true" 0.3 — so the widened number keeps the float's rounding error and the double literal keeps its own, much smaller one. Compiling this on a Mac even produces a warning that says so: "floating-point comparison is always false; constant cannot be represented exactly in type 'float'".
5 · The four dry runs
The sheet says: "Before compiling the following programs, predict exactly what they print." So predict first, then open the answer. All four outputs below were confirmed by compiling and running the exact code from the sheet.
for (int i = 1; i <= 5; i++) {
if (i == 4) break;
printf("%d ", i);
}Question: What is the output?
Dry run 1 — for i = 1..5, break when i == 4
Output: 1 2 3. The break fires before the printf on the pass where i = 4, so 4 is never printed, and break abandons the whole loop, so 5 isn't either. Read the body top to bottom: the order of statements inside the loop decides what you see.
int x = 5;
while (0) {
x++;
}
do {
x++;
} while (0);
printf("%d", x);Question: What is the output?
Dry run 2 — while (0) then do … while (0)
Output: 6. while (0) tests first, the condition is false, so the body runs zero times. do … while (0) tests after, so its body runs exactly once before the false condition stops it. x goes 5 → 5 → 6. This question exists to check that you know where the test sits; it is the definition of do-while in one line.
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 2; j++) {
printf("%d", j);
}
printf("-");
}Question: What is the exact string printed?
Dry run 3 — nested loops printing j, with a dash after each outer pass
The exact string: 12-12-12-. The inner loop prints 1 then 2; the dash is printed after the inner loop finishes, once per outer pass; the outer loop runs 3 times. Note there is no newline anywhere, so it is one unbroken string with a trailing dash — the "exact string" wording in the question is aimed at exactly that trailing dash.
for (int i = 1; i <= 5; i++) {
if (i % 2 == 0) continue;
printf("%d ", i);
}Question: What is the output?
Dry run 4 — continue when i is even
Output: 1 3 5. continue skips the rest of the body for that pass only, then goes to the update (i++) and the next test. The loop is not abandoned — that's break. So the even numbers are skipped and the odd ones print.
break leaves the loop. continue skips to the next pass. If you are asked for a sentence in a viva, that is the sentence. In a for loop, continue still runs the update step — which is why the loop can't hang on it — whereas continue in a while loop jumps straight to the condition, and if the update was below it, you have just written an infinite loop.
6 · Challenge · days between two dates
This challenge requires you to blend the complex conditionals (leap years, month lengths) you learned in Lab 3 with the iterative looping mechanics you learned today.
Write a program that takes two dates (each provided as day, month, year). The program must calculate and print the total number of days between the two dates. Example:
Enter Start Date (DD MM YYYY): 28 2 2024
Enter End Date (DD MM YYYY): 2 3 2024
Total days between: 3Your program must handle:
- Leap years correctly (using the rule from Lab 3).
- Advancing through the ends of months correctly.
- Rolling over from December 31st to January 1st of the next year.
Think Before You Code. Do not immediately start writing the program. Don't try to subtract the years and multiply by 365! First answer these questions:
- What is the initial state (starting date) and what is the target state (ending date)?
- What is the condition that tells you the loop should stop running?
- Inside the loop, how do you advance the date by exactly one day?
- What happens when the current day exceeds the number of days in the current month?
- What happens when the month exceeds 12?
Try to break the problem into smaller decisions that update the state step-by-step.
Lab 3's conditionals plus Lab 4's iteration.
if inside the loop instead of a formula.#include <stdio.h>
int main(void)
{
int d1, m1, y1, d2, m2, y2;
printf("Enter Start Date (DD MM YYYY): ");
scanf("%d %d %d", &d1, &m1, &y1);
printf("Enter End Date (DD MM YYYY): ");
scanf("%d %d %d", &d2, &m2, &y2);
int count = 0;
while (!(d1 == d2 && m1 == m2 && y1 == y2)) {
/* 1. how long is the CURRENT month? */
int len;
if (m1 == 4 || m1 == 6 || m1 == 9 || m1 == 11) {
len = 30;
} else if (m1 == 2) {
int leap = (y1 % 400 == 0) || (y1 % 4 == 0 && y1 % 100 != 0);
len = leap ? 29 : 28;
} else {
len = 31;
}
/* 2. advance exactly one day */
d1++;
if (d1 > len) { d1 = 1; m1++; } // past month end → next month
if (m1 > 12) { m1 = 1; y1++; } // past December → next year
count++;
}
printf("Total days between: %d\n", count);
return 0;
}
% ./days
Enter Start Date (DD MM YYYY): 28 2 2024
Enter End Date (DD MM YYYY): 2 3 2024
Total days between: 3
Verified on the cases that break naive solutions: 28 Feb 2024 → 2 Mar 2024 gives 3 (2024 is a leap year, so 29 Feb exists); 1 Jan 2024 → 1 Jan 2025 gives 366; 1 Jan 2025 → 1 Jan 2026 gives 365; 31 Dec 2025 → 1 Jan 2026 gives 1; and the same date twice gives 0 (zero-iteration boundary — the loop never runs).
Because the messy knowledge lives in one place — the "how long is this month" block — instead of being smeared across a formula you then have to patch. This is the decomposition idea from Block A of the syllabus doing real work: a state (the current date), a stop condition (state equals target), and one tiny advance step. It is slow for dates centuries apart and nobody cares; it is obviously correct.
The rule is divisible by 4, except centuries, unless divisible by 400: 2024 leap, 1900 not, 2000 leap. Writing only y % 4 == 0 gets 1900 wrong. Separately, the two ifs must run in the order shown — the day rollover can push the month to 13, and the month check has to come after it to catch that. Swap them and 31 December never becomes 1 January.
Unpack this step — what leap ? 29 : 28 means
The conditional operator: condition ? value_if_true : value_if_false. It's shorthand for a four-line if/else that assigns to len. Write the if/else instead if it reads more clearly to you — they compile to the same thing.
If the start date is after the end date this loop never terminates. Guard it if you have the time: compare the dates first and swap them, or print an error. Saying "I noticed and here's the guard" is worth more than a program that happens not to be tested on it.
7 · The bonus problems, briefly
Only if the rest is solid. Each is a few lines, and each is a pattern you already have.
If you finish early, try tackling one of these synthesis problems:
- Number Detective: Read a single integer and use loops to report every property it has in one pass: is it prime, a palindrome, an Armstrong number, a perfect number (sum of divisors equals the number), and what is its digit sum?
- Collatz Conjecture Stepper: Given n, repeatedly apply this rule: if even, divide by 2; if odd, multiply by 3 and add 1. Print every step until the number reaches 1. Count how many total steps it took. (This is a famous unsolved math problem, by the way)
- Perfect & Amicable Numbers: Find all perfect numbers in a given range. A perfect number's proper divisors sum to exactly the number itself (e.g., 6 = 1 + 2 + 3). Next, extend this to find an "amicable pair" where the sum of divisors of a equals b, and the sum of divisors of b equals a.
- Pascal's Triangle: Print the first N rows of Pascal's triangle.
Collatz stepper
State: n. Stop: n == 1. Advance: halve it or triple-plus-one.
int n = 6, steps = 0;
printf("%d ", n);
while (n != 1) {
if (n % 2 == 0) n = n / 2;
else n = 3 * n + 1;
printf("%d ", n);
steps++;
}
printf("\nSteps: %d\n", steps);
6 3 10 5 16 8 4 2 1
Steps: 8
Perfect numbers in a range
A nested loop where the inner one sums the proper divisors of the outer one's number.
for (int n = 2; n <= 500; n++) {
int sum = 0;
for (int d = 1; d <= n / 2; d++)
if (n % d == 0) sum += d;
if (sum == n) printf("%d ", n);
}
6 28 496
d <= n/2 because no divisor of n other than n itself can exceed n/2 — and "proper" divisors exclude n, which is why the loop stops there.
Pascal's triangle, without arrays
Each entry comes from the previous one in the same row, so you never need to store a row.
int N = 5;
for (int i = 0; i < N; i++) {
int val = 1;
for (int j = 0; j <= i; j++) {
printf("%d ", val);
val = val * (i - j) / (j + 1);
}
printf("\n");
}
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
Extra bonus (not on the sheet) · three marks, one scanf, grades on one line
Read the marks obtained in English, Maths and Science with a single scanf. Grade each subject: below 50 is Fail, 50 to 75 is Pass, above 75 is Distinction. Print only the three grades, on one line.
Sample Execution:
Enter marks in English, Maths and Science: 78 62 45
English: Distinction, Maths: Pass, Science: Failscanf with three %ds and three &-variables in the same order; an if / else if / else chain per subject; and controlling line breaks — a line ends only where you print \n.#include <stdio.h>
int main(void)
{
int english, maths, science;
printf("Enter marks in English, Maths and Science: ");
scanf("%d %d %d", &english, &maths, &science);
/* English */
printf("English: ");
if (english < 50)
printf("Fail");
else if (english <= 75)
printf("Pass");
else
printf("Distinction");
/* Maths */
printf(", Maths: ");
if (maths < 50)
printf("Fail");
else if (maths <= 75)
printf("Pass");
else
printf("Distinction");
/* Science */
printf(", Science: ");
if (science < 50)
printf("Fail");
else if (science <= 75)
printf("Pass");
else
printf("Distinction");
printf("\n");
return 0;
}
% ./a.out
Enter marks in English, Maths and Science: 78 62 45
English: Distinction, Maths: Pass, Science: Fail
Compiled and run with clang -Wall; boundary inputs checked too: 49 → Fail, 50 → Pass, 75 → Pass, 76 → Distinction, 0 → Fail, 100 → Distinction.
- One
scanf, three values. Each%dreads the next number the user types; the&-variables are matched to the%ds purely by order.%dskips whitespace, so spaces, tabs or Enter between the marks all work. Forgetting one&compiles and then stores garbage — the classic slip. - No lower bounds needed.
else if (english <= 75)is only reached whenenglish < 50was false, so it already means "50 to 75".>= 50 &&would be redundant, not wrong. - The boundary follows the wording. "50 to 75 is Pass, above 75 is Distinction" puts 75 in Pass and 76 in Distinction, hence
<= 75. Had it said "75 and above is Distinction", the test would be< 75. Examiners type 49, 50, 75, 76. - One line = no
\nuntil the end. The grade strings carry no newline; the separators", Maths: "and", Science: "ride on the label prints; a singleprintf("\n")closes the line. - The repetition is a smell the next block fixes. The same chain appears three times with only the variable changed. With functions (Block E) it becomes one
print_grade(...)called three times. Until then, copy-paste is honest — and the risk is fixing one copy and forgetting the other two.
8 · If they ask something you've never seen
Which they might — the lab evaluations are unannounced, and a fresh problem is the honest test of whether loops have landed. The recipe below is not a fallback; it is the method, and it works on every problem on this page.
- Write the sample output on your paper first. Literally copy it. It tells you how many lines, in what order, with what wording — half the marks are the output matching.
- Answer the three questions — state, stop, advance — before touching the keyboard. Two minutes here saves twenty.
- Trace two passes by hand with a small input, in a four-column table. If the table does what you want, the code will.
- Type the skeleton, compile it empty, then fill the body. A program that compiles at every step never has twenty errors at once.
- Test the boundaries: zero iterations, one iteration, the typical case. §19 of the sheet asks for exactly these, and volunteering them wins marks even when the program is imperfect.
It is open book — you may have the notes page and this one open. Get them onto the machine (or your phone) before the session starts rather than during it. And a lab evaluation you fumble is one of many, best k of n. The way to lose real marks tomorrow is to arrive exhausted from studying the wrong subject. Calculus first; this is 45 minutes.