Skip to content
VLSI Mentor

C Part I · Module 5 · Functions and Program Decomposition

Why Functions Exist

Every program in this curriculum so far has been a single block of statements inside main, and they have been getting longer. That was fine while each one demonstrated a single operator or a single construct, and it stops being fine the moment a program does more than one job. This module introduces the unit C provides for that, and this first chapter is about why it exists — which turns out not to be the reason most introductions give. The usual explanation is that functions save you from retyping code, which is true, incidental, and not nearly enough to justify how central they are. The real reason is about what a reader can check without reading everything, and the clearest way to see it is to take one working program and write it twice.

Foundation14 min readCPart IFundamentalsFunctions

Part I · Module 5 · Chapter 5.1 · Why Functions Exist

1. A Program That Has Outgrown One Block

Here is a program built entirely from Modules 2 to 4. It takes a sensor reading and a status word, rejects the reading if it is out of range, counts how many fault bits are set, rejects it again if there are too many, and otherwise reports a severity level.

Azvya Education Pvt. Ltd.VLSI Mentor
one job per paragraph, all in one block
#include <stdio.h>

int main(void)
{
    unsigned int raw = 3900u;
    unsigned int status = 0x0Cu;

    int accepted = 0;
    int fault_count = 0;

    if (raw > 4095u) {
        printf("reading %u is out of range\n", raw);
    } else {
        unsigned int bits = status;
        while (bits != 0u) {
            fault_count = fault_count + (int)(bits & 1u);
            bits = bits >> 1;
        }

        if (fault_count > 2) {
            printf("too many faults (%d), rejecting reading %u\n", fault_count, raw);
        } else {
            accepted = 1;
            if (raw >= 3800u) {
                printf("critical: %u (%d faults)\n", raw, fault_count);
            } else if (raw >= 3000u) {
                printf("warning: %u (%d faults)\n", raw, fault_count);
            } else {
                printf("normal: %u (%d faults)\n", raw, fault_count);
            }
        }
    }

    printf("accepted %d\n", accepted);

    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
critical: 3900 (2 faults)
accepted 1

Nothing here is wrong. Every construct is one you have used. It compiles cleanly and gives the right answer.

Now try to answer a question about it: is the bit-counting loop correct?

To be sure, you have to find it inside the else branch, check that bits was initialised from status and not from something else, confirm the shift is on an unsigned value, confirm fault_count was zero before the loop started — and that last one means looking at a line thirteen lines further up, outside the branch, where fault_count is declared next to an unrelated variable. Four separate places, to check one idea.

And having checked it, you have learned nothing you can reuse. Ask whether the severity thresholds are right and you start again, this time holding in mind that you are inside two nested else branches and that reaching this code at all required the earlier checks to pass.

That is the actual problem. Not length, and not repetition — there is no repetition in that program at all. The problem is that no part of it can be understood without the rest of it.

2. The Same Program, in Pieces

Azvya Education Pvt. Ltd.VLSI Mentor
the same logic, same inputs, same output
#include <stdio.h>

int reading_is_valid(unsigned int raw)
{
    return raw <= 4095u;
}

int count_set_bits(unsigned int word)
{
    int set = 0;
    while (word != 0u) {
        set = set + (int)(word & 1u);
        word = word >> 1;
    }
    return set;
}

void report_level(unsigned int raw, int fault_count)
{
    if (raw >= 3800u) {
        printf("critical: %u (%d faults)\n", raw, fault_count);
    } else if (raw >= 3000u) {
        printf("warning: %u (%d faults)\n", raw, fault_count);
    } else {
        printf("normal: %u (%d faults)\n", raw, fault_count);
    }
}

int main(void)
{
    unsigned int raw = 3900u;
    unsigned int status = 0x0Cu;

    int accepted = 0;

    if (!reading_is_valid(raw)) {
        printf("reading %u is out of range\n", raw);
    } else {
        int fault_count = count_set_bits(status);

        if (fault_count > 2) {
            printf("too many faults (%d), rejecting reading %u\n", fault_count, raw);
        } else {
            accepted = 1;
            report_level(raw, fault_count);
        }
    }

    printf("accepted %d\n", accepted);

    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
critical: 3900 (2 faults)
accepted 1

Byte-identical. Same inputs, same logic, same result — so nothing about the comparison depends on one version doing more than the other. The only thing that changed is how much you have to read to check any one claim.

Ask the same question now: is the bit-counting correct? The answer is in count_set_bits, which is nine lines long, reads one parameter, declares one local, and returns. Nothing outside it can affect it and nothing inside it can affect anything outside it. You can check it completely without knowing that a sensor exists.

And main is now readable as a sentence: if the reading is not valid, say so; otherwise count the faults, and if there are not too many, accept it and report the level. That sentence is what the program does. In §1 that sentence existed only in the author's head.

3. The Explanation That Is Not Quite Right

The usual first answer is:

A function lets you write code once and use it many times.

That is true, and it is genuinely useful — but it is not what happened above. Look again at the decomposed program: count_set_bits is called exactly once. So is reading_is_valid. So is report_level. By the repetition argument, none of the three extractions was worth doing.

They were all worth doing. So the repetition argument is not the reason.

A function is a named unit of execution with a boundary. The reuse is a consequence of having a boundary. The boundary is the point.

Three things that boundary buys you, each of which the decomposed program demonstrates.

You can name the idea. count_set_bits(status) says what the loop is for. A comment saying the same thing can drift out of date while the code changes beneath it; a function name is attached to the thing it names and travels with every call.

You can reason locally. The variable set inside count_set_bits cannot be read or modified by anything outside that function, and word is the function's own. When you are reading those nine lines, the rest of the program cannot surprise you. (Exactly which names are visible where is Chapter 5.6's subject; what matters here is that a boundary exists.)

You can exercise one piece. This is the one that makes the difference in practice, and it deserves its own demonstration.

4. Checking One Piece Without Running the Program

Because count_set_bits has a boundary, you can feed it values the full program would never produce and see what it does:

Azvya Education Pvt. Ltd.VLSI Mentor
exercising one function directly
#include <stdio.h>

int count_set_bits(unsigned int word)
{
    int set = 0;
    while (word != 0u) {
        set = set + (int)(word & 1u);
        word = word >> 1;
    }
    return set;
}

int main(void)
{
    printf("0x00 -> %d\n", count_set_bits(0x00u));
    printf("0x01 -> %d\n", count_set_bits(0x01u));
    printf("0x0C -> %d\n", count_set_bits(0x0Cu));
    printf("0xFF -> %d\n", count_set_bits(0xFFu));

    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
0x00 -> 0
0x01 -> 1
0x0C -> 2
0xFF -> 8

Four cases, including the empty one and the full one — exactly the boundary checking Chapter 4.7 argued for. The 0x00 case is the interesting one: it confirms the loop handles no bits set correctly, and in §1's version there was no way to try it without editing the program and recompiling the whole thing.

This is what "testable" means, and it is not a separate activity from writing the code. It is a property a function has and a block of statements in the middle of main does not.

Chapter 5.9 returns to this as a design discipline. For now it is enough to see that the boundary is what made it possible.

5. A First Look at the Shape

You have been reading functions for four chapters of this module without being told what the syntax means. Here is the smallest one:

Azvya Education Pvt. Ltd.VLSI Mentor
the smallest useful function
#include <stdio.h>

int is_even(int value)
{
    return value % 2 == 0;
}

int main(void)
{
    printf("4 is even: %d\n", is_even(4));
    printf("7 is even: %d\n", is_even(7));

    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
4 is even: 1
7 is even: 0

Four parts, named here and made precise in Chapter 5.2:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int        is_even    (int value)      { return value % 2 == 0; }
↑          ↑           ↑                 ↑
the type   the name    what it needs     what it does
it gives   you call    to be given
back       it by

And is_even(4) in the printf is a call. It is an expression — it produces a value of type int, exactly as Chapter 3.1 described any expression doing, which is why it can sit where a value is required.

Notice that main has had this shape all along. int main(void) is a function definition: it gives back an int, it is named main, and (void) says it takes nothing. Chapter 1.4 called those pieces provisional and promised they would be made precise later. This module is later.

6. Two Beliefs Worth Correcting Now

The second belief is about cost.

Breaking a program into functions makes it slower.

A call is not free. The implementation has to arrange for the arguments to be available to the function and for execution to resume afterwards, and Chapter 5.7 shows the machinery that usually does it. So the concern is not imaginary.

It is almost always the wrong thing to worry about, for three reasons worth keeping separate.

The cost is small, and it is a fixed small cost per call, not something that grows with program size.

Compilers commonly substitute a function's body at the call site when it is worth doing, which removes the cost entirely. That is common implementation behaviour, not a guarantee in C — the standard describes what the program computes, not how a compiler gets there, and Chapter 1.3 established why keeping those two apart matters. Do not write code that is only correct if an optimisation happens.

Programs are almost never slow where you guess they are. Trading a readable structure for an unmeasured performance theory is a bad trade, and it is the one that produces the thousand-line functions that nobody can safely change.

7. What a Function Is Not — Yet

Two things are deliberately not claimed in this chapter, because believing either of them early causes real trouble.

A function does not share your variables. In §2, count_set_bits has a parameter called word and main passes status. Those are not the same object, and the function cannot reach into main and change anything. Chapter 5.3 makes this precise, and it is the single most consequential rule about calls in C.

A function does not pause and resume. A call runs the function to completion and then execution continues after the call. Nothing is left running behind you.

8. What You Should Be Able to Say Now

  1. A long main is hard to work with because no part of it can be checked without the rest, not because it is long.
  2. Decomposing a program can leave its behaviour byte-identical and still improve it.
  3. A function is a named unit of execution with a boundary.
  4. The boundary gives you a name, local reasoning, and the ability to exercise one piece.
  5. Reuse is a consequence of having a boundary, not the reason for one — a function called once can be the right decision.
  6. Extract when a block has one describable job; if the name needs the word and, it is probably two functions.
  7. A call has a small cost; compilers commonly remove it by substituting the body, which is implementation behaviour rather than a C guarantee.
  8. A call is an expression that produces a value, so it can go where a value is required.
  9. int main(void) has been a function definition all along.
  10. A function cannot change the caller's variables — Chapter 5.3's subject.

9. Exercises

Exercise 1 — find the dependencies. In §1's program, list every line you must read to convince yourself the bit-counting loop is correct. Then do the same for count_set_bits in §2. State the two numbers.

Exercise 2 — name the jobs. §1's main does four separable things. Name each in a short verb phrase, and say which of them §2 turned into a function and which it left in main. Then suggest why the remaining one was left there.

Exercise 3 — predict the output. Do not compile yet.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#include <stdio.h>

int triple(int n)
{
    return n * 3;
}

int main(void)
{
    printf("%d\n", triple(5));
    printf("%d\n", triple(0));
    printf("%d\n", triple(triple(2)));
    return 0;
}

The third line is the interesting one. Explain the order in which things happen, using the fact that a call is an expression.

Exercise 4 — judge the extraction. For each description, say whether you would make it a function and give your reason in terms of §3's three benefits, not in terms of line count.

  • a two-line calculation used in eleven places
  • a thirty-line block used once, which you can name validate_frame_header
  • a four-line block used once, which you can only name do_the_middle_part
  • a single printf used in six places with different arguments each time

Exercise 5 — choose the boundary cases. §4 exercised count_set_bits at 0x00, 0x01, 0x0C and 0xFF. Explain what each one is testing. Then give one more input you would add, and say what it would catch that these four do not.

Exercise 6 — answer the objection. A colleague says the decomposed version in §2 "adds three function calls for nothing, since each is used once." Write a two-sentence reply that does not mention performance.

Exercise 7 — spot the wrong reason. Another colleague extracts a function because the file was "getting long", and the new function takes six parameters and is called from one place. Say what has probably gone wrong, and what question they should have asked before extracting.

Exercise 8 — decompose one. Take Chapter 4.8's staged setup program, and without writing any code, name the functions you would extract and give each a one-line description of its job. Say which parts you would leave in main and why.

10. Summary

  • A program outgrows one block when no part of it can be understood independently — length is the symptom, not the cause.
  • The same logic decomposed can produce identical output and still be a better program.
  • A function is a named unit of execution with a boundary; the boundary is what you are buying.
  • It gives you a name that cannot drift, local reasoning, and the ability to check one piece against inputs the whole program would never produce.
  • Reuse is a side effect. A function called once is often the right call.
  • The test for extraction is one describable job, not a line count or a call count.
  • Calls cost a little; compilers commonly remove that cost, which is implementation behaviour, not a C promise.
  • A call is an expression, and main has always been a function.

You can now say why you would want one. What you cannot yet say is what the pieces of that first line mean, what void is doing in main's parameter list, what happens to a value when you pass it, or what return actually does to the expression that contained the call. The next chapter takes the shape apart one piece at a time and establishes exactly what a call transfers.

Standards & specifications

Governing standard
ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)

Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C Programming curriculum.