Lecture 2 - Loops, switch, Functions, Multi-file Code & Makefiles¶
0. Recap & the for loop, properly¶
- Last time, in brief: the edit -> compile -> run loop with
clang, static typing,int/char/float/double,if/else,printf, andscanf(remember:scanfneeds the&- the address to store into). - We ran low on time, so loops are where we begin today. Three loop shapes -
for,while,do/while- are three ways of writing the same idea: do something repeatedly while a condition holds. - The
forloop, the one we glimpsed last time, spelled out: initruns once at the start (int i = 0)conditionis checked before each iteration (i < n) -0is false, anything non-zero is trueupdateruns after each iteration (i++)- Reading order for
for (int i = 0; i < n; i++): init -> test -> body -> update -> test -> body -> ... until the test is false.
The for loop also allows for multiple statements in each of its three components:
WARNING: If you have multiple statements in the condition - you should use
a logical operator (like && or ||) to combine the conditions, otherwise
the condition may be evaluated incorrectly. We will cover logical operators
in detail next week.
1. while loops¶
- The most stripped-down loop: just a condition and a body.
- The condition is tested before every iteration - so the body may run zero times if the condition is false to begin with.
- Use it when you don't have a simple counter - when you loop until something happens (a sentinel), not a fixed number of times.
- The classic hazard: the infinite loop. If nothing in the body ever makes the condition false, the loop never ends.
- A
forloop is really just awhileloop with the init/update bundled into the header. Anything you can write as one, you can write as the other.
2. do/while loops¶
- Same idea, but the condition is tested after the body:
- The key difference: the body always runs at least once, before the first
test.
whilecan run zero times;do/whilecannot. - The natural fit: input validation and menus - you always have to ask once before you can check the answer.
- Don't forget the trailing
;afterwhile (condition)- a common compile error.
In-class exercise break - for vs while vs do-while¶
Handout: Exercises 1-3 (Part A). Solutions below.
Exercise 1 - Sum 1..n, three ways. Read n, then compute the sum
1 + 2 + ... + n once with a for, once with a while, and once with a
do/while. Print all three - they should agree.
#include <stdio.h>
int main(void) {
int n;
printf("Sum 1..n; enter n: ");
scanf("%d", &n);
int sum_for = 0;
for (int i = 1; i <= n; i++) {
sum_for += i;
}
int sum_while = 0, i = 1;
while (i <= n) {
sum_while += i;
i++;
}
int sum_do = 0, j = 1;
do {
sum_do += j;
j++;
} while (j <= n);
printf("for=%d while=%d do-while=%d\n", sum_for, sum_while, sum_do);
return 0;
}
- The teaching moment - run it with
n = 0: theforandwhileversions print0, but thedo/whileprints1. The body ran once before the test, adding1when it shouldn't have. This is exactly the difference between "test first" and "test after." Lesson:do/whileis the wrong tool when zero iterations is a legal answer.
Exercise 2 - Input validation (where do/while shines). Keep asking until
the user gives a positive number.
int n;
do {
printf("Enter a positive number: ");
scanf("%d", &n);
} while (n <= 0);
printf("Thanks! You entered %d\n", n);
- Ask the room: why is
do/whilecleaner here thanwhile? (Withwhileyou'd have to prompt once before the loop and again inside it - duplication.)
Exercise 3 - Stretch: multiplication table. Read n, print n x 1 through
n x 10, one per line. A for (or while) reads best - we count a known number
of times.
#include <stdio.h>
int main(void) {
int n;
printf("Multiplication table for: ");
scanf("%d", &n);
for (int i = 1; i <= 10; i++) {
printf("%2d x %d = %d\n", i, n, i * n);
}
return 0;
}
-
%2dright-aligns the counter in a 2-wide field so the table lines up. -
Rule of thumb to put on the board:
- Counting a known number of times ->
for - Looping until a condition, maybe zero times ->
while - Looping until a condition, but at least once ->
do/while
3. switch / case¶
You may have seen long if-else if- else ladders in your code. For example:
int day;
printf("Enter a day (1-7): ");
scanf("%d", &day);
if (day == 1) {
printf("Monday\n");
} else if (day == 2) {
printf("Tuesday\n");
} else if (day == 3) {
printf("Wednesday\n");
} else if (day == 4) {
printf("Thursday\n");
} else if (day == 5) {
printf("Friday\n");
} else if (day == 6) {
printf("Saturday\n");
} else if (day == 7) {
printf("Sunday\n");
} else {
printf("Invalid day.\n");
}
- A
switchis a clean multi-way branch on a single integer orcharvalue - an alternative to a long
if/else ifladder. caselabels must be constant integers (orchars, which are integers). You cannotswitchon adouble, a string, or a range.breakmatters. Without it, execution falls through into the nextcase. Forgettingbreakis one of the most commonswitchbugs...- ...but deliberate fall-through is a useful idiom when several cases share one body:
defaultis optional but recommended - it's your "unexpected input" branch.- When to prefer
switchoverif/else if: when you're dispatching on the discrete values of one variable (a menu choice, an operator character, a state). For ranges or compound conditions, stick withif.
In-class exercise break - switch¶
Handout: Exercises 4-5 (Part B). Solutions below.
Exercise 4 - A menu with switch. Print a small menu, read the user's choice,
and switch on it; let default handle anything unexpected.
#include <stdio.h>
int main(void) {
int choice;
printf("1) Say hello\n2) Say goodbye\n3) Quit\nChoose: ");
scanf("%d", &choice);
switch (choice) {
case 1: printf("Hello!\n"); break;
case 2: printf("Goodbye!\n"); break;
case 3: printf("Quitting.\n"); break;
default: printf("Not a valid choice.\n");
}
return 0;
}
- This menu pattern is the skeleton of the calculator we build later - keep it in mind.
Exercise 5 - Vowel check (deliberate fall-through). Read one character; group the vowel cases so they share a single body.
#include <stdio.h>
int main(void) {
char c;
printf("Enter a letter: ");
scanf(" %c", &c); /* the leading space skips whitespace */
switch (c) {
case 'a': case 'e': case 'i': case 'o': case 'u':
printf("'%c' is a vowel\n", c);
break;
default:
printf("'%c' is not a vowel\n", c);
}
return 0;
}
- Note the
" %c"- the leading space tellsscanfto skip any leftover whitespace (like the newline from a previous entry) before reading the char.
4. Functions¶
- Why functions? Reuse (write once, call many times), naming (a good name
documents intent), and decomposition (break a big problem into small,
testable pieces). We've been calling
printfandscanfall along - now we write our own. - Three distinct things, often confused:
- Declaration (prototype): the function's signature - return type, name,
parameter types - ending in a
;, with no body. It's a promise that the function exists. - Definition: the prototype plus the body - the actual code.
- Call: using it.
int y = square(5); - Anatomy:
int square(int n)-> returns anint, takes oneintparameter namedn.returnhands a value back to the caller. A function that returns nothing has return typevoid. - Where do prototypes go, and why? C reads top to bottom. If you call a
function before the compiler has seen its definition, you need a prototype
near the top of the file so the compiler knows the types. The usual layout:
prototypes up top ->
main-> definitions below. (Next section: prototypes move into a header file.) - Parameters are passed by value. The function gets its own copy of each argument; changing a parameter inside the function does not change the caller's variable. (Getting a function to modify the caller's data needs pointers - Week 3.)
- Common errors to preview:
- Calling a function with no prototype in sight -> implicit declaration warning (and trouble).
- Return type / argument type mismatches.
In-class exercise break - write some functions¶
Handout: Exercises 6-8 (Part C, and the start of Part D). Solutions below.
Exercise 6 - Refactor Lecture 1's Celsius -> Fahrenheit into a function.
#include <stdio.h>
double c_to_f(double c); /* prototype */
int main(void) {
double celsius;
printf("Enter temperature in Celsius: ");
scanf("%lf", &celsius);
printf("%.1f C = %.1f F\n", celsius, c_to_f(celsius)); /* call */
return 0;
}
double c_to_f(double c) { /* definition */
return c * 9.0 / 5.0 + 32.0;
}
- Point at each of the three roles - prototype, call, definition - by name.
Exercise 7 - A small number toolkit. Three functions, each pairing a loop with a function:
#include <stdio.h>
int factorial(int n); /* 5! = 120 */
int is_prime(int n); /* returns 1 if prime, else 0 */
int gcd(int a, int b); /* greatest common divisor */
int main(void) {
printf("5! = %d\n", factorial(5));
printf("is_prime(7) = %d, is_prime(8) = %d\n", is_prime(7), is_prime(8));
printf("gcd(48, 36) = %d\n", gcd(48, 36));
return 0;
}
int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
int is_prime(int n) {
if (n < 2) {
return 0;
}
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) {
return 0; /* found a divisor -> not prime */
}
}
return 1;
}
int gcd(int a, int b) {
while (b != 0) { /* Euclid's algorithm */
int temp = b;
b = a % b;
a = temp;
}
return a;
}
is_primereturning1/0is idiomatic C - recall conditions are just integers. Thei * i <= ntest stops at sqrt(n).gcdis a tidywhileloop.
Exercise 8 - The calculator, in one file (functions + switch). add, sub,
mul, divide, each int (int, int); main reads two numbers and an operator
char, then a switch dispatches to the right function.
#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int divide(int a, int b) { return a / b; }
int main(void) {
int x, y;
char op;
printf("Enter: <num> <op> <num> (op is + - * /): ");
scanf("%d %c %d", &x, &op, &y);
int result;
switch (op) {
case '+': result = add(x, y); break;
case '-': result = sub(x, y); break;
case '*': result = mul(x, y); break;
case '/':
if (y == 0) {
printf("Error: division by zero\n");
return 1;
}
result = divide(x, y);
break;
default:
printf("Unknown operator '%c'\n", op);
return 1;
}
printf("%d %c %d = %d\n", x, op, y, result);
return 0;
}
- The spaces in
"%d %c %d"letscanfskip whitespace around the operator. We guard division by zero, anddefaultcatches unknown operators. - This exact program is what we split across files next (Exercise 9).
5. Code Organization - headers & multi-file builds¶
- So far, one
.cfile. Real programs are split across many files: for organization, for faster rebuilds, and so code can be shared and reused. - The convention:
.h(header) = declarations - function prototypes, type definitions, constants. What exists..c(source) = definitions - the actual code. How it works.- A file uses another's functions by
#include-ing its header: #include <stdio.h>- angle brackets for system/standard headers#include "mathops.h"- quotes for your own headers (look in this directory first)#includeis literally copy-paste done by the preprocessor: it drops the header's text into your file before compiling.- Because a header can get included more than once, guard it so its contents are only processed once - an include guard:
- Building is two steps:
- Compile each
.cto an object file (.o) - machine code with "holes" where it calls things defined elsewhere:clang -c mathops.c->mathops.o - Link the object files into one executable - filling the holes:
clang main.o mathops.o -o calc - A teaching demo worth doing live: compile
main.calone and try to run - or link withoutmathops.o- and read theundefined reference to 'add'linker error. That error means "you declared it, you called it, but I never got its definition to link in."
Live-coding / exercise (within section 5) - split the calculator¶
Handout: Exercise 9 (Part D). Solution below.
Take the single-file calculator from Exercise 8 and split it three ways.
mathops.h - declarations, behind an include guard:
#ifndef MATHOPS_H
#define MATHOPS_H
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int divide(int a, int b);
#endif /* MATHOPS_H */
mathops.c - definitions; it includes its own header so the compiler checks they
match:
#include "mathops.h"
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int divide(int a, int b) { return a / b; }
main.c - exactly the main from Exercise 8, now with #include "mathops.h"
instead of the inline function definitions:
#include <stdio.h>
#include "mathops.h"
int main(void) {
int x, y;
char op;
printf("Enter: <num> <op> <num> (op is + - * /): ");
scanf("%d %c %d", &x, &op, &y);
int result;
switch (op) {
case '+': result = add(x, y); break;
case '-': result = sub(x, y); break;
case '*': result = mul(x, y); break;
case '/':
if (y == 0) {
printf("Error: division by zero\n");
return 1;
}
result = divide(x, y);
break;
default:
printf("Unknown operator '%c'\n", op);
return 1;
}
printf("%d %c %d = %d\n", x, op, y, result);
return 0;
}
Build by hand and run:
- Break it on purpose: link
main.oalone (clang main.o -o calc) and read theundefined reference to 'add'error -addwas declared (somain.ccompiled) but its definition never got linked in.
6. Makefiles¶
- Retyping those
clanglines every time gets old fast - and as projects grow you don't want to recompile everything on every small change.makeautomates the build. - A
Makefileis a set of rules. Each rule: - The #1 gotcha: recipe lines must start with a real TAB, not spaces.
makewill error (missing separator) if you use spaces. (We'll point this out because every editor handles it differently.) - Use variables to avoid repetition:
- Run
make(builds the first target,calc) ormake clean(a "phony" target that just deletes build artifacts). - The payoff - incremental builds.
makecompares file timestamps and rebuilds only what's out of date. Edit onlymathops.c, runmake, and it recompilesmathops.oand relinks - but does not recompilemain.c. Listingmathops.has a prerequisite means changing the header rebuilds both.os, which is what you want.
In-class exercise break (within section 6) - write a Makefile¶
Handout: Exercise 10 (Part D). The
Makefileshown above is the solution.
- Write that
Makefilefor your split calculator (mind the tabs). - Run
makefrom scratch; run./calc. - Edit just
mathops.c(e.g. tweak a message), runmakeagain, and watch which files recompile. Thenmake clean. - What to observe: after editing only
mathops.c,makerecompilesmathops.oand relinkscalc, but does not recompilemain.c- nothing it depends on changed.