CS U111 · Lecture 7 · Labs 5A–5B · Notes — the compressed map

Functions, in one page

Every rule from Lecture 7, three worked examples at evaluation level, and the traps that lose marks. This is the revision layer. First time with functions? Start with the lesson, which has a step-through of a call and the reasoning behind each rule, then come back here.

Start here — what's examined, and what this page is built from

Where functions will be tested. Continuous lab evaluations are unannounced and open book, 10% of the course in total, best k of n. So this page is meant to be open beside you during one. The mid-semester exam (30%, closed book) comes after Lecture 7, so functions are almost certainly in its scope. Its date comes from the Institute academic calendar; the handout doesn't give one.

Source and scope. This page is built from Lecture 7's slides. Lab Sheets 5A and 5B, which the slides name, haven't been received yet; a solutions page follows when they are. Syllabus Block E (top-down design, prototypes, parameters, return values, scope, lifetime). Textbook: Hanly & Koffman 8e §3.4 §3.5 plus the scope section of ch. 6. The handout maps this block to ch. 6, so confirm section numbers against your copy. Not in scope yet: pointers, passing arrays to functions, recursion. The lecture defers all three.

The whole thing in three questions

Answer these on paper before writing any function, and the header writes itself:

1 · What goes in? Each input becomes a parameter with its own type: (double c), (int a, int b). Nothing? (void).
2 · What comes out? One value → that type, and a return. Nothing, it only acts (prints)? void.
3 · Return or print? "find / compute / check" → return, let main print. "display / print a report" → print inside, void.

The standard file layout

#include <stdio.h>

int add(int a, int b);            /* 1. prototypes: header + ;  (a promise) */

int main(void) {                  /* 2. main near the top: the summary     */
    int x = 10, y = 15;
    printf("%d\n", add(5, 3));          /* 8  */
    printf("%d\n", add(x + 5, y * 2));  /* 45 — arguments evaluated first, then copied */
    return 0;
}

int add(int a, int b) {           /* 3. definitions: header + body (the promise kept) */
    return a + b;
}

C compiles top to bottom. A call to a function the compiler hasn't met yet is a hard error with clang on a Mac: "call to undeclared function 'add'; ISO C99 and later do not support implicit function declarations". The prototype and the definition headers must match exactly. If they don't, clang reports "conflicting types for 'add'".

Every rule, intuition first

1 · A function is a black box

Inputs in, one result out, the inside sealed. You've used printf, sqrt for weeks without reading them.

return_type name(type param1, type param2) {
    body
    return value;       // value must match return_type
}

Each parameter needs its own type: (double x, double y), never (double x, y). main is itself one of these: it returns an int to the operating system.

2 · A call pauses the caller

Control jumps into the function, runs it, and comes back to the same spot. The call is then replaced by the returned value.

int result = calculateSquare(7);   // main waits; the call becomes 49
printf("%d\n", calculateSquare(3) + calculateSquare(4));   // 9 + 16 → 25

Writing a function doesn't run it. The program always starts at main, wherever main sits in the file.

3 · return is not printf

printf shows a value to the person. return hands it to the program, which can store, add or compare it.

void printSquare(int x) { printf("%d\n", x * x); }   // shows it; returns nothing
int  square(int x)      { return x * x; }            // hands it back

int total = square(3) + square(4);                   // 25 ✓
int bad   = printSquare(3) + printSquare(4);         // error: invalid operands ('void' and 'void')

4 · return ends the function on the spot

The first return reached wins. Anything after it on that path is dead code, and clang -Wall doesn't warn about it.

return result;
printf("done\n");     // never runs

Used on purpose for "found it, stop": several returns in one function is normal (see Example 3).

5 · void: acts, hands nothing back

Menus, reports, messages. Called as a statement on its own: printWelcomeMessage();

void printResult(int marks) {
    if (marks < 0 || marks > 100) { printf("Invalid\n"); return; }   // bare return: leave early
    printf("%s\n", marks >= 40 ? "pass" : "fail");
}

No return needed at the end: it finishes at the closing brace. return value; inside a void function is an error.

Unpack this step — marks >= 40 ? "pass" : "fail"

The ? : operator is a one-line if-else that produces a value: condition ? A : B gives A if the condition is true, B otherwise. Here it picks which word printf prints. An ordinary if (marks >= 40) printf("pass\n"); else printf("fail\n"); does exactly the same job.

