5

友人と私は、オブジェクトの構築について非常に興味深い議論を交わし、最終的に次のコードに行き着きました。

#include <iostream>

class Parent {
  public:
    Parent( ) {
      this->doSomething( );
    }

    virtual void doSomething( ) = 0;
};

class Child : public Parent {
    int param;

  public:
    Child( ) {
      param = 1000;
    }

    virtual void doSomething( ) {
      std::cout << "doSomething( " << param << " )" << std::endl;
    }
};

int main( void ) {
    Child c;
    return 0;
}

純粋な仮想関数がコンストラクタまたはデストラクタから呼び出されたときの動作を標準が定義していないことはわかっています。また、これは本番環境でコードを記述する方法の実際的な例ではありません。コンパイラが何をするかを確認するための単なるテストです。 .

Java プリントで同じ構造をテストする

何かをする( 0 )

親コンストラクターから呼び出されparamた時点で初期化されていないため、これは理にかなっています。doSomething()

C++ でも同様の動作が予想さparamれますが、関数が呼び出された時点で何かが含まれているという違いがあります。

(c++ (Ubuntu 4.4.3-4ubuntu5.1) 4.4.3)代わりに、上記のコードをコンパイルすると、への参照Parent::doSomething( )が未定義であるというリンカ エラーが発生します。

だから、私の質問は:なぜこれはリンカ エラーですか? これがエラーである場合、特に関数の実装があるため、コンパイラが文句を言うと思います。この場合、リンカがどのように機能するか、またはさらに読むための参照についての洞察は高く評価されます。

前もって感謝します!この質問が重複していないことを願っていますが、同様の質問が見つかりませんでした..

4

3 に答える 3

5

コンパイラーdoSomethingは、コンストラクター内から呼び出す場合、その呼び出しdoSomethingが仮想であっても、このクラスで参照する必要があることを認識しています。したがって、仮想ディスパッチを最適化し、代わりに通常の関数呼び出しを実行します。関数はどこにも定義されていないため、リンク時にエラーが発生します。

于 2012-07-11T16:37:20.133 に答える
5

だから、私の質問は: なぜこれはリンカ エラーですか? これがエラーである場合、特に関数の実装があるため、コンパイラが文句を言うと思います。この場合、リンカがどのように機能するか、またはさらに読むための参照についての洞察は非常に高く評価されます。

これをもう少し拡大してみましょう。

なぜリンカエラーなのですか?

コンパイラーParent::doSomething()がコンストラクターからの呼び出しを挿入しましたが、リンカーは関数の定義を検出していないためです。

特に関数の実装があるため、コンパイラが文句を言うと思います。

これは正しくありません。仮想ディスパッチを介してアクセスできる関数のオーバーライドがありますが、関数Parent::doSomething()は定義されていません。微妙ではあるが重要な違いがあり、動的ディスパッチを無効にすることで別の方法でテストできます。関数をクラス名で修飾することにより、特定の呼び出しの動的ディスパッチを無効にすることがChild::doSomething()できParent::doSomething()ますParent::doSomething()

なぜこれが重要なのですか?

関数が純粋仮想(純粋仮想とは、動的ディスパッチがその特定のオーバーロードに決してディスパッチされないことを意味します) であっても、定義して呼び出すこともできるため、重要です。

struct base {
   virtual void f() = 0;
};
inline void base::f() { std::cout << "base\n"; }
struct derived : base {
   virtual void f() {
      base::f();
      std::cout << "derived\n";
   }
};
int main() {
   derived d;
   d.f();        // outputs: base derived
}

現在、C++ には別のコンパイル モデルがあります。つまり、この特定の翻訳単位で関数を定義する必要はありません。つまりParent::doSomething()、同じプログラムにリンクされる別の翻訳単位で定義できます。コンパイラは、他のTUがその関数を定義するかどうかをおそらく知ることができず、リンカだけが知っているため、不平を言うのはリンカです。

この場合、リンカがどのように機能するか、またはさらに読むための参照についての洞察は非常に高く評価されます。

前に述べたように、コンパイラ (この特定のコンパイラ) は、Parentレベルで特定のオーバーライドへの呼び出しを追加しています。リンカーは、他のすべての関数呼び出しと同様に、翻訳単位のいずれかで定義されているシンボルを見つけようとして失敗し、エラーを引き起こします。

純粋仮想指定子は、仮想テーブルの作成時に関数の暗黙的な使用 (標準では odr-use ) を回避することを唯一の目的としています。つまり、純粋な指定子の唯一の目的は、そのシンボルへの依存関係を仮想テーブルに追加することではありません。つまり、リンカーはParent::doSomething、動的ディスパッチ (vtable) の目的でプログラム内にシンボル ( ) が存在する必要はありませんが、プログラムで明示的に使用されている場合は、そのシンボルが必要になります。

于 2012-07-11T16:51:53.030 に答える
0

doSomething() が呼び出されるポインターで、コンパイラーは *this の動的型が Parent であることを確認できるため、関数を間接的に呼び出す必要はありません。この動作は、使用するコンパイラ/リンカーに大きく依存します。

于 2012-07-11T16:37:50.713 に答える