例子1:
#Math.cpp
#include <iostream>
void Log(const char* message);
int Multiply(int a, int b)
{
Log("Multiply");
return a * b;
}
int main()
{
//std::cout << Multiply(5, 8) << std::endl;
std::cin.get();
}
#Log.cpp
#include <iostream>
void Logr(const char* message)
{
std::cout << message << std::endl;
}
生成项目时的报错信息:
1>------ 已启动生成: 项目: HelloWorld, 配置: Debug Win32 ------
1> Math.cpp
1>Math.obj : error LNK2019: 无法解析的外部符号 "void __cdecl Log(char const *)" (?Log@@YAXPBD@Z),该符号在函数 "int __cdecl Multiply(int,int)" (?Multiply@@YAHHH@Z) 中被引用
1>****: fatal error LNK1120: 1 个无法解析的外部命令
========== 生成: 成功 0 个,失败 1 个,最新 0 个,跳过 0 个 ==========
编译时无报错:
1>------ 已启动生成: 项目: HelloWorld, 配置: Debug Win32 ------
1> Math.cpp
========== 生成: 成功 1 个,失败 0 个,最新 0 个,跳过 0 个 ==========
以上代码可以发现,Log函数的名字写成了Logr,这是错误所在。
但是,如果只对Math.cpp文件编译,没有报错,说明编译过程只负责检查语法是否有错,对于是否有调用错误的函数是检查不出来的;
生成项目包含编译和链接,既然编译没有问题,那么问题肯定出在链接这个过程。
由于文件中会调用另一个文件/库的函数或常量,所以链接过程需要把相关的文件都连接起来,而我们连接的函数找不到(即名字写错或根本没有这个函数),所以会在这个过程报错,报错的标识是LNK,即在链接过程报错。
另外,我们还可以发现,在main函数其实没有调用Multiply函数,也就是没有实际调用Log函数,按理说,就不会出现报错信息,但显然,链接器还是出现报错了。这又如何解释呢?
这是因为虽然在本文件(Math.cpp)下不会调用到Multiply函数,但是不保证在其他文件不调用Multiply函数,所以链接器还是会将Log函数进行连接,从而发现函数名写错。
例子2:
但是,如果给Multiply函数前面添加static, 则Multiply的调用只会被限定在Math.cpp文件中,这样,如果在Math.cpp文件没有被调用,则不会在其他文件被调用,链接器就不必把相关的函数进行链接,也就不会有报错。
Multiply函数添加static后生成没有报错:
1>------ 已启动生成: 项目: HelloWorld, 配置: Debug Win32 ------
1> Math.cpp
1> HelloWorld.vcxproj -> ***\HelloWorld.exe
========== 生成: 成功 1 个,失败 0 个,最新 0 个,跳过 0 个 ==========
例子3:
进一步研究,如果把Log.cpp文件里的Logr函数名修正Log,然后将返回类型改为int, 并在函数后面添加return 0,生成项目依然会有报错。
#include <iostream>
int Log(const char* message)
{
std::cout << message << std::endl;
return 0;
}
#include <iostream>
void Log(const char* message);
static int Multiply(int a, int b)
{
Log("Multiply");
return a * b;
}
int main()
{
std::cout << Multiply(5, 8) << std::endl;
std::cin.get();
}
生成项目时的报错信息:
1>------ 已启动生成: 项目: HelloWorld, 配置: Debug Win32 ------
1> Math.cpp
1>Math.obj : error LNK2019: 无法解析的外部符号 "void __cdecl Log(char const *)" (?Log@@YAXPBD@Z),该符号在函数 "int __cdecl Multiply(int,int)" (?Multiply@@YAHHH@Z) 中被引用
1>*** : fatal error LNK1120: 1 个无法解析的外部命令
========== 生成: 成功 0 个,失败 1 个,最新 0 个,跳过 0 个 ==========
例子4:
如果在Log.cpp写了两个相同的函数,会出现编译错误。
#Log.cpp
#include <iostream>
void Log(const char* message)
{
std::cout << message << std::endl;
}
void Log(const char* message)
{
std::cout << message << std::endl;
}
生成项目时的报错信息:
1>------ 已启动生成: 项目: HelloWorld, 配置: Debug Win32 ------
1> Log.cpp
1>***\log.cpp(10): error C2084: 函数“void Log(const char *)”已有主体
1> ****\log.cpp(4) : 参见“Log”的前一个定义
========== 生成: 成功 0 个,失败 1 个,最新 0 个,跳过 0 个 ==========
错误发生的时候,链接还没开始,编译器可以识别这类错误,编译报错识别标识符是C。
例子5:
如果在重复写的Log函数剪切到Math.cpp文件里,编译可以通过,但是出现链接错误
#include <iostream>
void Log(const char* message);
void Log(const char* message)
{
std::cout << message << std::endl;
}
static int Multiply(int a, int b)
{
Log("Multiply");
return a * b;
}
int main()
{
std::cout << Multiply(5, 8) << std::endl;
std::cin.get();
}
生成项目时的报错信息:
>------ 已启动生成: 项目: HelloWorld, 配置: Debug Win32 ------
1> Math.cpp
1> Log.cpp
1> 正在生成代码...
1>Math.obj : error LNK2005: "void __cdecl Log(char const *)" (?Log@@YAXPBD@Z) 已经在 Log.obj 中定义
1>****\HelloWorld.exe : fatal error LNK1169: 找到一个或多个多重定义的符号
========== 生成: 成功 0 个,失败 1 个,最新 0 个,跳过 0 个 ==========
可以观察到,链接错误识别标识符是LNK,编译可以通过,但是在链接过程中,出现了两个相同的函数,不知道该链接哪个,出现报错。
例子6:
再来看另一个例子。添加一个头文件Log.h,并且在Log.cpp和Math.cpp都申明该头文件,并调用它。
#Math.cpp
#include <iostream>
#include "Log.h"
static int Multiply(int a, int b)
{
Log("Multiply");
return a * b;
}
int main()
{
std::cout << Multiply(5, 8) << std::endl;
std::cin.get();
}
#Log.cpp
#include <iostream>
#include "Log.h"
void InitLog()
{
Log("Initialized Log");
}
#pragma once
void Log(const char* message)
{
std::cout << message << std::endl;
}
生成项目时的报错信息:
1>------ 已启动生成: 项目: HelloWorld, 配置: Debug Win32 ------
1> Math.cpp
1>Math.obj : error LNK2005: "void __cdecl Log(char const *)" (?Log@@YAXPBD@Z) 已经在 Log.obj 中定义
1>******\HelloWorld.exe : fatal error LNK1169: 找到一个或多个多重定义的符号
========== 生成: 成功 0 个,失败 1 个,最新 0 个,跳过 0 个 ==========
明明只有一个Log函数的定义,为什么会报错有多重符号呢?
include语句的工作原理:当我们包含头文件时,取头文件的内容,粘贴到include语句的位置。
所以,实际情况是,Log函数被放在了Log.cpp文件和Math.cpp文件里,这就回到了例子5的情况,出现链接错误。
难道定义了一个头文件,就不能同时有两个文件同时调用吗?其实不然,可以将这个Log函数标记为静态的(static),意味着在链接这个函数时,log函数只能分别是Log.cpp和Math.cpp文件里的内部函数
#pragma once
static void Log(const char* message)
{
std::cout << message << std::endl;
}
当然,另一个相似的办法是,标记Log函数为内联函数(inline):
#pragma once
inline void Log(const char* message)
{
std::cout << message << std::endl;
}
内联函数:表示我们获取实际的函数体并将函数调用替换为函数体。
其实,以上报错的主要原因是log函数的定义被写在了两个cpp文件里,所以在连接时会出现多重符号报错。那么有没有一种方法另Log函数只在一个cpp文件里被定义?
答案是有的,这种解决方案是:
将Log函数挪到Log.cpp中,Log.h头文件只保留一行关于这个函数的申明语句,这样的话,在Log.cpp和Math.cpp文件中都申明了该函数,相而函数Log只在Log.cpp文件中被定义,所以,链接时只有一个Log函数体出现,没有报错。
#Log.cpp
#include <iostream>
#include "Log.h"
void InitLog()
{
Log("Initialized Log");
}
void Log(const char* message)
{
std::cout << message << std::endl;
}
#Log.h
#pragma once
void Log(const char* message);
总结
- 编译不会检查函数体调用的错误。
- 链接过程会把显式调用的函数全部连接起来,如果发现函数名(或其他形式)写错,则会报错。
- static可以限定函数只允许在本文件下被调用,不允许被外部文件调用。
- 链接函数过程中,函数的名字、函数的返回类型以及函数的参量一定要一致才行。
- 如果在同一个文件中出现相同的函数名和函数体,编译器可以识别出这类错误,出现编译错误。
- 如果在主文件和被调用文件里出现相同的函数名和函数体,编译器识别不出这类错误,但链接器可以,因为链接器找到了两个相同的函数,不知道该连接哪一个。
- 如果两个文件同时申明一个头文件,需要将头文件里的函数标记为静态的(static)或者内联函数(inline)。
- 在将所有的头文件内容全部粘贴到各自的cpp文件里后,要确保每个函数都是唯一的,在同一文件里唯一且在不同文件之间也是唯一的(不同文件里相同函数可由static和inline来赋予其唯一性)