Assignment 3 — Functions, and one program in three files

Four small functions, each in its own file, then the same three functions moved into one program split across three files — and a Makefile that builds it.

Due Sep 16, 2026

Everything in A2 lived inside main. This week you take code out of main and give it a name, which is the move every language has and C spells slightly differently. Then you take those names out of the file altogether.

Two things here are genuinely new, and neither exists in Java or Python. The first is that C reads your file top to bottom, once, so a function has to be declared before it is called. The second is that gcc is not one program: a compiler runs, and then a linker runs, and they fail in different ways with different messages. Steps 1 and 7 are where you meet them.

Start a new directory for this assignment. Not the A2 one. Several files below have the same names as A2 files, and the last step builds everything in the directory at once.

You will hand in nine files: max.c, digits.c, grade.c, swap.c, numbers.h, numbers.c, main.c, Makefile and notes.txt.

If the week runs out, get to 1, 2 and 5

Those three are what Tuesday’s class starts from — it opens with a program already split across files, and reads much better from a chair where that has happened once already.

The rest is practice at the same two ideas, and practice is the point: gcc main.c numbers.c -o numbers should be something you type without stopping to think by the end of the week.

The lab machines in Ben Franklin 103 are the guaranteed environment. Compile everything there before you submit, even if you wrote it elsewhere.

Submit early and submit again. Brightspace keeps the latest version, so upload what you have on Friday and replace it on Tuesday.

AI: the course AI policy applies in full. No AI-generated code, and no asking a chatbot to solve these before the deadline. The optional problems at the end say when that changes.

The shapes, for reference

Keep this next to you. [DIS] 16.4 — Functions is the reading that explains it, and it is due before Tuesday.

A definition is the function itself:

int max(int a, int b) {
    if (a > b) {
        return a;
    }
    return b;
}

A prototype is the same first line with a semicolon instead of a body. It tells the compiler that a function by that name exists, what it takes and what it gives back — and nothing about how it works:

int max(int a, int b);

A function that gives nothing back has the return type void, and no return statement at the end:

void print_row(int n) {
    printf("%d\n", n);
}

Every function must be declared before the line that calls it. You have two ways to do that, and both are normal C: put the whole definition above main, or put a prototype above main and the definition below it. Step 1 is you trying both.

The order to write the pieces in is the design recipe — signature and purpose first, then the examples with their answers worked out, and the body last. It walks through max, which is step 1 below, so read it before you start.

Examples are checked with assert, not read off the screen. assert(x) does nothing when x is true and stops the program when it is false. It needs #include <assert.h>, and because it is silent on success, main ends with a line saying so:

assert(max(6, 10) == 10);
printf("all examples passed\n");

A failure names the expression and the line it is on:

