CS U111 · Lecture 7 · Labs 5A–5B · Lesson — first time through
Functions, from the beginning
One idea at a time, with the reasoning before the syntax. About 45 minutes if you stop at each "check yourself" and actually try it. The compressed version lives on the notes page, with three evaluation-level worked examples and the trap table.
Source: Lecture 7's slides (Functions in C). The slides point to Lab Sheets 5A and 5B, which haven't reached these notes yet. When they arrive, a solutions page with every sheet problem worked will follow, as it did for Lab 4. Nothing here guesses at what those sheets ask; it teaches what the lecture taught.
Where you actually stand: writing your own C functions is new to the whole batch. Classmates who took school Computer Science may have written functions in Python, which helps a little. But C's version, with a declared type for every input and output and a prototype at the top, is new to almost everyone. Functions also lean on exactly one thing you already have: Lab 4's loops. Every example below that does real work is a loop you've already written, moved into a box with a name on it.
1 · You've been using functions since the first week
printf is a function. So are scanf and sqrt. You've called them dozens of times without ever seeing the code inside, and that is not a gap in your knowledge. It is the whole point.
Think about what you actually need to know to use sqrt. You need to know it wants one number, and that it gives back that number's square root. How it computes the root, you have never needed to know. A function is a black box: something goes in, something comes out, and the inside is somebody else's problem.
Until now, you have used black boxes someone else built. Lecture 7 is about building them. Every idea on this page is about one of two edges of the box: what goes in (parameters) and what comes out (the return value), plus the rule that the inside is sealed.
2 · Why build your own
Suppose a program needs the area of a circle in three places. Without functions, you write the formula three times:
area1 = 3.14159 * r1 * r1;
/* ... */
area2 = 3.14159 * r2 * r2;
/* ... */
area3 = 3.14159 * r3 * r3;
It works, until you find a mistake in the formula, or need more digits of π. Now you must fix it in three places and hope you didn't miss one. A function turns "write it again" into "call it again": written once, called three times, with one place to fix.
There is a second, bigger reason. In Lecture 4 you broke a grade-computation problem into boxes on a diagram: read scores → compute weighted average → determine letter grade → display result. Back then those boxes were just drawings. Functions let each box become real code with its own name. main then shrinks to a short summary of the whole program:
int main(void) {
readScores();
computeWeightedAverage();
determineLetterGrade();
displayResult();
return 0;
}
You can read that and know what the program does without reading a single detail. That is called top-down design: decide the big steps first, then write each step as its own function.
3 · The parts of a function
Here is the smallest useful function, taken apart:
int add(int a, int b) { // the header
int sum = a + b; // the body …
return sum; // … ending with what it hands back
}
| Part | In add | What it tells you |
|---|---|---|
| Return type | int (first word) | What kind of value comes out of the box |
| Name | add | How you call it. Make it a verb that says what it does. |
| Parameter list | (int a, int b) | What goes in: how many values, of which types, and the names the body will use for them |
| Body | everything in { } | The work |
return | return sum; | Sends a value back out to whoever called |
Notice you have been writing one of these all along. int main(void) { … return 0; } has a return type (int), a name (main), an empty parameter list (void means "takes nothing"), a body, and a return. main is simply the function the operating system calls to start your program, and the 0 it returns is how it reports "finished fine".
Check yourself: write just the header (first line) of a function average that takes two doubles and gives back a double.
double average(double x, double y). The return type comes first. Each parameter gets its own type, so double x, y is not allowed inside the parentheses (clang: "type specifier missing" for the y), even though it's fine for ordinary variable declarations. The names x and y are your choice.
4 · What actually happens when you call one
Take a first real function:
int calculateSquare(int x) {
int square = x * x;
return square;
}
int main(void) {
int result = calculateSquare(7);
printf("result = %d\n", result);
return 0;
}
result = 49
The important thing is what happens on the line int result = calculateSquare(7);. Control does not simply carry on to the next line. It jumps away:
mainreaches the call and pauses in the middle of the line.resultis still waiting for a value.- Execution jumps into
calculateSquare, and a fresh variablexis created holding a copy of 7. - The body runs.
returnsends 49 back. - Execution resumes in
mainon the same line, as ifcalculateSquare(7)had been replaced by 49. Soresult= 49.
The caller genuinely waits. Nothing in main runs while the function is working. Step through it below with a slightly richer example. Watch the boxes on the right: each function gets its own box of variables, which appears when the function is called and disappears when it returns.
Three things in that stepper are the whole lecture in miniature. Step 1: the program starts at main, even though addTen is written first. Writing a function doesn't run it; calling it does. Step 4: x is a brand-new box that receives a copy of 5. Step 8: a is still 5, because addTen only ever changed its copy. That last point gets its own section below.
Check yourself: what does printf("%d\n", calculateSquare(3) + calculateSquare(4)); print?
25 (verified). Each call is replaced by the value it returns, 9 and 16, and then they're added. A call that returns a value can go anywhere a number can go: inside arithmetic, inside printf, inside another call's parentheses.
5 · return is not printf
This is the most common confusion with first functions, and it is worth ten minutes to get straight. Here are two functions that look like they do the same thing:
void printSquare(int x) { /* SHOWS the answer on screen */
printf("%d\n", x * x);
}
int square(int x) { /* HANDS BACK the answer */
return x * x;
}
printSquare(4) puts 16 on the screen. That's all. The 16 goes to the human looking at the terminal. The program itself never gets it, so it can't add it, compare it or store it.
square(4) puts nothing on the screen. It hands 16 back to the program, which can then do anything with it:
printSquare(4); /* prints 16 — and that is all */
int total = square(3) + square(4); /* 9 + 16, nothing printed yet */
printf("total = %d\n", total);
16
total = 25
Try to use the printing version in arithmetic, printSquare(3) + printSquare(4), and clang refuses outright:
error: invalid operands to binary expression ('void' and 'void')
Functions that compute should return. Let the caller decide whether to print. A function that returns its answer can be used in a hundred ways. A function that only prints it can be used in one. When a problem says "write a function that finds / computes / checks …", it wants a return. When it says "write a function that displays / prints …", printing inside is right.
Check yourself: a function int cube(int n) ends with printf("%d", n*n*n); and has no return. What's wrong, and what will int v = cube(2); put in v?
It promises an int (the return type) but never hands one back. It shows 8 on the screen, but v gets garbage: whatever happened to be left where the answer should be. (Run on the machine these notes were written on, v came out as 1. Yours may differ.) clang -Wall warns about exactly this: "non-void function does not return a value". Fix: return n * n * n;, and print in main if you want to see it.
6 · return also ends the function, immediately
return does two things at once. It hands back a value, and it stops the function on the spot. Anything written after it in the same path never runs:
int multiply(int a, int b) {
int result = a * b;
return result; // the function stops here
printf("The multiplication is complete.\n"); // never runs
}
Calling multiply(6, 7) gives 42 and prints no message. That printf is dead code. Compiled with clang -Wall on a Mac, there isn't even a warning: the compiler quietly ignores it. So the only protection is knowing the rule.
The "stops immediately" part is often exactly what you want. A function can have several returns, and the first one reached wins. You'll see this used deliberately in the prime check on the notes page, where it means "found a divisor, stop looking".
7 · The compiler reads top to bottom: prototypes
C compiles your file in one pass, from the top down. When it reaches a call, it must already know that the function exists, and what types go in and out. So this fails:
int main(void) {
printf("%d\n", add(2, 3)); // compiler hasn't seen add yet
return 0;
}
int add(int a, int b) { return a + b; }
On your Mac this is a hard error, not a warning:
error: call to undeclared function 'add'; ISO C99 and later do not support implicit function declarations
You have two fixes. You can put every function above main, which works but pushes main, the summary of the program, to the bottom. Or you can put a prototype at the top. A prototype is the header on its own, ending in a semicolon, with no body:
#include <stdio.h>
int add(int a, int b); /* declaration (prototype) — note the ; */
int main(void) {
int x = 10, y = 15;
printf("%d\n", add(5, 3));
printf("%d\n", add(x + 5, y * 2));
return 0;
}
int add(int a, int b) { /* definition — the real code */
return a + b;
}
8
45
Read the prototype as a promise to the compiler: "a function shaped like this exists; the real code is further down." The definition is where the promise is kept. The two headers must match exactly, same return type and same parameter types, or you get an error about conflicting types.
That second call is worth noticing too: add(x + 5, y * 2). The arguments are worked out first (15 and 30), and only those values are passed. That's why the answer is 45.
Check yourself: you write the prototype as int add(int a, int b), without the semicolon, above main. What happens?
It won't compile. Without the ;, the compiler can't tell a promise from the start of a definition, and it runs straight into int main on the next line. Clang is kind about this one: it points at the end of your prototype and says "expected ';' after top level declarator". Other compilers sometimes report it on the next line instead, so here's the habit to carry: if an error points at a perfectly good line, look at the line above it. A missing ; is the usual culprit.
8 · Functions that don't hand anything back: void
Some functions have no answer to return. They do something instead, like print a menu or a report. Their return type is void, meaning "nothing":
void printWelcomeMessage(void) {
printf("Welcome!\n");
} // no return needed: it just ends at the brace
You call it as a statement on its own, printWelcomeMessage();. There's nothing to catch in a variable. A void function ends when it reaches its closing brace, and control goes back to the caller.
A void function can still use a bare return; (no value) to leave early. It's useful for rejecting bad input before doing the real work:
void printResult(int marks) {
if (marks < 0 || marks > 100) {
printf("Invalid marks: %d\n", marks);
return; /* leave now — skip the rest */
}
if (marks >= 40) printf("%d: pass\n", marks);
else printf("%d: fail\n", marks);
}
Called with 72, −5 and 31 it prints:
72: pass
Invalid marks: -5
31: fail
void plus a value
Declare a function void and then write return c * 9.0 / 5.0 + 32; inside it, and clang stops you: "void function should not return a value". The opposite mistake also fails: catch the "result" of a void function in a variable (double f = celsiusToFahrenheit(c);) and you get "initializing 'double' with an expression of incompatible type 'void'". Both messages mean the same thing: the return type in the header doesn't match how you're using the function.
9 · C passes a copy: pass by value
Here is the idea that the stepper showed in step 8, stated properly. When you call a function, C copies the value of each argument into the function's parameter. The function then works on its copy. Whatever it does to that copy, the caller's original is untouched.
The lecture's analogy works well: your variable in main is the original document, and the function is handed a photocopy. It can scribble all over the photocopy and your original stays exactly as it was.
void modifyValue(int x) {
x = 100;
printf("inside: x = %d\n", x);
}
int main(void) {
int a = 10;
modifyValue(a);
printf("outside: a = %d\n", a);
return 0;
}
inside: x = 100
outside: a = 10
a into x. After that there is no connection: changing x to 100 cannot reach a.This is not a quirk you can switch off. C has no pass-by-reference at all. Some languages (C++, Pascal) let a function work directly on the caller's variable; C never added that. Every argument, every time, is copied.
"But scanf changes my variable!" It does, and you've been using the workaround without knowing it. That & in scanf("%d", &n) passes the address of n, meaning where it lives in memory. The address itself is copied, like any value, but with it scanf can reach back and write into the original. That is pointers, a later lecture. For now, just know the name, and know that without it a function can't change the caller's variables.
Check yourself: void doubleIt(int n) { n = n * 2; }. In main: int k = 7; doubleIt(k); printf("%d", k);. What prints, and how would you make k actually become 14 using only what you know so far?
It prints 7: doubleIt doubled its copy and then that copy was destroyed. The fix with today's tools: return the new value and let the caller store it. int doubleIt(int n) { return n * 2; }, then k = doubleIt(k);. The caller decides to overwrite its own variable; the function never touches it. This is the standard pattern until pointers arrive.
10 · Each function is a sealed room: scope and lifetime
A variable declared inside a function is local to that function. Its scope, the part of the program that can see it, is that function's body and nothing else. And its lifetime is the duration of one call: it's created when the function is called and destroyed when the function returns. You saw that in the stepper: addTen's box disappeared at step 7.
The lecture's picture is a soundproof room. Variables made inside can't be seen or touched from outside, and they vanish when the function ends. A useful consequence is that two functions can use the same variable name safely:
void processData(int y) {
int x = 50; /* this function's own x */
printf("processData: x = %d, y = %d\n", x, y);
}
int main(void) {
int x = 10; /* main's x — a different box */
processData(5);
printf("main: x = %d\n", x);
return 0;
}
processData: x = 50, y = 5
main: x = 10
Same name, two rooms, two boxes. That's exactly what makes functions safe to combine: when you write processData, you don't have to check which names main already used. Parameters count as locals too: y lives in processData's room.
Lifetime has one more consequence: a local doesn't remember anything between calls. Call processData twice and x is created fresh, set to 50, and destroyed, twice. If a function needs to keep a running total across calls, a plain local variable can't do it.
Check yourself: inside main, straight after calling processData(5), can you write printf("%d", y);?
No. It's a compile error: y is undeclared in main. y only exists inside processData, and only while it's running. By the time main's next line runs, it has already been destroyed.
11 · Global variables, and why to use them sparingly
A variable declared outside every function, usually near the top of the file, is global. Every function can see it and change it, and it lives for the whole run of the program:
int systemStatus = 100; /* GLOBAL — outside every function */
void triggerAlert(void) {
printf("%d\n", systemStatus);
systemStatus = 0;
}
int main(void) {
printf("%d\n", systemStatus);
systemStatus = 50;
triggerAlert();
printf("%d\n", systemStatus);
return 0;
}
100
50
0
Trace it line by line and you'll get those three numbers. The last one is the uncomfortable one: main set the status to 50, called a function, and got it back as 0. Nothing on main's line says the call might change it. In a program with twenty functions, when a global holds a wrong value, you have to read all twenty to find the culprit. That's the debugging nightmare the lecture warns about. A function that depends on a global also can't be copied into another program on its own, which breaks the black-box idea.
The third danger is the sneakiest, shadowing. Declare a local with the same name as a global, and inside that function the local hides the global completely:
int total = 0;
void addMarks(int m) {
int total = 0; /* oops: a NEW local total hides the global */
total = total + m;
}
int main(void) {
addMarks(40);
addMarks(35);
printf("total = %d\n", total);
return 0;
}
total = 0
Each call added to its own local total, which was then destroyed. The global never moved. clang -Wall compiles this without a single warning, because it's legal C. One stray int turned "update the global" into "make a new local".
Prefer parameters and return values. Information goes in through the parameter list and comes out through return, and every line that calls the function shows exactly what's flowing. Keep globals for true constants that never change, like a value of π.
12 · No functions inside functions
Every function you write sits at the top level of the file, side by side, outside every other function. You cannot write a full function definition inside main's body. Try it and clang says:
error: function definition is not allowed here
If you need a helper, write it next to main, with a prototype at the top, and call it from main. (GCC, a different compiler, allows nested functions as a non-standard extra. It isn't part of C and you'll never need it.)
13 · Writing a function you haven't seen before
The routine is three questions, answered on paper before you type. Take "Write a function that checks whether a number is even."
- What does it need? One whole number. So one parameter,
int n. - What does it give back? A yes/no answer. So return type
bool(from<stdbool.h>, as in Lab 4), orintholding 1 or 0. "Checks" means return; no printing inside. - What's a good name? Yes/no functions read best as questions:
isEven.
The header writes itself: bool isEven(int n). The body is one line, return n % 2 == 0;, because n % 2 == 0 already is true or false. Then write the prototype at the top, and one line in main that calls it and prints the answer, so you can test it with 4, 7 and 0.
Check yourself: design (don't code yet) a function for "compute the area of a rectangle". Header only.
double rectangleArea(double length, double width). It needs two measurements, gives back one number, and "compute" means return, not print. int would also be defensible if the problem promises whole numbers. What's not defensible is void plus a printf inside, because then the caller can't use the area in anything else.
Use it as a tutor, never as a ghostwriter. The handout examines your ability to verify generated code (lectures 40–41), and typing functions yourself is what builds the fluency the lab evaluations test.
- "Here is a C program with two functions [paste]. Don't fix anything. For each call, tell me which values are copied into which parameters, and what each function returns. Then ask me to predict the final output."
- "Give me four short C programs of the kind in Hanly & Koffman 8th edition chapter 3 (functions with input arguments) where a function changes its parameter, or a local shadows a global. Ask me to predict each output. Don't show answers until I've replied."
- "I wrote this function header for this task [paste both]. Tell me whether it should return or print, and whether the parameter types are right. Explain only; no rewritten code."
Where to go next
- Functions notes: the compressed map, three evaluation-level worked examples (the lecture's temperature converter, a globals-and-shadowing trace, a prime-checking function used from
main), and the full trap table. This is the page to have open during an open-book lab evaluation. - Predict-the-output drill: generated programs with function calls, pass-by-value and scope, self-marking. The fastest way to make the stepper's behaviour automatic.
- Arrays ladder: Lecture 8's topic, in small one-idea steps. Arrays and functions meet properly after pointers, but you'll use both in the same lab soon.
- Hanly & Koffman, 8th ed.: §3.4 §3.5 (functions without and with input arguments), and the "Scope of names" section in ch. 6. The lecture slides just say "Functions chapter", and the handout maps this block to ch. 6, so confirm the section numbers against your copy. If the instructor names sections, those win.