A global declaration without an initializer constitutes a "tentative definition":
6.9.2p2:
A declaration of an identifier for an object that has file scope
without an initializer, and without a storage-class specifier or with
the storage-class specifier static, constitutes a tentative
definition. If a translation unit contains one or more tentative
definitions for an identifier, and the translation unit contains no
external definition for that identifier, then the behavior is exactly
as if the translation unit contains a file scope declaration of that
identifier, with the composite type as of the end of the translation
unit, with an initializer equal to 0.
Basically, tentative definitions serve as declarations that become a definition unless it is overridden by an actual definition.
The standard guarantees that this'll work in just one translation unit: i.e., what you are doing with the Books
variable being tentative in several translation unit is not guaranteed to work. However, practically it does work since common linkers (upcoming pun intended) use a special symbol type called a "common symbol" for symbols that were still tentative at the end of a translation unit, and with that special symbol type. tentative definitions work even across several translation units. (If there's one non-tentative definition, it overrides all the other ones; otherwise the tentative definitions get merged into just one zero-initialized external definition.)
Long story short, if you want to be perfectly standard compliant, you should replace the tentative definition in the header with a proper extern
declaration. If you don't, your code might still work. (On Unix-based platforms, it probably will work because common symbols are a feature that's very old, even though it's nonstandard feature.) Either way it's bad practice to put tentative definitions in header files.