@msc gives a good introduction into the rules behind this behavior.
I noticed that if I declare a global variable multiple times the compiler does not even output a warning.
C has three kinds of global declarations for objects, namely those that are (and I'm glossing over
- declarations that are not definitions —
extern int a;
- declarations that are also definitions —
int a = 3; or
extern int a = 3;
- tentative definitions —
Multiple declarations of type 1 & 3 are allowed, while at most one (type 2) definition is allowed.
What is the explanation for this behavior?
If you're also asking about motivation for these rules, it is support for separate compilation. (See translation unit).
In order to break a program into multiple files separately compiled, we need a few features, namely (a) being able to declare without necessarily defining, and, (b) forward declaration.
Within one translation unit we need to be able to refer to global functions and data in another translation unit. And we'd also like some error checking, here, to discover missing definitions, and erroneous duplicate definitions.
Sometimes, in the same translation unit, we declare a global, and then define it later. This can happen if we need a forward declaration for some reason, or if we use a common header file (that provides declarations) within the one translation unit that also offers explicit definitions.
Since separate compilation in C applies by linking global functions and data together, these features are required at the global level but not at the local level.
As @msc points out, none of this is necessary for local variables as they have no linkage.
C (like many other languages) does not provide linkage for local variables as the language does not attempt to support a single function spanning multiple separate translation units.
(Of course, you can have a function span multiple source files, but not multiple translation units.)
A tentative definition works just like a declaration in that it is allowed in multiple translation units (and also combines nicely with other declarations). However, if there is no (non-tentative) definition for the identifier in the whole program, the set of (one or more) tentative definitions across multiple translation units (for one identifier) is taken as a definition for the object whose initializer is zero.
This can be implemented by putting these into the .BSS Section with the proper size and alignment; the linker will either match them to the true definition if found, or else match them to each other, giving them zeroed out space in BSS.
The notion of separate compilation can be supported entirely without the feature of tentative definitions — I think tentative definitions are there mostly for historical reasons. (I'm not saying they're not useful, just if the language were created today, this might seen as unnecessary and hence not be offered.)
Some purposefully avoid tentative declarations, with the argument that a missing definition should produce a linkage error — silently initializing with zero is error prone so it is better to have a real error message making the programmer address the initialization explicitly.