2

Bruce Eckel のコード、C++ で考える

class A {
    int i;
  public:    
    A(int ii) : i(ii) {}
    ~A() {}
    void f() const {}
};

class B {
    int i;          
  public:
    B(int ii) : i(ii) {}
    ~B() {
    void f() const {}
};

class C : public B {
    A a;
  public:
    C(int ii) : B(ii), a(ii) {}
    ~C() {}  // Calls ~A() and ~B()
    void f() const {  // Redefinition
        a.f();
        B::f();
    }
};

int main() {
    C c(47);
}

このコードについて、彼は次のように述べています。

関数 C::f( ) は、継承した B::f( ) を再定義し、基底クラス バージョンも呼び出します。さらに、af( ) を呼び出します。関数の再定義について話すことができるのは継承の間だけであることに注意してください。メンバー オブジェクトを使用すると、オブジェクトのパブリック インターフェイスのみを操作でき、再定義することはできません。

彼はどういう意味ですか?

この関数は、スコープ解決演算子を介してC::f()呼び出しているだけです。これは継承されており、 にも同名の関数が存在するためです。 の関数は、クラスで定義されたオブジェクトを通じて呼び出されます。f()BCAf()C

f()では、エッケルが言う関数の再定義はどこにあるのでしょうか?

4

3 に答える 3

2

クラスCは から派生しているため、関数は独自のバージョンを定義しBて関数をC::f()オーバーライドします。B::f()型のオブジェクトを宣言してCそのf()関数を呼び出すと、 からC::f()完全に独立して実行されますB::f()。そのため、基底クラス関数が再定義されました。

classCには type のメンバーも含まれていることに注意してくださいA。これには関数もあり、それはf()C実装であり、f()たまたま呼び出されますa.f()。そのCため、独自f()の別のインターフェイスを提供できますが、の実装を変更 することはできません。A

于 2013-06-22T17:48:34.497 に答える
1

違いは、 function を記述せに (静的) typeのオブジェクトをC::f呼び出した場合にどうなるかです。fC

C c;
c.f();

が存在しないC::f場合、これは呼び出されますB::f(Bは の基本クラスであるため) が呼び出されますが、呼び出されCませんA::f(これは単なるメンバー オブジェクトであるため)。したがって、関数の存在によりC::f、呼び出しのセマンティクスが変化しますc.f()

ただし、仮想ではないためオーバーライドC::fしないことに注意してください。つまり、次のコードはではなくを呼び出します。 B::fB::fB::fC::f

C c;
B& b(c);
b.f();

B::f仮想の場合、C::fそれをオーバーライドし、そのコードは を呼び出しますC::f。ただし、B::fは仮想でC::fはないため、オーバーライドしないため、上記のコードは を呼び出しますB::f()

ところで、ここでの Bruce Eckel の用語には同意しません。私の見解では、「再定義」は定義を置き換えることを意味します。ただしC::f、 の定義を置き換えるのではなくB::f、単に非表示にします。

于 2013-06-22T17:57:15.570 に答える
0

「C::f」の定義を削除した場合でも、継承されるため、「C::f()」を呼び出すことができます。しかし、この例では、C に "f" の独自の実装を提供することを代わりに選択しています。これにより、タイプ「C」のオブジェクトを扱う人から B::f 関数が「隠蔽」されます。ただし、親型へのダウンキャストを介して型「C」のオブジェクトを表示する人からも定義が隠されるため、「再定義」と呼ばれます。

検討

#include <iostream>
using namespace std;

class A { void x() { cout << "A::x" << endl; } };
class A1 : public A {}; // No redefinition.
class A2 : public A { void x() { cout << "A2::x" << endl; A::x(); } };

int main(int argc, const char** argv)
{
    A1 a1;
    A2 a2;

    cout << "a1: "; a1.x();
    cout << "a2: "; a2.x();

    A2* pa2 = &a2;
    cout << " pa2: "; pa2->x();
    A* pa = (A*)a2;
    cout << "pa: " ; << pa;

    return 0;
}

その最後の、キャストが最も重要な部分です。A2 の "x" を呼び出すには、A2 型のオブジェクトまたはそれを再定義していない子孫を使用する必要があります。以下を作成したとします。

class Account {
   float m_balance;
public:
    Account() : m_balance(0) {}
    void Close() {} // What to do when the account is closing.
}

class SecureAccount : public Account {
    Transaction* m_transactionInProgress;
public:
    SecureAccount() : Account(), m_transactionInProgress(nullptr) {}
    void Close() {
        if(m_transactionInProgress) {
           m_transactionInProgress->Finish();
        }
    }
};

継承の全体的なポイントは、最小公分母を使用できるようにすることです。したがって、この場合、呼び出しを行う可能性が非常に高くなります。

/*account*/p -> Close();

アカウントの「クローズ」の定義のみを表示できます。そのため、ベクトルに SecureAccount* を追加し、まだ開いているトランザクションがある間にそれを閉じようとすると、お金を失うことになります。

これを解決する方法を確認するには、仮想関数の説明を読んでください。

于 2013-06-22T17:52:16.873 に答える