A variety of errors can creep into our programs. The compiler component and the linker (or the loader on POSIX systems) detect and report syntax and link errors, respectively. But the compiler components' abilities to localize the errors (i.e., to locate or find where the errors occur in the program) are not equal. A brief review of syntax errors makes understanding link errors easier.
The compiler component, the middle program in the full compiler system, detects and reports syntax errors. Its diagnostics include the names of the files containing errors, the line numbers where it first detects an error and where programmers begin searching for it, and the compiler's "best guess" about the errors' cause. Sometimes, the compiler's diagnostics (i.e., its error messages) accurately describe the error but are inaccurate and confusing at other times. When the error messages don't help us identify the problems, we begin at the line number reported in the error message and work upward toward the top of the file, using our understanding of C++ syntax to find the problem.
Although incorrect syntax doesn't cause link errors, the compiler system still reports them in the output window. We first looked at link errors in the narrow context of building single-file programs. Multi-file programs give us more opportunities to make errors that the linker catches and reports.
The three programs of the full compiler system (Figure 4) process program files in different ways.
The potential for link errors arises when a program defines a function in one file but calls it in another - a typical situation in multi-file programs. When the compiler component processes a source code file, it needs function declarations or prototypes for the functions defined in other files. The linker or loader combines all the object files - the files defining and calling each function - to create an executable program. The linker detects and reports the error when the function call and definition don't match. Link errors are denoted in the output window by the label "LNK" followed by a number, but don't worry about the number (it doesn't help identify the error); the loader reports errors with the label "ld."
file1: Definition | file2: Call |
---|---|
#include <iostream> using namespace std; void foo(int x, int y, int z) { cout << x << " " << y << " " << z << endl; } |
#include <iostream> using namespace std; void foo(int x, int y); // incorrect prototype int main() { foo(10, 20); return 0; } |
(a) | (b) |
Diagnostics | |
1>------ Build started: Project: LinkError, Configuration: Debug Win32 ------ 1> file2.cpp 1> file2.obj : error LNK2019: [wrap] unresolved external symbol "void __cdecl foo(int,int)" [wrap] (?foo@@YAXHH@Z) referenced in function _main 1>E:\tmp\cs1410.2\LinkError\Debug\LinkError.exe : fatal error LNK1120: 1 unresolved externals ========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ========== |
|
(c) |
file1: Definition | file2: Call |
---|---|
#include <iostream> using namespace std; void foo(int x, int y, int z) { cout << x << " " << y << " " << z << endl; } |
#include <iostream> using namespace std; void Foo(int x, int y, int z); // incorrect prototype int main() { Foo(10, 20, 30); return 0; } |
(a) | (b) |
Diagnostics | |
1>------ Rebuild All started: Project: LinkError, Configuration: Debug Win32 ------ 1> file2.cpp 1> file1.cpp 1> Generating Code... 1>file2.obj : error LNK2019: [wrap] unresolved external symbol "void __cdecl Foo(int,int,int)" [wrap] (?Foo@@YAXHHH@Z) referenced in function _main 1>E:\tmp\cs1410.2\LinkError\Debug\LinkError.exe : fatal error LNK1120: unresolved externals ========== Rebuild All: 0 succeeded, 1 failed, 0 skipped ========== |
|
(c) |
Labeling the last section as a "Solution" is presumptuous. No programming language or construct can prevent a determined programmer from writing buggy code! Nevertheless, there are steps that we can take to help minimize the impact of prototype errors and make them easier to correct. Chapter 5 version of the Time example demonstrated a program with one header and two source code files. Although that header file contained a structure specification and function prototypes, it's possible to create header files containing only function prototypes.
void foo(int x, int y, int z); |
#include <iostream> #include "example.h" using namespace std; int main() { foo(10, 20); // link error Foo(10, 20, 30); // link error return 0; } |
(a: example.h) | |
#include <iostream> #include "example.h" using namespace std; void foo(int x, int y, int z) { cout << x << " " << y << " " << z << endl; } |
|
(b: file1.cpp) | (c: file2.cpp) |
#include "example.h"
directive may follow the namespace statement. Stylistically, I like to keep my include directives together. However, if the prototype depends on a feature declared in another header file, the include directive must follow the feature's include.