max: max.c:12: main: Assertion `max(6, 10) == 10' failed.
Aborted (core dumped)

That is the program stopping on purpose, not crashing.

Part A — four functions, four files

  1. max.c — and the error you are supposed to cause. Write int max(int a, int b) returning the larger of the two, and a main that checks it on three examples:

    #include <stdio.h>
    #include <assert.h>
    
    int main() {
        assert(max(6, 10) == 10);
        assert(max(11, 3) == 11);
        assert(max(4, 4) == 4);
        printf("all examples passed\n");
    }

    Put both #include lines in before you start. Forget the second one and gcc complains that assert is implicitly declared — which is the very error this step is about, on the wrong name, and it will send you looking in the wrong place.

    Do it in this order, and compile with gcc -Wall max.c -o max after each:

    1. Definition below main first. This is the version that goes wrong. Copy the message gcc printed into notes.txt, exactly as it appeared. Then say whether it was a warning or an error — the word is in the message — and whether you got a working max program out of it anyway.

    2. Now move the whole definition above main. Compile again. What changed?

    3. Now move it back below main, and add a prototype above main instead. Compile again.

    Two of those three work. In notes.txt, say in one or two sentences what the compiler needed, given that in (a) the function was sitting right there in the file where you could see it.

  2. digits.c — your A2 program, with the work given a name. Write int count_digits(int n) which takes a number and returns how many digits it has. main reads a number with scanf and prints the count.

    The counting loop is A2’s — divide by 10 until you reach 0. Copy it in and put it inside the function. No printf and no scanf inside count_digits: it takes a number and returns a number, and that is all it does.

    Run it on 90210, 7 and 0. If 0 is wrong, it is wrong in the same way it was in A2.

    If you never got A2’s digits.c working, write it now. The loop is n = n / 10 and a counter, running until n is 0.

  3. grade.c — a function that returns a letter. Write char letter_grade(int score) which returns 'A', 'B', 'C', 'D' or 'F'. Same boundaries as A2: 90 and above is A, 80–89 is B, 70–79 is C, 60–69 is D, below 60 is F.

    One if / else if / else chain, as before. A character constant is in single quotes, 'A', it compares with == like any other number, and printf prints one with %c.

    main checks it on 90, 89, 60 and 59:

    assert(letter_grade(90) == 'A');
    assert(letter_grade(89) == 'B');
    assert(letter_grade(60) == 'D');
    assert(letter_grade(59) == 'F');

    Now add a fifth, well away from any boundary: assert(letter_grade(20) == 'F');.

    Then read what gcc -Wall says about grade.c. In notes.txt: did the fifth assertion pass, and did gcc say anything? If your chain has no final else, the assertion can pass anyway and the warning is the only thing that tells you. That is A2’s argument one step worse: a test passing is not the same as being right.

  4. swap.c — the one that does not work. Write void swap(int a, int b) whose body swaps a and b using a third variable. In main:

    int x = 3;
    int y = 8;
    printf("before: x = %d, y = %d\n", x, y);
    swap(x, y);
    printf("after:  x = %d, y = %d\n", x, y);
    assert(x == 8);
    assert(y == 3);

    Compile it and run it. gcc -Wall says nothing at all about this program — it compiles clean, and the only thing that objects is the assertion, at run time.

    In notes.txt: what did the two lines print, what did you expect, and which assertion failed? Then, in two or three sentences, your best guess at what swap actually swapped. There is a name for this and the reading has it; you do not need the name to describe what happened.

    Do not try to fix it. It cannot be fixed with anything you have been shown yet, and the tool that fixes it is week 9.

Part B — one program, three files

Now the three functions from steps 1–3 move into a program of their own. Nothing new is being computed. What changes is where the code lives.

  1. Make the header and the source file.

    1. numbers.h holds the three prototypes and nothing else:
    // Include guard: magic that stops this file being pasted in twice.
    // Name it after the file:  numbers.h -> NUMBERS_H
    #ifndef NUMBERS_H
    #define NUMBERS_H
    
    int max(int a, int b);
    int count_digits(int n);
    char letter_grade(int score);
    
    #endif

    Copy the comment too. The name has to differ from every other header’s or the guards start silencing each other, and deriving it from the file name is what guarantees that — so the next header you write is not a second NUMBERS_H.

    1. numbers.c holds the three definitions — copied from steps 1, 2 and 3 — and no main. Its first line is #include "numbers.h". Note the quotes: angle brackets mean a header that came with the system, quotes mean a file of mine, in this directory.

    2. main.c holds only main, and starts with both #include <stdio.h> and #include "numbers.h". It calls all three functions and prints the results. What it prints is up to you; make it obvious from the output which function produced which line.

    Build it with:

    gcc -Wall main.c numbers.c -o numbers

    Done when ./numbers runs and you can see all three functions in its output.

  2. Find out what #include actually did. #include is not Java’s import and it is not Python’s. It is a paste: the contents of the named file are dropped into your file, in place of that line, before the compiler sees any of it. You can watch this happen:

    gcc -E main.c > main.i

    -E stops after the pasting and prints the result. Open main.i and look at how long it is.

    In notes.txt: roughly how many lines is main.i, and how many is main.c? Find your three prototypes in there — where are they, relative to main? Then say in one sentence where the extra thousand-odd lines came from.

    Now look again at the #ifndef and #define at the top of numbers.h. If #include pastes, then a header included twice would paste twice. Those three lines are what stops that. You do not need them yet with only one header — they are there because every header you will ever read has them, and now you know why.

  3. Break the build on purpose — twice. Both of these are one command each. Record the exact message in notes.txt both times.

    1. Leave a file out:
    gcc -Wall main.c -o numbers
    1. Put it back, but take the include out. Restore the full command from step 5, then delete the #include "numbers.h" line from main.c and build again.

    Then, in three or four sentences: the two messages are different in kind, not just in wording. One of them names a line in your file and one of them does not. Which is which, and what does that tell you about how many separate programs gcc ran on your behalf? Put the #include back when you are done.

  4. Write a Makefile, and submit. In the same directory, a file called exactly Makefile:

    numbers: main.c numbers.c numbers.h
    	gcc -Wall -o numbers main.c numbers.c
    
    clean:
    	rm -f numbers

    The whitespace before gcc and before rm must be a single tab character, not spaces. This is the single most common way a Makefile fails, it looks completely fine on screen, and the error you get says missing separator. In nano, a tab is the Tab key and it stays a tab.

    Then run make, and make clean, and make again. Put the output of all three in notes.txt.

    Finally: touch nothing, and run make a second time in a row. It says something different. What, and why? One sentence. [DIS] 17.5 — make and Makefiles is the reading for this, and section 17.5.6 is the one about the errors.

    Then submit all nine files on Brightspace. Compile on a lab machine one last time first.

Grading

Your programs are marked on compiling and running, and notes.txt on answering the questions in steps 1, 2, 3, 4, 6, 7 and 8.

As in A1 and A2, written answers are graded on being honest about what you saw, not on being right. “It printed after: x = 3, y = 8 and I expected 8 and 3, and I do not know why” is worth full marks. A confident description of something that did not happen is not.

A file that does not compile earns nothing for the parts that would have worked, so if you are out of time, hand in the version that builds rather than the version you were in the middle of.

Presentation

One of you will be asked on Thursday, September 17 to put your files on the projector and walk the class through them. You will not be told in advance. There are ten of us and about twelve assignments over the term, so plan on presenting about once — twice at the most. See the syllabus on how presentation is graded.

What else to do (optional part)

Ungraded. No points, and skipping them costs nothing.

  1. Give numbers.c a fourth function of your own and add it to numbers.h. Then add it to main.c without rebuilding, and see what happens when you run the old ./numbers.

  2. Add a prototype to numbers.h for a function you never define, and call it from main.c. Which of the two failures from step 7 do you get?

  3. Take the multiplication table from A2 and turn it into void print_table(int size). Then call it three times from one main, with 3, 5 and 9. This is the argument for functions in one line of code.

  4. Change the gcc line in your Makefile to something misspelled, run make, and read what it tells you. Then replace the tab in front of it with four spaces and run make again. Those are the two Makefile errors you will actually meet.

  5. Work the Functions set in Dive Into Systems: Exercises. They run in the browser and mark themselves.

  6. After the deadline has passed — and only then, per the AI policy — paste your step 4 swap.c into a chatbot and ask why it does not swap anything. Does the answer make sense to you yet? Write one line on which part of it you could not follow; that part is week 9.