When starting g++ -I. -l. ABC.cpp g++ -I. -l. ABC.cpp g++ -I. -l. ABC.cpp you ask the compiler to create an executable file from ABC.cpp . But the code in this file responds to the function defined in XYZ.cpp , so the executable cannot be created because of this missing function.
You have two options (depending on what you want to do). Or you give the compiler all the source files at once to have all the definitions, for example.
g++ -I. -l. ABC.cpp XYZ.cpp
or you use the -c option to compile ABC.cpp into object code (.obj on Windows, .o on Linux), which can be linked later, for example
g++ -I. -l. -c ABC.cpp
ABC.o will be created, which can later be linked to XYZ.o to create an executable file.
Edit: What is the difference between #include and linking?
Understanding this completely requires an understanding of what happens when compiling a C ++ program, unfortunately, even many people who consider themselves C ++ programmers do not. At a high level, compiling a C ++ program goes through three stages: preprocessing, compilation, and binding.
Preprocessing
Each line starting with # is a preprocessor directive, which is evaluated at the preprocessing stage. The #include directive is literally copy and paste. If you write #include "XYZ.h" , the preprocessor replaces this string with all the contents of XYZ.h (including the recursive evaluations of #include inside XYZ.h ).
The goal of inclusion is to make ads visible. To use the GetOneGaussianByBoxMuller function, the compiler must know that GetOneGaussianByBoxMuller is a function and know what (if any) arguments it takes and what value it returns, the compiler will need to see the declaration for it, declarations go in the header files, and the header files are included to make declarations visible to the compiler to the point of use.
Compilation
This is the part in which the compiler is executed, and turns your source code into machine code. Note that machine code is not the same as executable code. The executable file requires additional information on how to load machine code and data into memory, and how to bring external dynamic libraries if necessary. It is not done here. This is only the part where your code goes from C ++ to the original machine instructions.
Unlike Java, Python, and some other languages, C ++ has no concept of a "module." Instead, C ++ works in terms of translation units. In almost all cases, the translation block corresponds to one (non-header) source code file, for example. ABC.cpp or XYZ.cpp . Each translation unit is compiled independently (regardless of whether you execute separate -c commands for them or you pass them to the compiler all at once).
When the source file is compiled, the preprocessor runs first and performs #include copying, as well as macros and other things that the preprocessor does. The result is one long stream of C ++ code, consisting of the contents of the source file and everything contained in it (and everything that is included in it, etc.). This long stream of code is a translation unit.
When the translation block is compiled, each function and each variable used must be declared. The compiler will not allow you to call a function for which there is no declaration or use a global variable for which there is no declaration, because then it will not know the types, parameters, return values, etc., And it cannot generate reasonable code. That's why you need headers - keep in mind that at the moment the compiler does not even remotely know about the existence of any other source files; it only considers this stream of code created by processing the #include directives.
In machine code generated by the compiler, there are no variables such as variable names or function names. Everything should become a memory address. Each global variable must be translated to the memory address where it is stored, and each function must have a memory address to which the thread of execution executes when it is called. For things that are defined (i.e., for functions implemented) in the translation unit, the compiler can assign an address. For things that are only declared (usually as a result of the included headers) and are not defined, the compiler does not currently know what the memory address should be. These functions and global variables, for which the compiler has only a declaration, but not a definition / implementation, are called external characters, and it is assumed that they exist in another translation unit. At the moment, their memory addresses are represented by placeholders.
For example, when compiling a translation unit corresponding to ABC.cpp , it has an ABC definition (implementation), so it can assign an address to the ABC function and, wherever in this ABC translation unit, it can create a jump instruction to this address. On the other hand, although his declaration is visible, GetOneGaussianByBoxMuller not implemented in this translation unit, so its address must be represented by a placeholder.
The result of compiling a translation unit is an object file (with the suffix .o on Linux).
Communication
One of the main tasks of the linker is to resolve external characters. That is, the linker looks at the set of object files, sees their external characters, and then tries to figure out what memory address should be assigned to them, replacing the placeholder.
In your case, the GetOneGaussianByBoxMuller function GetOneGaussianByBoxMuller defined in the translation block corresponding to XYZ.cpp , so inside XYZ.o specific memory address was assigned to it. In the translation block corresponding to ABC.cpp , it was declared, therefore inside ABC.o it is only a placeholder (external character). The compiler, if both ABC.o and XYZ.o , will see that ABC.o needs an address filled in for GetOneGaussianByBoxMuller , find this address in XYZ.o and replace it with ABC.o Addresses for external characters can also be found in libraries.
If the linker does not find the address for GetOneGaussianByBoxMuller (as in your example, where it works only with ABC.o , as a result of the lack of XYZ.cpp passed XYZ.cpp compiler), it will report an unresolved external character error, also described as an undefined reference.
Finally, as soon as the compiler resolves all external characters, it combines all the object code containing no names, adds all the boot information needed by the operating system, and creates an executable file. TA-dah!
Note that for all this, file names do not matter a single bit. It is an agreement that XYZ.h should contain declarations for things defined in XYZ.cpp , and it is useful for supported code to organize such things this way, but the compiler and linker do not care whether this is true or not. The compiler will look at all the object files that it gave, and only the object files that it gave to try to resolve the character. He does not know or care about which title contains the declaration of the symbol, and he will not automatically try to retract other object files or compile other source files to resolve the missing symbol.
... wow, that was a long time.