C CLP study materials
C CLP study resources

One Definition, Three Translation Units

C CLP · Put your knowledge to work in context.

All case studies

One Definition, Three Translation Units

Your team maintains a three-file firmware project: main.c, adc.c, and uart.c. All three include board.h. For years board.h contained int g_baud; and static int s_errors;, and the project built with GCC 9 using its default -fcommon. The CI pipeline now uses GCC 12 with -std=c11 -fno-common, and the link fails with multiple definition of 'g_baud'. A developer proposes two changes: wrap the definition in board.h as static int g_baud;, and remove static from s_errors so that main.c can read the UART error count. main.c currently declares extern int s_errors; and prints it in a diagnostic screen. The UART driver increments s_errors on framing errors. The product owner wants the diagnostic screen to show the real count, not a per-module copy, and wants no new linker flags. A second developer warns that the proposed changes will compile but behave incorrectly.

1. Explain precisely why the original header produced a link error under GCC 12 but not under GCC 9. Include what the C standard says about the header definition and what `-fcommon` changed.

Read the worked answer

In each translation unit that includes board.h, the line int g_baud; is a tentative definition with external linkage. C11 6.9.2 says that if a translation unit contains a tentative definition and no other definition for that identifier, the tentative definition becomes a full definition at the end of the translation unit. Thus main.c, adc.c, and uart.c each emit a definition of the same external object. C11 6.9/5 permits only one external definition for an identifier in the entire program, so this header violates the one-definition rule.

GCC 9's default -fcommon placed such uninitialized tentative definitions into a common block. The linker merged the common symbols into one object, hiding the violation. GCC 10 changed the default to -fno-common, so each object file now contains a strong definition and the linker reports multiple definition. The error is not caused by the newer compiler being stricter than the standard; it is the standard-conforming behavior that the old default masked. Initializing the variable in the header would not help either: an initialized definition is never a common symbol, so it would fail even with -fcommon.

2. Evaluate the two proposed changes. What actually happens to `g_baud` and `s_errors` in each translation unit, and what design should replace the header definitions?

Read the worked answer

Making g_baud static in the header gives every translation unit its own internal-linkage copy. adc.c and uart.c would each read and write a different g_baud, so changing the ADC baud rate would not affect the UART driver. The link error disappears, but the program becomes silently incorrect.

Removing static from s_errors also fails. With -fno-common, three tentative definitions of s_errors again produce a multiple-definition link error. More subtly, main.c includes board.h first, so main.c's own static int s_errors; is visible before the later extern int s_errors;. C11 6.2.2 says an extern declaration after an internal-linkage declaration keeps internal linkage. main.c therefore refers to its own private copy, not the UART driver's counter, and prints zero. Removing static does not export uart.c's counter; it merely creates another external definition conflict.

The header should contain declarations, not definitions: extern int g_baud; and an accessor such as unsigned long uart_error_count(void);. Exactly one .c file defines int g_baud = 115200;. uart.c keeps static unsigned long s_errors; and exposes the value through the accessor. main.c calls the accessor. This gives one shared object and keeps the counter private to its module.

3. A teammate suggests putting a small helper function in board.h as `static inline int clamp_baud(int b) { ... }`. Does that create the same multiple-definition problem, and what trade-off does it introduce?

Read the worked answer

No. static inline gives the function internal linkage, so each translation unit gets its own private copy. There is no external symbol for the linker to merge, and the one-definition rule for external identifiers is not violated. This is a common and safe pattern for small header-only helpers.

The trade-off is that each translation unit may emit its own code for the function, increasing code size, and the address of clamp_baud may differ between modules. Code must not compare function pointers across translation units and expect equality. If a single external function is required, use a plain declaration in the header and one definition in a .c file, or use C11 inline carefully with exactly one external definition in the program. For a tiny validation helper, static inline is usually the right choice; it avoids both the multiple-definition error and the per-module data problem that the proposed static variable would create.