6 · Pass by value: the function gets a photocopy

Each argument's value is copied into the parameter. Changing the copy never touches the caller's variable.

void modifyValue(int x) { x = 100; }
int a = 10;
modifyValue(a);
printf("%d\n", a);     // 10

C has no pass-by-reference. To change a caller's variable today: return the new value and let the caller assign it, k = twice(k);. The other route is pointers (the & you give scanf), which is a later lecture.

modifyValue's memory x 10 → 100 main's memory a 10 still 10 value copied at the call
Rule 6, drawn: two separate boxes in memory. At the call, the value 10 is copied from a into x. After that there is no connection: changing x to 100 cannot reach a.

7 · Locals: each function is a sealed room

Variables declared inside a function (parameters included) are visible only there, created at the call, destroyed at the return.

void processData(int y) { int x = 50; }   // this x ≠ main's x
int main(void) { int x = 10; processData(5); /* x still 10; y doesn't exist here */ }

Scope = where a name can be seen. Lifetime = how long the box exists. For a local, both are one function / one call. Two functions can reuse a name safely.

8 · Globals, and shadowing

Declared outside every function: every function can read and change it, for the whole run. Use for true constants only.

int total = 0;                      // global
void addMarks(int m) {
    int total = 0;                  // a local with the same name HIDES the global
    total = total + m;              // changes the local only
}

Call addMarks(40); addMarks(35); then print the global: total = 0. Compiles with no warning under -Wall. The lecture's three dangers: any function can change a global (so debugging means reading all of them), shadowing, and functions that can't be reused without dragging the global along.

9 · No functions inside functions

Every definition lives at the top level of the file, side by side.

int main(void) {
    int add(int a, int b) { return a + b; }   // error: function definition is not allowed here
}

Three worked examples at evaluation level

Worked example 1 · the lecture's in-class activity

Temperature converter, with no logic in main

"Write celsiusToFahrenheit(double c), returning c * 9.0/5.0 + 32. Write printTemperatureReport(double c), a void function that calls the function above and prints both values nicely. In main(), declare the prototypes, then call printTemperatureReport() for three different temperatures. Trace what happens if you accidentally write void celsiusToFahrenheit(...) with no return."

  1. Answer the three questions for each function. celsiusToFahrenheit: in, one double; out, one double; "returning", so return. printTemperatureReport: in, one double; out, nothing; "prints", so void.
  2. Write both prototypes at the top, copied from those answers, each ending in ;.
  3. Write main as three calls and nothing else. Pick test temperatures you can check by hand: 0 (freezing: 32 °F), 37 (body temperature: 98.6 °F), and −40, the one temperature that is the same on both scales.
  4. Write the definitions below main. The report function calls the converter, catches its return value in a variable, and prints. A function calling a function is completely normal.
#include <stdio.h>

double celsiusToFahrenheit(double c);
void printTemperatureReport(double c);

int main(void) {
    printTemperatureReport(0.0);
    printTemperatureReport(37.0);
    printTemperatureReport(-40.0);
    return 0;
}

double celsiusToFahrenheit(double c) {
    return c * 9.0 / 5.0 + 32;
}

void printTemperatureReport(double c) {
    double f = celsiusToFahrenheit(c);
    printf("%7.2f C = %7.2f F\n", c, f);
}
   0.00 C =   32.00 F
  37.00 C =   98.60 F
 -40.00 C =  -40.00 F
Unpack this step — what %7.2f does

%.2f (which you know) prints two decimal places. The 7 in front is a minimum width: pad with spaces on the left until the number takes 7 characters. That's why the three lines' numbers line up in columns. %.2f alone would be marked just as correct.

  1. Check one line by hand. 37 × 9 = 333; 333 ÷ 5 = 66.6; 66.6 + 32 = 98.6 ✓.
  2. The activity's trap question. Change the converter's return type to void (in the prototype and the definition) and keep everything else. Clang refuses with two errors, one per misuse:
    error: void function 'celsiusToFahrenheit' should not return a value [-Wreturn-mismatch]
    error: initializing 'double' with an expression of incompatible type 'void'
    The first is inside the converter: a void function can't hand back c * 9.0/5.0 + 32. The second is in the report function: there's nothing coming back to store in f. Both say the same thing: the header's return type must match what the function does and how it's used.
