2

ケース 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 つの定義に遭遇し、エラーを発行します。ケース 2では、同じクラスの 2 つの定義が許可されます。

質問 1: 同じクラスの定義が複数ある場合にリンカが文句を言わないのはなぜですか? 関数名はその命令が始まるアドレスの名前であり、クラス名は新しい型の名前であるという事実に関連していますか?

さらに、クラスの異なる定義を使用してもリンカーは文句を言いません (クラス コンストラクターを呼び出す関数を追加して、それらがシンボル テーブルに表示されるようにしました)。

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

リンカーはMyClass::MyClass最初の obj ファイルからコンストラクターを選択しますがMyClass::foo、2 番目のファイルからコンストラクターを選択するため、出力は予期しない (間違った) ものになります。

MyClass::MyClass() [モジュール 2]
MyClass::foo() [モジュール 1]: n = 1

リンカーに渡すときに、オブジェクト ファイルがこの順序でリストされている場合:

module1.obj module2.obj main.obj

リンカーはMyClass、最初の obj ファイルから両方のメンバーを選択します。

MyClass::MyClass() [モジュール 1]
MyClass::foo() [モジュール 1]: n = 123

質問 2: 上記のエラーにつながる可能性のある複数のクラス定義を許可する方法でリンカーが設計されているのはなぜですか? リンク処理がオブジェクトファイルの順序に依存するのは間違っていませんか?

リンカは、オブジェクト ファイルをスキャンするときに最初に見つけたシンボル定義を選択し、その後のすべての定義の重複を黙って破棄しているようです。
質問 3: これは、リンカがシンボル ルックアップ テーブルを作成する方法ですか?

4

2 に答える 2

4

質問 1 について: 1 つの定義規則 (ODR) に違反しない限り、クラスとインライン関数の複数の定義が許可されます。

クラス内で関数を定義すると、暗黙的にinline. のコンストラクタで ODR に違反することにより、未定義の動作を呼び出しましたMyClass

この動作の理論的根拠は次のとおりです。クラスにインライン関数がある場合、それは多くのコンパイル単位で表示されますが、明らかに「優先」コンパイル単位であるコンパイル単位はありません。ただし、ツールチェーンは ODR に依存し、インライン化されたすべてのメソッドが同じセマンティクスを持つと想定できます。したがって、リンカはインライン化された関数定義のいずれかをリンクで選択できます。これらはすべて同じだからです。

この問題の解決策は簡単です。ODR に違反しないことです。

于 2012-05-18T13:13:53.017 に答える
3

Q1: 関数定義はリンケージ用のシンボルを生成しますが、クラス定義は生成しません。

これは一般的な場合には当てはまらないことに注意してください。一部の関数はリンケージに参加しない場合があり (例: static キーワードを使用するグローバル)、一部のクラスは間接的に参加する場合があります (例: 仮想メソッドまたは静的変数を使用する場合)。

Q2、Q3: リンカーはシンボル名でのみ機能します。シンボルが変数なのか、関数なのか、それとも何か他のものなのかはわかりません。コンパイラによって生成された一連のモジュール M1、M2、M3、...、Mn を取ります。それは、お互いを知らない異なるコンパイラである可能性があります。各モジュールには 、 、 などの記号を含めることができMi.AMi.BMi.C、などのMi.foo外部記号を参照することができ??.Eます。(リンカーは、モジュールの単なるアーカイブであるライブラリも受け取ります)。??.F??.G??.printf

リンカーの仕事は、その名前のシンボルを含むモジュールを見つけて、各外部シンボル参照を解決することです。

たとえば、M1 に と が含まれmainて参照さ??.printf??.foo、M2 に が含まれているfoo場合、リンカは のすべての参照を の??.fooアドレスに置き換え、 のすべてのM2.foo参照を の??.printfアドレスに置き換えますstandard_c_library.printf

モジュールを単一のバイナリにマージし、シンボルへの各参照を最終的なメモリ アドレスに置き換え、未使用のシンボルを破棄します。

于 2012-05-18T13:18:45.017 に答える