Outside of any function, int x; is a tentative definition, and some compilers and linkers treat them as a sort of “cooperative definition,” where an identifier can be declared this way in multiple files and will result in defining only one object.
C’s rules for external declarations (declarations outside of functions) are a bit complicated due to history—C grew with different people developing and experimenting, rather than by design with the knowledge we have today.
Definitions: Outside of a function int x = 3; is a definition. It both declares the identifier x and reserves memory for an int, and it initializes the int to 3.
Declarations: extern int x; is a declaration but not a definition. It declares the identifier x but does not reserve memory for it.
Both of these declarations give x external linkage. This means, when they appear in different source files, the two instances of the identifier will be linked to refer to the same thing in memory.
The C standard says “there shall be” at most one definition for an identifier with external linkage (C 2018 6.9 5). (If the identifier is used in the program, there must be a definition. If it is not used in an expression, you do not need a definition.)
Tentative definitions: int x; is a hybrid. It is a special kind of possible definition called a tentative definition. The C standard says that, if there is a tentative definition in a translation unit (the source file being compiled, along with all the files it includes) and no regular definition, the tentative definition becomes a regular definition.
Now, what happens if you violate the rule that “there shall be” at most one definition? Here is the thing: It is not a rule a program has to obey. When the C standard says “shall” it means, if a program obeys this rule, the behavior will be as the C standard says. If a program disobeys this rule, the C standard does not define the behavior (C 2018 4 2). Instead, we let the compiler and the linker define the behavior.
Common behavior in compilers and linkers when a program violates the rule about at most one definition is:
- If there are multiple regular definitions when linking, report an error.
- If there are multiple tentative definitions but only zero or one regular definitions, coalesce them into a single definition.
This was the defined default behavior in GCC and associated tools prior to GCC version 10 and was explicitly mentioned in the C 2018 standard’s informative section on common extensions, in J.5.11. In current versions of GCC, multiple definitions of any type are treated as an error by default. You can request the old behavior with the command-line switch -fcommon.