Check yourself: why 9.0 / 5.0 and not 9 / 5?

9 / 5 is integer division: the fraction is thrown away, giving 1. Write the formula as 9 / 5 * c + 32 and 100 °C comes out as 132.00 (verified), not 212. Here the order would actually save you: c * 9 / 5 evaluates left to right, and c is a double, so c * 9 is already a double. But writing 9.0 means you never have to think about order. It's Lab 4's integer-division trap in a new place.

Worked example 2 · predict the output

A global, a parameter with the same name, and a local that shadows it

#include <stdio.h>

int count = 5;                       /* global */

void bump(int count) {               /* parameter hides the global */
    count = count + 10;
    printf("bump: %d\n", count);
}

void addOne(void) {
    count++;                         /* no local count here: the global */
}

int main(void) {
    int x = count;
    bump(x);
    addOne();
    printf("main: %d %d\n", x, count);
    int count = 100;                 /* from here on, main's count hides the global */
    addOne();
    printf("main: %d\n", count);
    return 0;
}
  1. For every name, ask "which box?" Inside a function, a name means the nearest declaration: a local or parameter of that function if there is one, otherwise the global. A local only counts from the line where it's declared onwards.
  2. Draw one column per box, not per name. There are three different counts here: the global, bump's parameter, and (later) main's local.
  3. Walk main line by line, stepping into each call.
    lineglobal countmain's xbump's countmain's countprints
    start5———
    int x = count;55——(no local count yet → global)
    bump(x)555 → 15—bump: 15
    addOne()65gone—
    printf65——main: 5 6
    int count = 100;65—100
    addOne()75—100(addOne can't see main's locals)
    printf75—100main: 100
  4. Read off the output (verified by compiling and running):
    bump: 15
    main: 5 6
    main: 100

Three things to notice. bump changed only its own parameter, so the global stayed 5 during that call. addOne reaches the global both times, even after main declared its own count, because a local is visible only inside the function that declares it. And the global ends at 7, which is never printed: after int count = 100;, main has no way to name the global any more. Compiled with clang -Wall, this program gives zero warnings. That silence is why "prefer parameters and return values" is the rule.

Worked example 3 · write one from a spec

An isPrime function, used from main

"Write a function that checks whether a number is prime. Use it to print all primes from 1 to 30 and how many there are."

  1. Three questions. In: one int. Out: yes/no, so bool (from <stdbool.h>, as in Lab 4). "Checks" means return, not print. Name it as a question: bool isPrime(int n).
  2. Split the jobs. isPrime knows nothing about 1 to 30 or about printing. It answers one question about one number. main owns the loop, the printing and the count. That split is the point of the exercise.
  3. Body of isPrime: Lab 4's prime test, with return doing the flag's job. Numbers below 2 aren't prime, so return false at once. Then try divisors: the first one found means return false, which leaves the function immediately. If the loop finishes without finding one, the number is prime.
  4. Write main as an ordinary counting loop that calls isPrime(k) as its if condition. A call that returns bool can sit wherever a condition can.
#include <stdio.h>
#include <stdbool.h>

bool isPrime(int n);

int main(void) {
    int count = 0;
    for (int k = 1; k <= 30; k++) {
        if (isPrime(k)) {
            printf("%d ", k);
            count++;
        }
    }
    printf("\n%d primes up to 30\n", count);
    return 0;
}

bool isPrime(int n) {
    if (n < 2) return false;             /* 0, 1, negatives */
    for (int i = 2; i * i <= n; i++) {
        if (n % i == 0) return false;    /* found a divisor: leave now */
    }
    return true;                         /* loop finished: no divisor */
}
2 3 5 7 11 13 17 19 23 29
10 primes up to 30
Unpack this step — why the loop can stop at i * i <= n

Divisors come in pairs, a × b = n, and both can't be above √n (their product would exceed n). So if there's a divisor at all, one is at most √n. Same argument as the Lab 4 notes, pattern 5.

  1. Test the edges, not just the middle. 1 (not prime: the n < 2 line), 2 (prime: the loop runs zero times because 2 × 2 > 2, so it falls through to return true), 4 (first composite), 29 (largest in range). Ten primes up to 30 matches the list you know.
Check yourself: write int maxOfThree(int a, int b, int c) in the same style, and say what it returns for (−3, −8, −1).

