The preprocessor, multi-file programs and make · about 50 min

Linkage: extern and static

Why this matters

As soon as a program has several files, names start to collide. Two modules both want a helper called init or a counter called count. Some variables genuinely need to be shared between files, like a configuration setting; most should be private to the file that owns them. C controls this with linkage, and two keywords, extern and static. Getting linkage right is the difference between modules that can be combined freely and a program where adding a file breaks the build with "multiple definition".

Three kinds of linkage

Every name in C has a linkage, which answers: "if the same name appears in another place, is it the same thing?"

linkagemeaningtypical examples
externalthe same name in any file of the program refers to the same object or functionordinary functions; file-scope variables without static
internalthe same name refers to the same thing only within this translation unitfunctions and file-scope variables declared static
noneeach declaration is its own separate thinglocal variables, function parameters

The defaults:

The linker only works with names that have external linkage. Internal names never leave the object file as global symbols, which is why they cannot clash.

static at file scope: making things private

Here is a module with a public function, a shared variable, and private details:

counter.h:

#ifndef COUNTER_H
#define COUNTER_H

extern int counter_calls;      /* declaration: defined in counter.c */

int counter_next(void);

#endif

counter.c:

#include "counter.h"

int counter_calls = 0;           /* definition, external linkage */
static int current = 100;        /* internal linkage: private to this file */

static int step(void)            /* private helper */
{
    return 5;
}

int counter_next(void)
{
    counter_calls++;
    current += step();
    return current;
}

main.c, which has its own, unrelated step:

#include <stdio.h>
#include "counter.h"

static int step(void)            /* a different, unrelated step */
{
    return 1;
}

int main(void)
{
    for (int i = 0; i < 3; i += step())
        printf("next: %d\n", counter_next());
    printf("counter_next was called %d times\n", counter_calls);
    return 0;
}
$ gcc -std=c17 -Wall -Wextra -c counter.c main.c
$ nm counter.o
0000000000000000 B counter_calls
000000000000000b T counter_next
0000000000000000 d current
0000000000000000 t step
$ nm main.o
                 U counter_calls
                 U counter_next
000000000000000b T main
                 U printf
0000000000000000 t step
$ gcc counter.o main.o -o demo && ./demo
next: 105
next: 110
next: 115
counter_next was called 3 times

Read the nm letters: the letter is the section (T code, D initialized data, B data initialized to zero, U undefined here) and the case is the linkage. Upper case is global, lower case is local. Both files have a lower-case t step; the linker ignores local symbols when matching names, so the two step functions coexist peacefully. Without static on both, the link fails:

/usr/bin/ld: /tmp/ccevSTk5.o: in function `helper':
h2.c:(.text+0x0): multiple definition of `helper'; /tmp/ccyMGnPd.o:h1.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

The rule of thumb: make every function and file-scope variable static unless another file needs it. Public names belong in the header; everything else is static. This is C's version of "private", and it also helps the compiler, which can inline or remove a static function it knows nobody else calls. gcc even warns about an unused static function (defined but not used), which it cannot do for external ones.

extern: sharing one variable across files

To share a variable, you need exactly one definition (which allocates the storage and may initialize it) and any number of declarations (which say "this exists somewhere, with this type"):

int counter_calls = 0;      /* definition: in exactly one .c file */
extern int counter_calls;   /* declaration: in the header, seen by everyone */
  counter.h:  extern int counter_calls;   ---- included by ---->  main.c, counter.c, ...
  counter.c:  int counter_calls = 0;      ---- the one real object in memory (.bss)

  main.o: U counter_calls  --\
                              >-- linker points both at the same 4 bytes
  counter.o: B counter_calls --/

extern without an initializer never allocates storage. If no file defines the variable, you get the familiar linker error:

