---
title: Functions
source: 'https://academia.sh/en/courses/c-fundamentals/functions'
course: 'C Fundamentals'
language: en
updated: '2026-09-26T03:02:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures that a call being bound to a definition in another unit is resolved at the linker stage'
    - 'Shows the separation of declaration from definition and that parameter passing is always by copy'
    - "Distinguishes with an example how the return value's type determines the caller's interpretation"
---

# Functions

A call being bound to a definition in another unit being resolved at the linker stage; the declaration–definition distinction, parameter passing always being by copy, and the return value's type determining the caller side's interpretation.

The Control Flow lesson dealt with a question that always stayed inside the same
function: which branch gets chosen. This lesson lifts that boundary and measures how a
call crosses a function's **boundary**. When a function is called, three separate
questions are resolved at once: which definition the call is bound to, how the arguments
reach the function, and how the returned value is interpreted — all three decided at the
same time, but for different reasons. The first belongs to the linker, because it
requires more than one unit; the other two are decisions the compiler makes by looking at
a single unit — even without knowing exactly where a call will be bound, the compiler can
work out from the declaration how the arguments will be copied and how the return will be
interpreted.

The Programming Fundamentals course (M01/K02) already established the concept of a
function with Python; there, a function only needed to be defined before it could be
called, and there was no declaration–definition distinction. C's counterpart is measured
here: a call can be compiled by relying only on a declaration, without ever seeing the
definition — the definition itself can sit in another unit, and the step that binds the
two sides together belongs to a separate stage. The three decisions measured in this
lesson — binding, passing, return — are three separate components of a function's calling
surface, and each is fixed at its own stage, independently of the others.

## The Linker: A Function in Another Unit

The excerpt below is part of the core introduced in the Compilation Stages lesson: a
function is called in one unit, while its definition sits in another.

```c
// stage.h — declarations of names defined in other units (excerpt: function declaration)
#ifndef STAGE_H
#define STAGE_H

/* This function is defined in another unit: its call is resolved at link time. */
int identity(void);

#endif
```

```c
// choice.c — the second unit: the function's definition lives here
#include "stage.h"

int identity(void) {
    return 7;
}
```

```c
// stage.c — the linking of a function in another unit (excerpt)
#include <stdio.h>
#include "stage.h"

int main(void) {
    printf("%s %d\n", "function in another unit", identity());
    return 0;
}
```

```
function in another unit 7
```

**BS8.** While `stage.c` is compiled, `identity`'s body appears nowhere — the compiler
sees only the declaration in `stage.h`, and from that declaration checks that the call
takes the right number of arguments of the right type, that the return type is `int`, and
leaves a "I need this name" note in the object file. `choice.c` is compiled separately
and writes `identity`'s real body into its own object file. When the two object files are
brought together, the linker matches the note `stage.c` left with the body `choice.c`
supplies — without this match, an executable program never comes into being at all.
Scope and storage duration were questions that stayed inside a single unit; which body a
call is bound to, on the other hand, is resolved only once units come together, at the
**linker** stage.

This three-stage flow — first the two units are compiled separately, then the linker
merges the two — is the general pattern measured in the Compilation Stages lesson,
applied to functions. Compiling `stage.c` proceeds unaware of whether `choice.c` even
exists; the compiler works only by looking at the declaration in `stage.h`, under the
assumption "a function with this name, with this signature, must be defined somewhere,"
and confirming that assumption is the linker's job. If `choice.c` had never been
compiled, or `identity`'s definition had been mistakenly deleted, compiling `stage.c`
would still complete without trouble — the error would only surface at the linking step,
when the linker cannot find the name `identity` in any object file. This shows why a
compile error and a link error can surface at different stages, at different times.

This mechanism belongs to the same family as the `extern` variable binding measured in
the Variables, Scope and Storage Duration lesson: there, which storage location a name
corresponds to, here, which body a name corresponds to, is resolved at the linker. The
difference is that functions have external linkage by default — while a variable needs
`extern` written to be shared, a function is already visible and callable across units
unless it is written `static`. Because the `identity` function is not `static`, its
definition in `choice.c` can be seen from `stage.c`; had the function been defined as
`static identity(void)`, it would close into its own unit, and compiling `stage.c` would
fail because the linker could not find the same name.