Start with a guess and improve it, the "running best" pattern from Lecture 8's minimum-finding:

int maxOfThree(int a, int b, int c) {
    int best = a;
    if (b > best) best = b;
    if (c > best) best = c;
    return best;
}

(−3, −8, −1) gives −1; (4, 9, 2) gives 9; (7, 7, 7) gives 7 (all verified). Starting best at 0 instead of a is the classic bug: it would return 0 for three negatives.

Classic traps — the standard ways marks are lost

The mistakeWhat you seeThe fix
Printing instead of returning: a "compute" function ends in printfThe answer appears on screen, but the caller can't use it; void + void errors if you tryReturn the value; print in main. "Find / compute / check" means return.
Missing return in a non-void function, or on one path of an ifWarning: "non-void function does not return a value in all control paths". The caller gets garbage (a run here gave 1 instead of 9).Every path through the function ends in return value;. Read the -Wall warnings; this one is a real bug.
No prototype, and the function is defined below mainError: "call to undeclared function"Prototype at the top, ending in ;.
Prototype without its ;Error: "expected ';' after top level declarator"Add it. If an error points at a good line, check the line above.
Prototype and definition disagree (int vs double)Error: "conflicting types"Copy-paste the header, then add ; for the prototype.
void function returning a value, or its "result" stored in a variableErrors: "void function should not return a value" / "incompatible type 'void'"Make the return type match what the function hands back.
Expecting a function to change the caller's variabledoubleIt(k); then k is unchangedPass by value. Return the new value: k = twice(k);
A local that shadows a global (a stray int in front of the name)The global never changes; no warningDon't reuse global names for locals. Better still, don't use globals for data.
Integer division in a formula: 9 / 5 * c100 °C → 132 °F9.0 / 5.0. At least one operand must be a double.
Code after returnA message that never appears; no warningPut the printf before the return, or in the caller.
Using a function's local in mainError: "use of undeclared identifier"Locals don't leave their function. Return the value instead.
(double x, y) in a parameter listError: "type specifier missing"Every parameter gets its own type: (double x, double y).

Every message above was produced by clang -Wall on the Mac these notes were written on. On a lab machine using gcc, the wording differs but the problems are the same.

Minimal prerequisite kit

The complete list of what this lecture leans on. Nothing else is assumed.

FactWhy it appears
Variable declaration and types: int, double, bool (via <stdbool.h>)Every parameter and every return type is one of these
int / int truncates; one double operand makes it real divisionTemperature formulas, averages
% is the remainderisEven, isPrime
A condition like n % 2 == 0 is itself a true/false valuereturn n % 2 == 0; is a complete yes/no function
Loops from Lab 4: counter, flag, running bestAlmost every useful function body is one of those loops
printf formats: %d, %.2fPrinting results in main

What to practise

Ranked for a short evening. When Lab Sheets 5A and 5B arrive, their problems go to the top of this table: they are what the evaluations are drawn from.

SkillDrill it onTextbook backup Hanly ch. 3 · ch. 6How many
Predicting output across callsPredict-the-output drill, function families; then Example 2 above, redone from memorySelf-check exercises, "Functions with input arguments"1 sheet a day · 10 min
Writing a header from a specHeaders only for: area of a circle, is-leap-year, grade from marks, digit sumSelf-check, "Functions without arguments"4 · 5 min
Return vs print; voidThe lecture's temperature activity (Example 1), typed from scratchProgramming exercises, end of those sections (odd-numbered: answers in the back)1 · 15 min
Turning a Lab 4 program into a functionLab 4 Problem 1 (digit sum) as int digitSum(int n), and palindrome as bool isPalindrome(int n)"Top-down design" case study, ch. 32 · 20 min
Scope and shadowingChange Example 2's program in one place and predict again before running"Scope of names", ch. 6 (self-check only; the pointer parts of ch. 6 come later)3 variations

Section titles rather than numbers where possible, because editions renumber. Confirm against your copy; if the instructor names sections, those win.

Don't do this tonight

Don't read the rest of Hanly ch. 6. Most of it is about output parameters, which are pointers, and the lecture has explicitly put pointers later. Don't start on recursion either (a function calling itself). It's Block G, weeks away, and it's much easier once calls feel automatic. And don't let an AI write the functions: the lab is invigilated, and typing them yourself is what makes the three questions automatic.