e.c:(.text+0x6): undefined reference to `limit'

If two files define it, you get multiple definition. With GCC 10 and later this includes definitions without initializers: int count; in two files is an error. (Older compilers merged such "tentative definitions" silently, which is why some old code relies on it and now fails to link; -fcommon restores the old behavior, but fixing the code is better.)

Functions are extern by default, so extern int counter_next(void); and int counter_next(void); mean the same thing. People usually leave out extern on function prototypes and always write it on variable declarations.

Shared globals: use sparingly

A global variable with external linkage can be changed by any code in the program, which makes behavior hard to reason about and tests hard to write. Prefer, in order:

  1. A static variable in one file, accessed through functions (set_verbosity(3), get_verbosity()). The module controls every change and can validate it.
  2. Passing values as parameters, or in a struct that holds the program's configuration.
  3. An extern global, when it really is program-wide and simple, like a verbosity flag or errno itself.

static inside a function: a different meaning

The same keyword has a second, unrelated job. Inside a function, static changes a local variable's storage duration, not its linkage:

int next_ticket(void)
{
    static int n = 0;   /* initialized once, before main; keeps its value */
    return ++n;
}

Each call returns 1, 2, 3, ... because n lives in the data segment like a global, not on the stack, but its name is only visible inside the function. You met this in the functions module. The two uses of static are easy to mix up:

wherewhat static changeseffect
file scope (outside functions)linkage: external to internalthe name is private to this file
block scope (inside a function)storage duration: automatic to staticthe variable survives between calls

Common mistakes

Recap

Going further: run nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep ' T ' | wc -l in the sandbox to count the functions libc exports. Then check that nm on one of your own programs shows your static helpers in lower case.

Exercises

Exercise 1 · quiz

Reading nm

nm counter.o prints:

0000000000000000 B counter_calls
000000000000000b T counter_next
0000000000000000 d current
0000000000000000 t step

Which statements are true? Select all that apply.

Answer
  • counter_next is a function other object files can call

  • step is a function declared static, invisible to other object files

  • counter_calls is a variable with external linkage, initialized to zero (it lives in .bss)

nm's letter gives the section (T/t text = code, D/d initialized data, B/b zero-initialized data, U undefined), and the case gives the linkage: upper case is global (external linkage), lower case is local to this object file (internal linkage, from static). current is lower-case d: a static variable with an initializer, private to counter.c.

Exercise 2 · write code

Hide the internals

Your main.c is meant to be a reusable module: the only thing other files should be able to use is the function int new_id(void). The variable last_id and the helper log_msg are internal details, but right now they have external linkage, so they clash with any other file in the program that happens to use the same names.

Give last_id and log_msg internal linkage, keeping new_id public. Do not rename anything. The given main prints exactly:

id 1
id 2
id 3

The checker links your file (with main renamed) together with another file that defines its own last_id and log_msg and calls your new_id. That link must succeed and both files must keep their own variables.

File: main.c

Starting point

#include <stdio.h>

int last_id = 0;

void log_msg(const char *msg, int value)
{
    fprintf(stderr, "ids: %s %d\n", msg, value);
}

int new_id(void)
{
    last_id++;
    log_msg("issued", last_id);
    return last_id;
}

int main(void)
{
    for (int i = 0; i < 3; i++)
        printf("id %d\n", new_id());
    return 0;
}

Example

expected output

id 1
id 2
id 3
Hint 1

At file scope, the keyword static gives a name internal linkage: it is visible only inside this file.

Hint 2

Put static in front of int last_id = 0; and in front of the log_msg definition, and nothing else.

Reference solution
#include <stdio.h>

static int last_id = 0;

static void log_msg(const char *msg, int value)
{
    fprintf(stderr, "ids: %s %d\n", msg, value);
}

int new_id(void)
{
    last_id++;
    log_msg("issued", last_id);
    return last_id;
}

int main(void)
{
    for (int i = 0; i < 3; i++)
        printf("id %d\n", new_id());
    return 0;
}

Exercise 3 · task in a shell

One variable, three files

The workspace holds a small program in three source files that share a global variable verbosity: config.c sets it, report.c and main.c read it. It does not link:

$ gcc -std=c17 -Wall -Wextra -o app main.c config.c report.c
/usr/bin/ld: ... multiple definition of `verbosity'; ... first defined here

Fix it by editing only config.h and config.c: the header must only declare the variable (with extern), and config.c must contain its single definition, still initialized to 1. Then build app with the command above and run it; it should print:

verbosity starts at 1
report: verbosity is now 3 (detailed)
Reference solution
cat > config.h <<'EOF'
#ifndef CONFIG_H
#define CONFIG_H

extern int verbosity;

void set_verbosity(int level);

#endif
EOF
cat > config.c <<'EOF'
#include "config.h"

int verbosity = 1;

void set_verbosity(int level)
{
    if (level < 0)
        level = 0;
    verbosity = level;
}
EOF
gcc -std=c17 -Wall -Wextra -o app main.c config.c report.c
./app

Exercise 4 · quiz

Two meanings of static

static int total = 0;          /* (1) at file scope */

int add(int x)
{
    static int calls = 0;      /* (2) inside a function */
    calls++;
    total += x;
    return calls;
}

Which statement is correct?

Answer
  • (1) gives total internal linkage; (2) gives calls static storage duration, so it keeps its value between calls and is initialized only once

At file scope, every variable already lives for the whole program, so static there only changes linkage: the name is not visible to other files. Inside a function, a variable normally has no linkage anyway, so static there changes the storage duration instead: calls lives for the whole program, is initialized once before the program starts, and keeps counting (add returns 1, 2, 3, ...). Same keyword, two different jobs. Neither has anything to do with const.