C / FUNCTIONS
extern and sharing names across files
Share a variable across several .c files by declaring it extern in a header, defining it once, and reading the linker errors when that pairing breaks.
What you will learn
- Declare a name owned by another .c file with extern, which reserves no storage
- Keep exactly one definition in one .c file and its extern declaration in a header
- Read 'undefined reference' as no definition and 'multiple definition' as two
- Match the extern type to the definition: the linker matches names, not types
Understanding extern and sharing names across files
Every .c file is compiled on its own into an object file that carries two lists of names: the ones this file provides and the ones it still needs from elsewhere. A file-scope name with external linkage lands on the first list when you define it and on the second when you only declare it. Writing extern int total; is exactly that second case: it promises the object exists somewhere in the finished program and tells the compiler its type, so machine code can be emitted with a hole where the address belongs. The linker fills that hole later by matching the name total against whichever object file defines it.
The sharing recipe falls straight out of that model. One .c file defines the object and every other file merely declares it, so put the extern declaration in a header, include that header everywhere the name is used, and include it from the defining file too so the compiler can compare the declaration with the definition on the spot. Functions differ only in that they already have external linkage by default, which is why a plain prototype crosses files and writing extern in front of it changes nothing at all.
The sharp edge is that the linker compares names and nothing else. If one file declares extern double rate; while the definition is int rate = 3;, both files compile without complaint and the link succeeds, because no single translation unit ever sees both spellings, and the running program then reads four bytes of an integer as if they were a floating-point value. That is why routing declarations through a shared header is not a matter of taste: it is the only mechanism that ever puts both halves in front of one compiler invocation.
<stdio.h>
/* A declaration, not a definition: it names an object with external
linkage that is defined somewhere else in the program. */
extern int shared_total;
void add(int n);
int main(void)
{
printf("start %d\n", shared_total);
add(7);
add(35);
{
/* Redeclaring the same external object inside a block.
No new variable appears; this still names the one below. */
extern int shared_total;
printf("inside block %d\n", shared_total);
}
printf("end %d\n", shared_total);
return 0;
}
/* The single definition. This line is what reserves the storage. */
int shared_total = 0;
void add(int n)
{
shared_total += n;
}
extern introduces a name with external linkage without creating it, so one translation unit owns the definition and the linker connects every other file to it by name.
Worked examples
One definition, one header, two files
The standard layout for a variable that two translation units both need to touch.
/* ==== config.h ==== */
CONFIG_H
CONFIG_H
extern int retries; /* declaration only */
extern const char *stage; /* declaration only */
void bump_retries(void); /* extern here would be redundant */
/* ==== config.c ==== */
"config.h"
int retries = 0; /* the one definition */
const char *stage = "warmup"; /* the one definition */
void bump_retries(void)
{
retries++;
}
/* ==== main.c ==== */
<stdio.h>
"config.h"
int main(void)
{
bump_retries();
bump_retries();
printf("%s: retries=%d\n", stage, retries);
return 0;
}
/* build: cc main.c config.c -o app */
Example explained
Line 1extern int retries; in the header creates nothing, so including it in ten files still yields zero objects.
Line 2int retries = 0; in config.c is the only line that allocates the integer the whole program shares.
Line 3config.c includes its own header, so the compiler sees declaration and definition together and rejects a disagreement.
Line 4bump_retries needs no extern: functions have external linkage unless you mark them static.
An extern array declared without its size
Why extern int table[]; is legal and what you lose by declaring an incomplete type.
/* ==== table.h ==== */
TABLE_H
TABLE_H
extern int table[]; /* no size: a declaration does not need one */
extern int table_len;
/* ==== table.c ==== */
"table.h"
int table[] = { 2, 3, 5, 7, 11 };
int table_len = (int) (sizeof table / sizeof table[0]);
/* ==== main.c ==== */
<stdio.h>
"table.h"
int main(void)
{
int i, sum = 0;
for (i = 0; i < table_len; i++)
sum += table[i];
/* sizeof table would not compile here: the type is incomplete */
printf("%d elements, sum %d\n", table_len, sum);
return 0;
}
/* build: cc main.c table.c -o app */
Example explained
Line 1extern int table[]; omits the size because indexing only needs the element type and the array's address.
Line 2In table.c the definition completes the type, so sizeof table works there and computes table_len at compile time.
Line 3Because main.c sees an incomplete type, the element count has to travel as its own extern int rather than being recovered with sizeof.
Line 4The link works because main.o asks for the symbols table and table_len, and table.o supplies both under those exact names.
Important notes
extern int x = 5; is a definition despite the keyword, because an initializer always makes one; keep it out of headers, and expect a warning like 'x initialized and declared extern'.
GCC 10 and later default to -fno-common, so int x; with no initializer in two different files is now a 'multiple definition' link error rather than being silently merged as it was in older builds.
Common mistakes
Putting int counter = 0; in the header instead of extern int counter;: every file that includes it defines its own object and the link dies with 'multiple definition of counter'.
Writing extern int counter; in every file and defining it in none: each file compiles happily, then the linker reports 'undefined reference to counter'.
Hand-copying a declaration with the wrong type, such as extern long n; for an int n: it compiles and links cleanly and then reads or writes the wrong bytes at run time, with no diagnostic anywhere.
Try it yourself
Change, predict, then run
In one file, put extern long score; at the top, long score = 0; at the bottom, and a function that adds to score; run it, then change the definition to int score = 0; and note the 'conflicting types' error the compiler gives you here but could never give you if the two lines lived in separate .c files.
Open the C workspaceCheck your understanding
a.c contains int total = 5;. b.c contains extern short total; and prints total. Both files are compiled and linked into one program. What happens?
- b.c fails to compile, because short does not match the int definition
- The link fails with a type mismatch error for the symbol total
- It builds and runs, and b.c may print a wrong value because only the name is matched
- It builds and runs, and b.c reads the int and converts it to short
Show answer
b.c is compiled alone and sees only extern short total;, so the compiler has nothing to compare it against, and the linker pairs the symbols by the name total without any C type information at all. b.c therefore reinterprets part of a four-byte object as a two-byte one, which is undefined behaviour and depends on byte order. Option 1 is the tempting one, but ordinary object files carry no types for the linker to check; option 3 cannot happen either, since no conversion code was ever generated.