C / MULTI-FILE PROGRAMS AND BUILDS
Compiling and linking multiple files with gcc
Compile each .c file into an object file with gcc -c, link the objects into one executable, and tell compile-stage errors apart from link-stage errors.
What you will learn
- Compile each .c with gcc -c, then link the .o files with a single gcc command
- Tell a compile error from a link error by whether it names a line or a symbol
- Fix 'undefined reference' by adding the missing .o, not by editing the header
- Name the executable with -o, or the link step writes a.out
Understanding Compiling and linking multiple files with gcc
gcc is not one program but a driver over a pipeline: the preprocessor pastes in headers and expands macros, cc1 turns that single stream of tokens into assembly, and as assembles it into an object file. The -c flag stops the pipeline right there, leaving area.o, which holds machine code plus a symbol table saying rect_area is defined here while printf and other names are only referenced. Nothing in that step ever opens main.c; the compiler's entire view of the world is one preprocessed source file.
Linking is a second, separate job. When you run gcc main.o area.o -o prog, gcc adds the C startup objects and libc and calls ld, which merges the sections, assigns final addresses, and patches every unresolved reference to the address of the matching definition. It matches by name only: ld has no idea rect_area takes two doubles, so a declaration that disagrees with the definition compiles and links cleanly and then misbehaves at runtime. If no object you listed defines a needed name, you get undefined reference to `rect_area', a message that names a symbol rather than a line of code, which is exactly how you know which stage failed.
Reading a command line as "which stage am I asking for" removes most of the confusion. Any invocation with -c compiles only; any invocation without it ends in a link, which is why gcc area.c -o area fails with undefined reference to `main' even though area.c is perfectly good code. -o names whatever the final stage produced, defaulting to a.out for a link and to source.o for -c. And -I only changes where the preprocessor looks for headers while -l only adds libraries for the linker, so passing one where you needed the other changes nothing at all.
/* ---- area.h ---- */
AREA_H
AREA_H
double rect_area(double w, double h);
/* ---- area.c ---- */
"area.h"
double rect_area(double w, double h)
{
return w * h;
}
/* ---- main.c ---- */
<stdio.h>
"area.h"
int main(void)
{
printf("%.2f\n", rect_area(3.0, 4.5));
return 0;
}
/* Two stages, three commands:
* gcc -Wall -Wextra -c area.c produces area.o
* gcc -Wall -Wextra -c main.c produces main.o
* gcc area.o main.o -o prog produces prog
* ./prog
*/
gcc performs two distinct jobs, per-file compilation that trusts declarations and a final link that matches symbol names, and every error belongs to one job or the other.
Worked examples
Each object remembers its own source
Shows that the two .c files really are compiled independently, because each one bakes in its own file name at its own compile time.
/* ---- parts.c ---- */
<stdio.h>
void where_am_i(void)
{
printf("compiled from %s\n", __FILE__);
}
/* ---- main.c ---- */
<stdio.h>
void where_am_i(void); /* declaration only, no code here */
int main(void)
{
printf("compiled from %s\n", __FILE__);
where_am_i();
return 0;
}
/* gcc -c parts.c main.c
* gcc main.o parts.o -o prog
* ./prog
*/
Example explained
Line 1__FILE__ is expanded by the preprocessor while that one unit is compiled, so parts.o permanently carries the string "parts.c".
Line 2void where_am_i(void); is a promise about a name; main.o leaves the call site as a relocation for ld to fill in later.
Line 3gcc -c parts.c main.c compiles two units in one invocation but still writes two independent objects, parts.o and main.o.
Line 4Listing the objects in the other order on the link line prints the same two lines, because output order comes from the calls in main, not from ld.
Compiling and linking in one command
Demonstrates a build with no visible .o files and a reference chain that ld resolves across three objects at once.
/* ---- a.c ---- */
int add(int x, int y)
{
return x + y;
}
/* ---- b.c ---- */
int add(int x, int y);
int sum_to(int n)
{
int total = 0;
int i;
for (i = 1; i <= n; i++)
total = add(total, i);
return total;
}
/* ---- main.c ---- */
<stdio.h>
int sum_to(int n);
int main(void)
{
printf("%d\n", sum_to(5));
return 0;
}
/* One command runs both stages:
* gcc -Wall main.c b.c a.c -o sum
* ./sum
*/
Example explained
Line 1There is no -c, so gcc compiles all three units into temporary objects, links them, then deletes the temporaries.
Line 2main.o needs sum_to and b.o needs add; ld resolves both from the set of objects it was given, so the order of the files on the line does not matter.
Line 3Leaving a.c off the command still compiles main.c and b.c successfully and fails only at the link, with undefined reference to `add'.
Line 4Drop the -o sum and the same program is produced under the default link output name a.out.
Seeing what an object needs
Uses nm to read the symbol table that the linker will consult, before any linking happens.
/* ---- main.c ---- */
int helper(void);
int main(void)
{
return helper();
}
/* gcc -c main.c
* nm main.o
*/
Example explained
Line 1T main means main.o defines main in its text section, at offset 0 because addresses are not assigned until link time.
Line 2U helper means main.o references helper without defining it; this is the entry that becomes an undefined reference if no other object supplies it.
Line 3The blank address column for U exists because the compiler cannot know where helper will end up, so it emits a relocation instead of an address.
Important notes
-c and -o combine for a single file, as in gcc -c area.c -o build/area.o, but gcc -c a.c b.c -o both.o is rejected: one -o cannot name two outputs.
fatal error: area.h: No such file or directory is a preprocessor failure fixed with -I, while undefined reference is a link failure fixed by adding an object; editing headers never fixes the second one.
Common mistakes
Listing the header instead of the second source file, as in gcc main.c area.h -o prog: gcc precompiles area.h into area.h.gch, never compiles area.c, and the link still fails with undefined reference to `rect_area'.
Expecting gcc area.c -o area to check one file on its own; with no -c, gcc goes on to link and dies with undefined reference to `main', which looks like a bug in area.c but is not.
Writing #include "area.c" in main.c and also passing area.c to gcc, so the definition is compiled twice and ld reports multiple definition of `rect_area'.
Try it yourself
Change, predict, then run
Write count.c defining int count_vowels(const char *s) and main.c printing the count for "linking", build them with two gcc -c calls plus one link, then run the link again with count.o left out and read the error you get.
Open the C workspaceCheck your understanding
main.c declares double rect_area(double w); while area.c defines double rect_area(double w, double h). Both files compile and the program links with no warning, even under -Wall. Why?
- Each file is compiled alone against the declarations visible in it, and ld matches only the name rect_area, never the parameter list
- The linker compares the two prototypes and silently keeps the one from the file listed first on the command line
- gcc inserts a conversion because both versions use double, so the call is corrected during the link
- Extra or missing arguments are always harmless in C, so a call that passes fewer arguments than the definition takes is well defined
Show answer
An object file records symbol names and relocations, not signatures, so nothing compares main.c's declaration with area.c's definition; while compiling main.c the compiler cannot even see area.c, which is why -Wall has nothing to warn about, and at runtime h is read from whatever register or stack slot the caller left untouched. Option 4 is tempting because printf really does accept extra arguments, but that works only because printf is declared with an ellipsis; here the mismatch is undefined behaviour that gcc diagnoses only with whole-program analysis such as -flto.