Skip to content
VLSI Mentor

C Part I · Module 5 · Functions and Program Decomposition

Defining and Calling a Function

The previous chapter showed what a function buys you and deliberately left the syntax vague. This one takes it apart. There are four pieces to a definition and each of them is a decision, including one that looks like pure punctuation and is not — the difference between empty parentheses and the word void inside them changes what the compiler is willing to let you do, and the C23 standard changed the rules around it in a way worth seeing demonstrated rather than described. The second half of the chapter is about the call itself: what is handed over, in what order it happens, what comes back, and which parts of that sequence C guarantees and which it deliberately leaves open.

Foundation17 min readCPart IFundamentalsFunctions

Part I · Module 5 · Chapter 5.2 · Defining and Calling a Function

1. The Four Parts

Azvya Education Pvt. Ltd.VLSI Mentor
a function definition, labelled
int is_even(int value)
{
    return value % 2 == 0;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int          is_even        (int value)          { ... }
↑            ↑              ↑                    ↑
return type  function name  parameter list       body

The return type says what type of value a call to this function produces. Here, int. This is the same int you have been declaring objects with since Module 2 — a call to is_even is an expression of type int, and everything Chapter 3.12 said about conversions applies to it.

The name is how you call it. The naming rules are the same as for variables.

The parameter list declares the objects the function needs to be given. int value is a declaration: it creates an object of type int named value, which exists while the function runs. If a function needs two things, the parameters are separated by commas and each needs its own type:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int larger_of(int a, int b)      /* correct */
int larger_of(int a, b)          /* not C — b has no type */

The body is a block, exactly the kind Chapter 4.1 introduced. Statements run in it top to bottom, and return ends the function.

Together, the return type, the name and the parameter types are the function's signature — the part a caller has to agree with.

2. Functions That Give Nothing Back

Some functions exist entirely for their effect. report_level in Chapter 5.1 printed something; there is no result to hand back. The return type for that is void:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
void report_sample(unsigned int code, unsigned int millivolts)
{
    printf("code %4u -> %4u mV\n", code, millivolts);
}

void is not a type you can make objects of. It is how C says there is no value here. A call to a void function is still an expression, but one with no value, so there is nothing to use:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int x = show(5);          /* show returns void */
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: initializing 'int' with an expression of incompatible type 'void'

A void call therefore appears as a statement on its own:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    report_sample(code, mv);

3. The Parameter List Is Not Optional Punctuation

A function that takes nothing is written (void):

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int get_limit(void)
{
    return 10;
}

It is tempting to write () instead, and it is tempting to assume the two mean the same thing. On this curriculum's C17 baseline they do not, and the difference is not cosmetic.

int get_limit(void) declares a function taking no parameters. int get_limit() declares a function whose parameters are unspecified — it is not saying no arguments, it is saying I am not telling you. And a compiler cannot check calls against information it was not given.

Here is the same file, compiled twice. The function takes nothing, and the call passes three arguments:

Azvya Education Pvt. Ltd.VLSI Mentor
a call that should obviously be wrong
#include <stdio.h>

int get_limit() { return 10; }

int main(void)
{
    printf("%d\n", get_limit(1, 2, 3));
    return 0;
}

Built as C17:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: a function declaration without a prototype is deprecated in all
    versions of C [-Wstrict-prototypes]
warning: too many arguments in call to 'get_limit'
warning: passing arguments to 'get_limit' without a prototype is deprecated in
    all versions of C and is not supported in C23 [-Wdeprecated-non-prototype]

Warnings only. The program builds, runs, and prints 10.

The identical file, built as C23:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: too many arguments to function call, expected 0, have 3

An error. The program does not build.

And with (void) instead of (), C17 rejects it too, immediately:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: too many arguments to function call, expected 0, have 3

4. What a Call Transfers

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int answer = is_even(7);

Five things happen, in this order:

  1. The argument expressions are evaluated. 7 here; in general any expression. Each produces a value, with a type.
  2. Those values are given to the function's parameters. The parameter value is an object belonging to this call, and it starts out holding the argument's value.
  3. Control leaves the call site and the function body runs, to completion.
  4. return ends the function and names the value to hand back.
  5. Control resumes immediately after the call, and the call expression now is that value, with the function's return type.
Six stages. First, the argument expressions are evaluated, each producing a value. Second, those values become the starting values of the function's parameters, which are objects belonging to this call. Third, control leaves the call site and enters the function body. Fourth, the body runs to completion and a return statement names the value to hand back. Fifth, control resumes at the point immediately after the call. Sixth, the call expression now has that value and the function's return type.A call, step by step01EVALUATE THE ARGUMENT EXPRESSIONSeach produces a value, with a type02THE PARAMETERS START OUT HOLDING THOSE VALUESparameters are objects belonging to this call03CONTROL ENTERS THE FUNCTION BODYthe call site waits04THE BODY RUNS; RETURN NAMES THE VALUEthe function finishes completely05CONTROL RESUMES JUST AFTER THE CALLnothing is left running behind you06THE CALL EXPRESSION IS NOW THAT VALUEwith the function's return type
Figure 1 — what a call transfers, in order. Arguments are evaluated first and their values are what the parameters start out holding; control then moves into the body, runs it to completion, and comes back to the point immediately after the call, carrying the returned value. The call expression takes on that value and the function's return type, which is why a call can appear anywhere a value of that type is allowed.

Step 2 is the one with consequences, and Chapter 5.3 is entirely about it. For now, note the precise wording: the parameter starts out holding the argument's value. It is an object in its own right, not the caller's object under a new name.

Step 6 is why a call can go wherever a value of that type can go:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    printf("%d\n", is_even(4));        /* as an argument */
    int answer = is_even(4);           /* as an initialiser */
    if (is_even(count)) { ... }        /* as a condition */
    int n = is_even(4) + is_even(6);   /* inside a larger expression */

Nested calls follow from the same rule — the inner call has to produce its value before the outer one can be given it:

Azvya Education Pvt. Ltd.VLSI Mentor
which runs first?
#include <stdio.h>

int double_it(int n)
{
    printf("  double_it(%d) running\n", n);
    return n * 2;
}

int add_one(int n)
{
    printf("  add_one(%d) running\n", n);
    return n + 1;
}

int main(void)
{
    printf("start\n");
    int result = add_one(double_it(5));
    printf("result %d\n", result);

    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
start
  double_it(5) running
  add_one(10) running
result 11

double_it(5) is an argument of add_one, so by step 1 it must be evaluated before add_one can run. Its value, 10, is what add_one's parameter starts out holding.

5. What C Does Not Promise About the Order

Step 1 says the arguments are evaluated. It does not say in which order, and neither does C.

When a call has several arguments, the order in which they are evaluated is unspecified. Nothing requires left to right, and nothing requires a compiler to be consistent about it.

Chapter 3.11 established this for operands generally; function arguments are the place you are most likely to trip over it, because it is natural to read a call as happening left to right.

Azvya Education Pvt. Ltd.VLSI Mentor
printing from inside the arguments
#include <stdio.h>

int announce(int which)
{
    printf("  evaluating argument %d\n", which);
    return which;
}

int main(void)
{
    printf("before the call\n");
    printf("result %d and %d\n", announce(1), announce(2));

    return 0;
}

On clang 21, this run produced:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
before the call
  evaluating argument 1
  evaluating argument 2
result 1 and 2

That is an observation, not a rule. A different compiler, or the same compiler with different settings, may evaluate announce(2) first and print those two lines in the other order — and it would be equally correct. What is guaranteed is that both arguments are fully evaluated before printf runs, which is why result 1 and 2 always appears last and always pairs the right values.

The engineering consequence is a short one:

Do not write argument expressions whose effects depend on each other. If the order matters, evaluate them into named objects on separate lines first, where the order is written down.

Chapter 3.11 also covered the harsher case — two arguments that modify the same object are not merely unspecified but undefined, and the standard imposes no requirements on what happens at all.

6. Where a Function Must Appear

A function must be declared before it is called. In a single file, the simplest way to satisfy that is to define it above the first call:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int helper(int n) { return n + 1; }      /* defined first */

int main(void)
{
    return helper(3);                    /* then called */
}

Reverse the order and C99 and later reject it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: call to undeclared function 'helper'; ISO C99 and later do not support
    implicit function declarations [-Wimplicit-function-declaration]

The message is worth reading carefully, because it describes a rule that used to be different. Very old C would assume an undeclared function returned int and accept whatever you passed it. That assumption caused enough damage that C99 removed it, and this is now an error rather than the silent guess it once was.

Defining above the first call is a real solution and it is what every example in this chapter does. It is not the general one — it fails as soon as two functions need to call each other, and it forces you to order a file by the compiler's needs rather than by what a reader wants to meet first. Chapter 5.5 introduces the mechanism that removes the constraint.

7. A Realistic Set of Functions

Azvya Education Pvt. Ltd.VLSI Mentor
three functions and a short main
#include <stdio.h>

unsigned int clamp_code(unsigned int code)
{
    if (code > 4095u) {
        return 4095u;
    }
    return code;
}

unsigned int code_to_millivolts(unsigned int code)
{
    return (code * 3300u) / 4095u;
}

void report_sample(unsigned int code, unsigned int millivolts)
{
    printf("code %4u -> %4u mV\n", code, millivolts);
}

int main(void)
{
    unsigned int raw = 2048u;

    unsigned int code = clamp_code(raw);
    unsigned int mv = code_to_millivolts(code);
    report_sample(code, mv);

    report_sample(clamp_code(5000u), code_to_millivolts(clamp_code(5000u)));

    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
code 2048 -> 1650 mV
code 4095 -> 3300 mV

Several decisions in that program are worth naming.

clamp_code and code_to_millivolts return a value and have no effect; report_sample has an effect and returns nothing. Keeping those two kinds of function apart is one of the most useful habits in the module, and Chapter 5.9 makes the case for it properly.

The parameter types are unsigned int throughout, matching the values being passed, so no conversion happens at the call. If clamp_code took an int and you passed an unsigned int, Chapter 3.12's conversions would apply at step 2 — silently. Argument passing is one more place where types matter.

The arithmetic in code_to_millivolts multiplies before dividing, which Chapter 3.3 explains: integer division truncates, so dividing first would throw away the precision the multiplication needed.

And the last line is deliberately written badly. clamp_code(5000u) appears twice, doing the same work twice, and the two arguments are evaluated in an order §5 says you cannot predict. It happens to be correct here because neither call has any effect — but the first version, which names code and mv on separate lines, is the one to copy.

8. A Misconception About Names

9. What You Should Be Able to Say Now

  1. A definition has four parts: return type, name, parameter list, body. The first three are its signature.
  2. Each parameter needs its own type; (int a, b) is not C.
  3. void as a return type means the call produces no value — using one is a compile error.
  4. (void) means no parameters. () in C17 means unspecified, so calls are not checked.
  5. C23 redefined () to mean (void) — the same file warns under C17 and is an error under C23.
  6. A call evaluates the arguments, gives their values to the parameters, runs the body, and resumes after the call.
  7. A call is an expression with the function's return type, so it goes anywhere that type is allowed.
  8. The order in which arguments are evaluated is unspecified — but all of them are evaluated before the function runs.
  9. A function must be declared before it is called; implicit declarations were removed in C99.
  10. Parameter names are internal. Only the signature crosses the boundary.

10. Exercises

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

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

int half(int n)
{
    return n / 2;
}

int main(void)
{
    printf("%d\n", half(10));
    printf("%d\n", half(7));
    printf("%d\n", half(half(20)));
    return 0;
}

The second line needs Chapter 3.3. Say which rule gives the answer.

Exercise 2 — fix the signatures. Each of these is rejected or unsafe. Say what is wrong and write the corrected line.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int add(int a, b)
void report(int n) { return n; }
int get_count() { return 7; }
int scale(int n) { return n * 2 }

Exercise 3 — explain the two builds. §3's program warns under C17 and fails under C23. Explain what () means in each standard, and say why the C17 version was allowed to run despite being obviously wrong.

Exercise 4 — trace the nesting. Give the exact output, in order, and explain which rule determines it.

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

int f(int n) { printf("f(%d)\n", n); return n + 1; }
int g(int n) { printf("g(%d)\n", n); return n * 10; }

int main(void)
{
    printf("answer %d\n", f(g(2)));
    return 0;
}

Exercise 5 — classify the behaviour. For each, say whether the result is guaranteed, unspecified, or undefined, and why.

  • printf("%d %d\n", f(1), f(2)); where f prints and returns its argument
  • printf("%d %d\n", i++, i++);
  • int x = f(1) + f(2); where f has no effects at all
  • int y = f(g(3));

Exercise 6 — repair the call site. §7's last line calls clamp_code(5000u) twice. Rewrite those two lines so the clamping happens once, and give two reasons the rewrite is better — one about duplicated work and one about §5.

Exercise 7 — find the error. This does not compile. Quote what the compiler objects to, explain the rule, and give two different repairs — one that moves code and one that does not.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int main(void)
{
    printf("%d\n", square(4));
    return 0;
}

int square(int n)
{
    return n * n;
}

Exercise 8 — write two functions. Write int clamp(int value, int low, int high) returning the value limited to the range, and void describe(int value, int clamped) printing both. Call them from main for values below, inside and above the range. State what each function returns for value exactly equal to low and to high, before you compile.

11. Summary

  • A function definition is return type, name, parameter list, body; the first three form the signature.
  • void means no value here — as a return type it makes the call unusable as a value.
  • (void) is not decoration. In C17 () means unspecified parameters and disables argument checking; C23 redefined it to mean no parameters.
  • A call evaluates the arguments, hands their values to the parameters, runs the body, and resumes after the call.
  • A call is an expression of the function's return type.
  • Arguments are all evaluated before the body runs, but their relative order is unspecified.
  • A function must be declared before it is called — implicit declarations have been gone since C99.
  • Parameter names are private to the function. Only the signature is shared.

Step 2 of the call sequence was stated carefully and then left alone: the parameter starts out holding the argument's value. That phrasing was chosen over the more natural-sounding alternative — that the function is given your variable — because the difference between the two is the single most consequential rule about calls in C, and getting it wrong produces a class of bug that looks like the language is broken. The next chapter is about nothing else.

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.