2

案例一:一个函数的多个定义

模块1.cpp:

void f(){}

主.cpp:

void f(){} // error LNK2005: "void __cdecl f(void)" (?func@@YAXXZ) already defined in module1.obj
int main(){} 

案例2:一个类的多个定义

模块1.cpp:

class C{};

主.cpp:

class C{}; // OK
int main(){} 

案例 1中,正如预期的那样,(Microsoft)链接器遇到相同函数的两个定义并发出错误。在案例 2中,它允许同一类的两个定义。

问题 1:为什么在同一个类有多个定义时链接器不报错?这是否与函数名只是其指令开始的地址的名称而类名是新类型的名称这一事实有关?

此外,即使我们使用不同的类定义,链接器也不会抱怨(我添加了调用类构造函数的函数,因此它们出现在符号表中):

模块1.cpp:

class MyClass
{
    int n;
public: 
    MyClass() : n(123){}
};

void func()
{
   MyClass c;
}

主.cpp:

class MyClass
{
   float n;
public: 
   MyClass() : n(3.14f){}
};

int main()
{
   MyClass c;
} 

我设置了编译器选项,以便它COD沿着文件生成OBJ文件。我可以看到两个构造函数都出现在相同的名称(??0MyClass@@QAE@XZ)下,每个都在自己的单元(COD文件)中。如果在模块中引用了某个符号,则链接器将使用同一模块中的定义(如果存在)。如果不是,它将使用定义它的模块中的符号定义。这可能很危险,因为链接器似乎从它遇到的第一个目标文件中选择符号:

模块1.h:

#ifndef MODULE1_H_
#define MODULE1_H_

void func1();

#endif

模块1.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();
}

模块2.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;
}

主.cpp:

#include "module1.h"

int main()
{
    func1();
}

如果目标文件在传递给链接器时按此顺序列出:

module2.obj module1.obj main.obj

MyClass::MyClass链接器将从第一个 obj 文件中选择构造函数,但从MyClass::foo第二个文件中选择构造函数,因此输出是意外的(错误的):

MyClass::MyClass() [module2]
MyClass::foo() [module1]: n = 1

如果目标文件在传递给链接器时按此顺序列出:

module1.obj module2.obj main.obj

MyClass链接器将从第一个 obj 文件中选择两个成员:

MyClass::MyClass() [module1]
MyClass::foo() [module1]: n = 123

问题 2:为什么链接器的设计方式允许多个类定义,这会导致上述错误?链接过程取决于目标文件的顺序,这不是错的吗?

似乎链接器在扫描目标文件时选择了它遇到的第一个符号定义,然后默默地丢弃所有后续定义重复。
问题 3:链接器是这样构建其符号查找表的吗?

4

2 回答 2

4

关于您的问题 1:只要您不违反单一定义规则 (ODR),就允许对类和内联函数进行多重定义。

当你在一个类中定义一个函数时,它是隐式的inline。您通过使用MyClass.

这种行为的基本原理是:当你在一个类中有一个内联函数时,它在许多编译单元中都是可见的,没有编译单元显然是“首选”编译单元。但是,您的工具链可以依赖 ODR 并假设所有内联方法具有相同的语义。因此,链接器可以在链接中选择任何内联函数定义,因为它们都是相同的。

问题的解决方案很简单:不要违反 ODR。

于 2012-05-18T13:13:53.017 回答
3

Q1:因为函数定义会生成链接符号,而类定义不会。

请注意,这在一般情况下是不正确的,因为某些函数可能不参与链接(例如,使用 static 关键字的全局),而某些类可能间接参与(例如,使用虚拟方法或静态变量)。

Q2、Q3:链接器仅适用于符号名称;它不知道符号是变量还是函数或其他东西。它需要一组由编译器生成的模块 M1、M2、M3、...、Mn。那可能是彼此不了解的不同编译器。每个模块可以包含符号,例如,,,并且Mi.A可以引用外部符号,例如,,,,。(链接器还获取只是模块档案的库)。Mi.BMi.CMi.foo??.E??.F??.G??.printf

The linker's job is to resolve each external symbol reference by finding the module that contains symbol with that name.

For example, if M1 contains main and refers to ??.printf and ??.foo, and M2 contains foo, the linker will replace all references of ??.foo with address of M2.foo, and all references of ??.printf with address of standard_c_library.printf.

That's basically all that a linker does — merges modules into a single binary, replaces each reference to a symbol with its final memory address, throws away unused symbols.

于 2012-05-18T13:18:45.017 回答