This confirms that the `static` keyword performs the same function for functions as it
does for variables: it narrows scope to the file boundary and prevents the linker from
exposing that name to other units. Declaring a function `static` explicitly marks in the
source that the function is only a helper belonging to its own unit's internal working,
not offered as an interface to the outside; this is the function-level counterpart of a
unit being able to hide its own internal details.

## The Separation of Declaration from Definition

A declaration tells the compiler a function's signature (name, parameter types, return
type); a definition also includes the body. `int identity(void);` in `stage.h` is a
declaration — it ends with a semicolon, it carries no body. `int identity(void) { return
7; }` in `choice.c` is a definition. A function can be declared as many times as wanted
across different units (through the same header file), but it must have exactly **one**
definition — otherwise the linker finds more than one body for the same name, and this is
a contradiction. This separation is the function-level application of the
declaration–definition distinction measured for variables in the previous lesson: `extern
int unit_counter;` was a variable declaration while `int unit_counter = 2;` was the
definition; here the same pattern repeats, with a declaration carrying only the signature
in place of the body.

Without a function declaration, it is not possible to check whether a call is correct
just by looking at how it is called; the compiler learns how many parameters are expected
and of what type only from the declaration. `stage.h` being included by `stage.c`
provides exactly this: while `stage.c` is compiled, it never sees `identity`'s body, but
it knows it is a function taking no parameters and returning `int`, and it compiles the
call accordingly. A declaration is therefore not merely documentation — it is the minimum
information the compiler needs to produce the call correctly.

## Parameter Passing: Always a Copy

In C, every argument passed to a function is **copied** into the function's own
parameter. A change made to the function's parameter never affects the caller side's
variable.

```c
// copy.c — parameter passing is always a copy, it never affects the caller's variable
#include <stdio.h>

static void double_value(int value) {
    value = value * 2;
    printf("%s %d\n", "value inside the function", value);
}

int main(void) {
    int number = 5;
    double_value(number);
    printf("%s %d\n", "number on the caller side", number);
    return 0;
}
```

```
value inside the function 10
number on the caller side 5
```

The measurement prints two lines: `value` inside the function, `10` after the
multiplication; `number` on the caller side is still `5`. When `double_value` is called,
`number`'s value (`5`) is copied into a new storage location named `value`; the change
inside `value` affects only that copy, it never touches `number` in `main`. When the
function returns, `value` disappears — an instance of the automatic storage duration
measured in the Variables, Scope and Storage Duration lesson — and `number` remains
untouched. This behavior is the same regardless of the type of parameter the function
takes (integer, floating point, struct): passing in C is always **pass-by-value**; the
source's own variable never passes directly into the function.

This copying works the same way regardless of how many parameters there are, and is
independent of the function body's length: the name `value` corresponds to a storage
location separate from both the caller's `number` and the function's own body, created
the instant the function is called and disappearing when it returns. This shows that the
automatic storage duration rule measured in the Variables, Scope and Storage Duration
lesson also applies to parameters: a parameter behaves just like an ordinary local
variable declared inside the body — only its initial value comes from the caller side. A
source writer who genuinely wants to modify the caller's variable does this in C not by
passing the value, but by passing that value's **address**; but the address itself is not
printed in this lesson, and this subject belongs to the pointers subject, to which an
entire course is devoted.

## The Return Value's Type

A function's return type determines how the caller interprets that value, independently
of which expression is computed inside the body.

```c
// returns.c — the return type determines how the caller interprets the value
#include <stdio.h>

static int ratio(int numerator, int denominator) {
    double actual_ratio = (double)numerator / (double)denominator;
    return (int)(actual_ratio * 100);
}

int main(void) {
    int percentage = ratio(1, 3);
    printf("%s %d\n", "percentage with int return type", percentage);
    return 0;
}
```

```
percentage with int return type 33
```

Inside `ratio`, `actual_ratio` is a `double`, and `1 / 3` is computed as floating-point
division rather than integer division (`0.333...`). But because the function's return
type is declared `int`, the value in the `return` expression — `33.333...` — is converted
to `int` before it reaches the caller, and the fractional part is dropped: `33`. This
conversion is applied not by looking at where or which type is used inside the function
body, but solely at the function's **declared** return type — just as a compound
assignment's result was forced into the left side's type, here the result is forced into
the type written in the function's signature. Different calls like `ratio(1, 3)` and
`ratio(1, 4)` produce different actual ratios, but both go through the same `return`
expression, the same narrowing-to-`int` rule — regardless of which input it is called
with, the conversion rule itself never changes.

