The reason that you can compile code in separate translation units is linkage: Linkage is the property of a name, and names come in three kinds of linkage, which determine what the name means when it is seen in different scopes:
None: the meaning of a name with no linkage is unique to the scope in which the name appears. For example, "normal" variables declared inside a function have no linkage, so the name i
in foo()
has a distinct meaning from the name i
in bar()
.
Internal: the meaning of a name with internal linkage is the same inside each translation unit, but distinct across translation units. A typical example are the names of variables declared at namespace scope that are constants, or that appear in an unnamed namespace, or that use the static
specifier. For a concrete example, static int n = 10;
declared in one .cpp file refers to the same entity in every use of that name inside that file, but a different static int n
in a different file refers to a distinct entity.
External: the meaning of a name with external linkage is the same across the entire program. That is, wherever you declare a specific name with external linkage, that name refers to the same thing. This is the default linkage for functions and non-constants at namespace scope, but you can also explicitly request external linkage with the extern
specifier. For example, extern int a;
would refer to the same int
object anywhere in the program.
Now we see how your program fits together (or: "links"): The name print
has external linkage (because it's the name of a function), and so every declaration in the program refers to the same function. There's a declaration in main.cpp that you use to call the function, and there's another declaration in print.cpp that defines the function, and the two mean the same thing, which means that the thing you call in main
is the exact thing you define in print.cpp.
The use of header files doesn't do any magic: header files are just textually substituted, and now we see precisely what header files are useful for: They are useful to hold declarations of names with external linkage, so that anyone wanting to refer to the entities thus names has an easy and maintainable way of including those declarations into their code.
You could do entirely without headers, but that would require you to know precisely how to declare the names you need, and that is generally not desirable, because the specifics of the declarations are owned by the library owner, not the user, and it is the library owner's responsibility to maintain and ship the declarations.
Now you also see what the purpose of the "linker" part of the translation toolchain is: The linker matches up references to names with external linkage. The linker fills in the reference to the print
name in your first translation unit with the ultimate address of the defined entity with that name (coming from the second translation unit) in the final link.