24 Declaring and defining functions
What to know first
Looking back
Chapter 16 said “the compilation stage is satisfied with a declaration (an announcement), and it is the linker that finds and joins the body.” That “declaration” and the “variable declaration” of chapter 23 use the same word — is that a coincidence?
A. It is not. In C a declaration is always the same job — “telling the compiler in advance a name and its shape (its type).” A variable declaration announces the name of a value; a function declaration announces the name of a piece of work. Once we have made the second kind in this chapter, even the identity of what #include was fetching (a bundle of declarations) is threaded onto one line.
The need for this chapter, and its context
int main(void), carried since chapter 15, is paid.By the end of this chapter
return, and the first step into scope. And at the end of this chapter the credit carried since chapter 15 — int main(void) and return 0 — is settled in full.The questions this chapter answers
- Then why not make everything
static inline? - If
mainis a function too, may we callmain()ourselves?
24.1 Definition — the syntax for making a worker
We now make workers like chapter 21′s abs or printf ourselves. The syntax of a function definition is:
return-type name(parameter list)
{
body — a list of statements
}The demonstration makes it quick. A worker that takes an integer and returns its square:
examples-en/ch24/fn.c
#include <stdio.h>
int square(int n) /* a worker taking one integer and returning one integer */
{
return n * n;
}
int main(void)
{
printf("%d\n", square(12));
return 0;
}
Output
144
Reading square’s definition part by part.
- The return type
int— the promise that what this worker puts out is an integer. - The parameter
int n— a variable declaration that receives the material (exactly chapter 23′s syntax). When called, the body starts with the material’s value held in this variable. Callsquare(12)and 12 is put innand the body runs. - The
returnstatement — puts out the result and leaves immediately.return n * n;evaluates the expression and returns its value to the caller — the supplier’s side of “the call site turns into the return value”, learned in chapter 21.
The picture of a call is now complete. Chapter 21 was the consumer’s eye (calling) and this chapter the producer’s (making) — in printf("%d\n", square(12)) the whole relay is now visible: the value 12 into n, the 144 of n * n to the call site, and on as printf’s material.
24.2 Declaration — signing the contract in advance
In the example above, square’s definition is above main. That is no accident — the compiler reads a file top to bottom (chapter 16), so by the time it meets square(12) inside main it must already know what square is (the types of material and result) in order to check that the call is right.
But there is no need to show the whole definition in advance. It is enough to announce the signature alone — that is a function declaration, idiomatically a prototype:
int square(int n); /* declaration: such a worker exists (somewhere) */No body, just the signature and a semicolon. Write prototypes at the top of the file and the definitions may be anywhere below — and, more importantly, they may be in another file. Chapter 16′s riddle is now fully solved: the identity of the tens of thousands of lines pasted in by #include <stdio.h> is exactly such a bundle of prototypes. Showing the compiler printf’s contract signature in advance — that is the header file’s job, and the linker joins the body (the story of that division of labour growing into multi-file projects is chapter 54).
We can now also state the age of this invention, the prototype — as chapter 12 showed, it came in with C89′s standardisation. Before that C did not check the types of materials, and a call made with the wrong materials passed silently and caused accidents. The prototype was C’s first step towards being a language of contracts: “check the contract at compile time.”
In practice. The wound left by calls without prototypes — implicit declaration
The C of the era before prototypes (chapter 12) had one generous rule. Call a name never declared as if it were a function and the compiler would make up a declaration, assuming “presumably a function returning an integer”, and let it pass (implicit declaration). It looks convenient; the result was grim — a function that really returned a floating-point number was mistaken for an integer function, and passing an integer to a function taking a pointer compiled without a single warning. Code that collapsed silently when moved to a machine where integers and pointers differ in size poured out — a great many bugs of this class surfaced during the move from 32 bits to 64. C99 finally removed implicit declaration from the standard, and today’s compilers catch it as an error. That the rule “declare before use” is not tiresome formality but a barrier against accidents — that is the lesson of half a century of wounds.24.3 One more word a declaration takes — inline
Among the words that may fill a declaration’s front slot, two attach only to functions. The standard calls these function specifiers, and the list is just inline and _Noreturn. _Noreturn stepped back in C23 (use the [[noreturn]] attribute instead), so in practice there is one.
examples-en/ch24/inline_fn.c
/* The function specifier inline - what it promises and what it does not.
Only the two forms that work in practice; the rest is left in comments. */
#include <stdio.h>
/* (1) A helper used inside one file - the safest form.
Being static it is this translation unit's definition; no external
definition is needed elsewhere. */
static inline int square(int x) { return x * x; }
/* (2) The form for a header used by several files.
The two lines below are a pair - one inline definition plus one extern
declaration. The extern declaration says "this translation unit emits
the external definition". */
inline int cube(int x) { return x * x * x; }
extern int cube(int x);
int main(void)
{
printf("(1) static inline square(5) = %d\n", square(5));
printf("(2) inline + extern cube(3) = %d\n", cube(3));
/* (3) Its address can be taken - a function is a function.
But the moment you take it, one real definition must exist. */
int (*f)(int) = square;
int (*g)(int) = cube;
printf("(3) calling through pointers: f(6)=%d g(2)=%d\n", f(6), g(2));
/* (4) inline is a request, not a promise. The compiler may expand it
or may not. */
printf("(4) the answer is the same, expanded or not: %d %d\n", square(4), f(4));
return 0;
}
Output
(1) static inline square(5) = 25
(2) inline + extern cube(3) = 27
(3) calling through pointers: f(6)=36 g(2)=8
(4) the answer is the same, expanded or not: 16 16
24.3.1 inline is a request, not an order
Clear away the common misunderstanding first.
A common misconception. “Mark it inline and the function call disappears”
No. The standard’s wording is a suggestion that the call be made as fast as possible, and it pins down that an implementation may ignore that suggestion and still conform. In practice today’s compilers expand functions not marked inline when it pays, and decline to expand marked ones that are large or unprofitable.
★ So the real worth of inline is not “make this fast” but permission for the definition to appear in several translation units. Without that permission you cannot put a function body in a header — the moment two files include it, the linker catches a duplicate definition (chapter 54).
To truly force expansion you must step outside the standard — __attribute__((always_inline)) on GCC and Clang, __forceinline on MSVC — and then it belongs in a platform box.
24.3.2 Its address can be taken — and that is when a real one is needed
★ Answer the frequent question first: the address of an inline function can be taken. inline is not part of the type but a property of that declaration, so nothing stops it going into a function pointer. Output ③ is that.
But here is where C’s inline earns its reputation. Taking an address means a real thing (an external definition) must exist somewhere. And an inline definition does not provide one.
24.3.3 C’s inline model — one rule and its consequences
Stated exactly:
> If a function is declared inline in a file and none of its file-scope > declarations there use extern, then that file’s definition is an inline > definition. An inline definition does not provide an external definition.
The consequences are nasty. Measured, it splits like this.
| What was done | -O0 | -O2 |
|---|---|---|
an inline definition alone, then called | ★ undefined reference to 'add' | links fine |
| letting the address really escape | fails | ★ fails — undefined reference |
inline definition + extern declaration | works | works |
static inline | works | works |
Table 24.1
★ Look at the first row. Turn optimisation on and it links; turn it off and it breaks. That is the opposite direction from the “bug that only shows in release” seen elsewhere in this book. The reason is simple — -O2 expanded every call and never needed the real thing, while -O0 genuinely tried to call it and found nothing. This is the common cause of a project whose debug build fails to link.
24.3.4 So in practice only two forms are used
| Form | Where it goes | When |
|---|---|---|
static inline | header or source | ★ most of the time. each translation unit gets its own copy and no external definition is needed |
inline definition + one extern declaration | definition in the header, extern declaration in one source | when several files use it and the real thing must be single (comparing addresses, say) |
Table 24.2
The second form’s pair, written out:
/* util.h - the definition goes here */
inline int cube(int x) { return x * x * x; }
/* util.c - this one line. this translation unit emits the external definition */
#include "util.h"
extern int cube(int x);The example puts the pair in one file to show it, but the real layout is the above.
Counter-example. Putting only an inline definition in a header
/* util.h */
inline int cube(int x) { return x * x * x; } /* and nothing else */With no extern declaration in any source, the program has no external definition anywhere. And yet in most builds it works — the optimiser expands every call. Then someone makes a debug build, or puts the function in a pointer, and undefined reference appears.
★ The nature of this defect is bad: it shows up with no change to the code, only to the build settings. The cure is one of two — if it is not for several files, make it static inline; if it is, put an extern declaration in exactly one source.
24.3.5 Two restrictions on an inline definition
A non-static inline definition carries restrictions from the standard. Since the definition may appear in several translation units, it must not rely on anything that could differ between them.
| Forbidden | Why |
|---|---|
| defining a modifiable object of static or thread storage duration | one per translation unit would mean several “that counter”s |
referring to a name with internal linkage (static) | other translation units have no such name at all |
Table 24.3
Measured, GCC says:
warning: 'n' is static but declared in inline function 'counter' which is not static
warning: 'secret' is static but used in inline function 'peek' which is not static★ Note that a warning is the default; -pedantic-errors raises them to errors. Read-only is fine — referring to a static const constant is permitted. And none of these restrictions apply to static inline: it belongs to that translation unit to begin with.
Q. Then why not make everything static inline?
A. Mostly you should, and this book recommends it as the default. There are two places where you pay.
First, copies multiply. Each translation unit that did not expand it gets its own copy, so code can grow. For a small function this is usually negligible.
Second, the address is not unique. Each translation unit has a different real thing, so comparing function pointers may come out unequal. Code that registers a callback and later checks whether it is the same one will have a bug. That is where the second form (inline + extern) belongs.
24.4 Scope — the range in which a name is visible
Will square’s n collide with some name in main? No need to worry. A name declared inside a block is visible only inside that block. This range of visibility is called scope. n exists only in square’s body, and main does not know that name at all. It is as if each function has its own workbench, so workers name things freely without worrying about touching one another’s tools — half the secret of a program not collapsing as it grows is this partition. (The deeper circumstances of the workbench — the lifetime of names, the precise picture of how values travel between functions — continue in chapters 30 and
- And a name has two further properties besides scope — linkage and its
name space — which chapters 54 and 55 set side by side with it.)
24.5 Settling the credit — int main(void)
Now for the thing saved up for this moment. The two lines left as an “incantation” to the last in chapter 15′s hello world, reread with the grammar learned today.
int main(void)
{
...
return 0;
}This is simply a function definition. The name is main, the void in the parameter list is the notation stating “no materials”, and the return type is int — that is, “a worker that works without materials and puts out one integer.” What is special is not the grammar but the caller: what calls main is not our code but the operating system. Running a program is the operating system calling main, and return 0; returns a result value to that caller — by convention 0 means “finished without trouble” and a nonzero value “there was a problem.” That value is not discarded — the operating system and the shell receive it, and automation that joins programs together uses it to tell success from failure (like chapter 10′s streams, this too is a device for joining programs as components).
Chapter 15′s credit ledger is with this entirely settled. There is now nothing unknown in hello world’s six lines — preprocessing directive, function definition, block, call expression statement, string literal, format, and return. The first mountain of the introduction has been crossed.
Q. If main is a function too, may we call main() ourselves?
A. Grammatically possible, but the practice is not to, and there is no reason to — main’s essence is its role as “the entrance the operating system calls”, so calling it again inside the code is like building the front door of a building inside a room. If there is work you want to reuse, take that work out as a separate function and call it — which is in fact the whole reason for making functions: giving a name to work worth naming.
We have named values (chapter 23) and named work (chapter 24). Now the last promise deferred in chapter 22 can be kept — with somewhere to hold a value, at last it is input.