Where this conversion happens matters: the expression `actual_ratio * 100` is still
computed at `double` width, inside `ratio`'s body; narrowing to `int` is applied only as
the `return` expression's value leaves the function, exactly at the function's boundary.
The caller side (`main`) never knows and does not need to know which intermediate types
were used inside `ratio` — it only sees the declared return type (`int`) and assigns the
incoming value to a variable of that type (`percentage`). This shows that a function's
boundary provides a kind of **encapsulation**: whatever intermediate types the
computation inside the body uses, the only piece of information that leaks outward is the
return type written in the signature.

## The Bounding Measurement

A call compiles and links safely as long as it is compatible with the called function's
declaration — the `correct.c` example below shows this.

```c
// stage.h — the full declaration telling the compiler the call's shape
#ifndef STAGE_H
#define STAGE_H

int identity(void);

#endif
```

```c
// choice.c — the definition carries the same signature as the declaration
#include "stage.h"

int identity(void) {
    return 7;
}
```

```c
// correct.c — when declaration and definition share the same signature, the call links safely
#include <stdio.h>
#include "stage.h"

int main(void) {
    printf("%s %d\n", "correctly declared call", identity());
    return 0;
}
```

```
correctly declared call 7
```

In this setting, a source trying to call a function whose declaration is never seen is
rejected directly by the compiler — the compiler needs at least a declaration in order to
check a call's shape, and stops compiling the source without one. But this does not mean
the compiler catches every mismatch. A declaration is checked only within the unit it
sits in; while the compiler compiles `stage.c` it never looks inside `choice.c`, and
there is no mechanism verifying that the real definition in `choice.c` genuinely matches
the declaration. C's linker also looks only at the symbol name, not the type — if two
units declare and define a function with the same name but different parameter types,
the linker still binds them to each other. Such a call falls into undefined behavior once
compiled, linked, and run, because the caller side prepares the arguments one way while
the called side reads them another; under this course's general rule, such a mismatch is
not tried in a code block. This lesson runs only the correctly declared setup above: the
declaration tells the compiler the call's shape, but what guarantees that it genuinely
matches the definition is the source writer's own consistency — neither the compiler nor
the linker checks this across units.

The practical consequence of this limit is why a header file is kept as a single
**source of truth**. As long as every unit calling `identity` includes `stage.h`, and
`choice.c` also includes the same header, declaration and definition come from the same
source and automatically stay consistent with each other — the source writer never has to
hand-write the same signature twice in two different places. If a unit did not include
the header and instead hand-wrote the function's signature in its own source, the two
declarations would sit separately in the source, and one could go stale unnoticed if the
other changed — exactly the source of the risk described above. Sharing the header file
does not eliminate this risk, but it at least guarantees the signature is written once,
and hence consistency coming from at least one source.

## Summary

- Which definition a call is bound to is resolved by the **linker** once units come
  together; the compiler only checks the signature from the declaration, it never sees
  the body.
- A declaration carries only the signature (name, parameter types, return type); a
  definition also includes the body. A function can be declared many times, but is
  defined exactly once.
- Parameter passing is always pass-by-**value**: a change made inside the function never
  affects the caller's own variable.
- A function's return value is converted to the function's **declared** return type,
  independently of the types of the intermediate computations inside the body.
- The match between a declaration and that name's real definition is checked across
  units by neither the compiler nor the linker; keeping this consistency is the source
  writer's job.
- A `static` function closes into its own unit and is exposed to no other unit by the
  linker; the same hiding rule from variables applies to functions as well.

## Next Step

All the decisions measured in this lesson were about a function's calling **shape**:
which definition it is bound to, how its argument is passed, how its return value is
interpreted. The Arrays and Strings lesson adds to this ground the question of when a
piece of data's **length** is known: an array's element count can be produced at compile
time from a constant expression in the source, while a string's length can only be
learned at runtime by scanning until a terminating byte is reached — and the "parameter
is always a copy" rule measured in this lesson turns into an interesting exception once
an array is passed to a function: when an array name is passed to a function, what is
copied is not the array itself, but the address of the array's first element — and this
is also the source of why length information must be carried separately.
