案例1:函数的多个定义
module1.cpp:
void f(){}
main.cpp中:
void f(){} // error LNK2005: "void __cdecl f(void)" (?func@@YAXXZ) already defined in module1.obj
int main(){}
案例2:类的多个定义
module1.cpp:
class C{};
main.cpp中:
class C{}; // OK
int main(){}
在案例1 中,正如预期的那样,(Microsoft)链接器遇到同一函数的两个定义并发出错误。在案例2 中,它允许同一类的两个定义。
问题1:为什么链接器在同一个类的多个定义时不会抱怨?它是否与函数名称只是其指令开始和类的地址名称有关name是新类型的名称?
此外,即使我们使用不同的类定义,链接器也不会抱怨(我添加了调用类构造函数的函数,因此它们出现在符号表中):
module1.cpp:
class MyClass
{
int n;
public:
MyClass() : n(123){}
};
void func()
{
MyClass c;
}
main.cpp中:
class MyClass
{
float n;
public:
MyClass() : n(3.14f){}
};
int main()
{
MyClass c;
}
我设置了编译器选项,因此它会在COD
个文件中生成OBJ
个文件。我可以看到两个构造函数都出现在相同的错位名称(??0MyClass@@QAE@XZ
)下,每个都在它自己的单元(COD
文件)中。如果模块中引用了某个符号,则链接器将使用来自同一模块的定义(如果存在)。如果没有,它将使用定义它的模块中的符号定义。这可能很危险,因为链接器似乎从它遇到的第一个目标文件中选择了符号:
module1.h:
#ifndef MODULE1_H_
#define MODULE1_H_
void func1();
#endif
module1.cpp:
#include <iostream>
#include "module1.h"
class MyClass
{
int myValue;
public:
MyClass() : myValue(123)
{
std::cout << "MyClass::MyClass() [module1]" << std::endl;
}
void foo()
{
std::cout << "MyClass::foo() [module1]: n = " << myValue << std::endl;
}
};
void func1()
{
MyClass c;
c.foo();
}
module2.cpp:
#include <iostream>
class MyClass
{
public:
MyClass()
{
std::cout << "MyClass::MyClass() [module2]" << std::endl;
}
};
// it is necessary that module contains at least one function that creates MyClass object
void test2()
{
MyClass c;
}
main.cpp中:
#include "module1.h"
int main()
{
func1();
}
如果目标文件在传递给链接器时按此顺序列出:
module2.obj module1.obj main.obj
链接器将从第一个obj文件中选择构造函数MyClass::MyClass
,但从第二个文件中选择MyClass::foo
,因此输出是意外的(错误的):
MyClass :: MyClass()[module2]
MyClass :: foo()[module1]:n = 1
如果目标文件在传递给链接器时按此顺序列出:
module1.obj module2.obj main.obj
链接器将从第一个obj文件中选择MyClass
个成员:
MyClass :: MyClass()[module1]
MyClass :: foo()[module1]:n = 123
问题2:为什么链接器的设计方式允许多个类定义导致上述错误?链接过程取决于目标文件的顺序是不是错了?
链接器似乎选择扫描目标文件时遇到的第一个符号定义,然后以静默方式丢弃所有后续定义重复项。
问题3:这是链接器构建其符号查找表的方式吗?
答案 0 :(得分:4)
关于您的问题1:只要您不违反单一定义规则(ODR),就允许对类和内联函数进行多重定义。
在类中定义函数时,隐式inline
。您通过使用MyClass
的构造函数违反ODR来调用未定义的行为。
这种行为的基本原理是:当一个类中有一个内联函数时,它在许多编译单元中都可见,没有编译单元显然是“首选”编译单元。但是,您的工具链可以依赖于ODR,并假设所有内联方法都具有相同的语义。因此,链接器可以在链接中选择任何内联函数定义,因为它们都是相同的。
问题的解决方案很简单:不要违反ODR。
答案 1 :(得分:3)
Q1:因为函数定义为链接生成符号而类定义不生成。
请注意,在一般情况下并非如此,因为某些函数可能不参与链接(例如,全局和静态关键字),而某些类可能间接参与(例如,使用虚方法或静态变量)。
Q2,Q3:链接器仅适用于符号名称;它不知道符号是变量还是函数或其他东西。它需要一组由编译器生成的模块M1,M2,M3,...,Mn。这可能是彼此不了解的不同编译器。每个模块可以包含例如符号Mi.A
,Mi.B
,Mi.C
,Mi.foo
并且可以引用外部符号,例如??.E
,??.F
,??.G
,??.printf
。 (链接器还使用只是模块存档的库。)
链接器的工作是通过查找包含具有该名称的符号的模块来解析每个外部符号引用。
例如,如果M1包含main
并且引用??.printf
和??.foo
,并且M2包含foo
,则链接器将替换??.foo
的所有引用地址为M2.foo
的地址,以及地址为??.printf
的{{1}}的所有引用。
这基本上就是链接器所做的一切 - 将模块合并为一个二进制文件,将每个对符号的引用替换为其最终的内存地址,抛弃未使用的符号。