74

SO で似たようなタイトルの質問をいくつか見つけましたが、回答を読んだとき、それらは質問のさまざまな部分に焦点を当てていましたが、それらは本当に具体的なものでした (例: STL/コンテナ)。

ポリモーフィズムを実装するためにポインター/参照を使用する必要がある理由を教えてください。ポインターが役立つことは理解できますが、確かに参照は値渡しと参照渡しを区別するだけですか??

確かに、ヒープにメモリを割り当てている限り-動的バインディングを行うことができれば、これで十分でした-明らかにそうではありません。

4

6 に答える 6

64

「ヒープにメモリを割り当てている限り」-メモリが割り当てられている場所は関係ありません。それはすべてセマンティクスに関するものです。たとえば、次のようにします。

Derived d;
Base* b = &d;

dはスタック (自動メモリ) にありますが、ポリモーフィズムは引き続きb.

基本クラス ポインターまたは派生クラスへの参照がない場合、派生クラスがなくなるため、ポリモーフィズムは機能しません。取った

Base c = Derived();

cオブジェクトは ではなくDerivedです。スライスBaseのためです。したがって、技術的には、ポリモーフィズムは引き続き機能しますが、話すオブジェクトがなくなっただけです。Derived

今取る

Base* c = new Derived();

cBaseメモリ内のどこかを指しているだけで、それが実際にかかはあまり気にしませんDerivedが、メソッドの呼び出しはvirtual動的に解決されます。

于 2013-03-03T18:10:04.450 に答える
59

C++ では、オブジェクトは常にコンパイル時に既知の固定の型とサイズを持ち、(そのアドレスを取得でき、取得した場合) 存続期間中は常に固定のアドレスに存在します。これらは C から継承された機能であり、両方の言語を低レベルのシステム プログラミングに適したものにするのに役立ちます。(ただし、これはすべて as-if ルールの対象です: 適合コンパイラは、保証されている適合プログラムの動作に検出可能な影響を及ぼさないことが証明されている限り、コードで好きなことを自由に行うことができます。標準で。)

C++のvirtual関数は、オブジェクトの実行時の型に基づいて実行されるものとして定義されます (多かれ少なかれ、極端な言語弁護士は必要ありません)。オブジェクトで直接呼び出された場合、これは常にオブジェクトのコンパイル時の型になるため、virtual関数がこの方法で呼び出された場合、ポリモーフィズムはありません。

これは必ずしもそうである必要はないことに注意してください。関数を持つオブジェクト型は、通常、各型に固有virtualの関数テーブルへのオブジェクトごとのポインターを使用して C++ で実装されます。virtualもしそうなら、C++ の仮想バリアントのコンパイラは、オブジェクトの内容とテーブル ポインタの両方を一緒にコピーするように、オブジェクト ( など) に代入を実装できますBase b; b = Derived()virtualBaseDerivedは同じサイズでした。2 つのサイズが同じでない場合、コンパイラは、プログラム内のメモリを再配置し、そのメモリへのすべての可能な参照を更新するために、任意の時間プログラムを一時停止するコードを挿入することさえできます。プログラムのセマンティクスに検出可能な影響がないことが証明され、そのような再配置が見つからない場合はプログラムを終了します。ただし、これは非常に非効率的であり、停止することを保証することはできません。代入演算子にとって明らかに望ましくない機能です持ってる。

したがって、上記の代わりに、オブジェクトへの参照とポインターが宣言されたコンパイル時の型とそのサブタイプのオブジェクトを参照およびポイントできるようにすることで、C++ でのポリモーフィズムが実現されます。関数が参照またはポインターを介して呼び出され、virtual参照または指し示されるオブジェクトが、その関数の特定の既知の実装を持つランタイム型であることをコンパイラーが証明できない場合virtual、コンパイラーは正しいものを検索するコードを挿入します。virtualランタイムを呼び出す関数。このようにする必要はありませんでした: 参照とポインタは、非ポリモーフィック (宣言された型のサブタイプを参照またはポイントすることを許可しない) として定義でき、プログラマはポリモーフィズムを実装する別の方法を考え出す必要がありました。 . 後者は、常に C で行われているため、明らかに可能ですが、その時点では、新しい言語を使用する理由はあまりありません。

要するに、C++ のセマンティクスは、オブジェクト指向ポリモーフィズムの高レベルの抽象化とカプセル化を可能にする一方で、(低レベルのアクセスやメモリの明示的な管理などの) 機能を保持しながら、次の用途に適しているように設計されています。低レベルの開発。他のセマンティクスを持つ言語を簡単に設計できますが、それは C++ ではなく、さまざまな利点と欠点があります。

于 2013-03-03T20:34:59.680 に答える
14

次のように代入するときにコピー コンストラクターが呼び出されることを理解すると、非常に役立ちます。

class Base { };    
class Derived : public Base { };

Derived x; /* Derived type object created */ 
Base y = x; /* Copy is made (using Base's copy constructor), so y really is of type Base. Copy can cause "slicing" btw. */ 

y は元のオブジェクトではなく Base クラスの実際のオブジェクトであるため、これに対して呼び出される関数は Base の関数です。

于 2015-06-21T15:19:03.453 に答える
4

リトル エンディアン アーキテクチャを検討してください。値は下位バイトが最初に格納されます。そのため、指定された符号なし整数の場合、0 ~ 255 の値が値の最初のバイトに格納されます。任意の値の下位 8 ビットにアクセスするには、そのアドレスへのポインターが必要です。

uint8したがって、クラスとして実装できます。uint8のインスタンスが ... 1 バイトであることはわかっています。そこから派生して 、 などを生成する場合uint16uint32インターフェース抽象化のために同じままですが、最も重要な変更の 1 つは、オブジェクトの具体的なインスタンスのサイズです。

もちろん、 と を実装uint8charた場合、同様にサイズは同じになる可能性がありますsint8

ただし、operator=とは異なる量のデータを移動しようとしていますuint8uint16

ポリモーフィック関数を作成するには、次のいずれかができる必要があります。

a/ データを正しいサイズとレイアウトの新しい場所にコピーすることにより、引数を値で受け取ります。 b/ オブジェクトの場所へのポインタを取得します。 c/ オブジェクト インスタンスへの参照を取得します。

テンプレートを使用して a を実現できるため、多態性uint128ポインターや参照なしで機能しますが、テンプレートをカウントしない場合は、実装して関数に渡すとどうなるかを考えてみuint8ましょう ? 回答: 128 ではなく 8 ビットがコピーされます。

では、ポリモーフィック関数を受け入れるようuint128にして、それをuint8. コピーしていたものが残念ながら見つかった場合uint8、関数は 128 バイトをコピーしようとし、そのうち 127 バイトはアクセス可能なメモリの範囲外でした -> クラッシュします。

次の点を考慮してください。

class A { int x; };
A fn(A a)
{
    return a;
}

class B : public A {
    uint64_t a, b, c;
    B(int x_, uint64_t a_, uint64_t b_, uint64_t c_)
    : A(x_), a(a_), b(b_), c(c_) {}
};

B b1 { 10, 1, 2, 3 };
B b2 = fn(b1);
// b2.x == 10, but a, b and c?

編纂された時点fnでは、 の知識はありませんでしBた。ただし、Bは から派生しているため、ポリモーフィズムによりでA呼び出すことができるはずです。ただし、返されるオブジェクトは、単一の int で構成される必要があります。fnBA

Bこの関数にのインスタンスを渡すと、返される{ int x; }のは a、b、c のない a だけです。

これが「スライス」です。

ポインターと参照を使用しても、これを無料で回避することはできません。検討:

std::vector<A*> vec;

このベクトルの要素は、 へのポインタAまたは から派生したものである可能性がありますA。言語は通常、「vtable」を使用してこれを解決します。これは、オブジェクトのインスタンスへの小さな追加であり、型を識別し、仮想関数の関数ポインターを提供します。次のように考えることができます。

template<class T>
struct PolymorphicObject {
    T::vtable* __vtptr;
    T __instance;
};

すべてのオブジェクトが独自の vtable を持つのではなく、クラスがそれらを持ち、オブジェクト インスタンスは単に関連する vtable を指します。

問題はスライスではなく、型の正確さです。

struct A { virtual const char* fn() { return "A"; } };
struct B : public A { virtual const char* fn() { return "B"; } };

#include <iostream>
#include <cstring>

int main()
{
    A* a = new A();
    B* b = new B();
    memcpy(a, b, sizeof(A));
    std::cout << "sizeof A = " << sizeof(A)
        << " a->fn(): " << a->fn() << '\n';
}          

http://ideone.com/G62Cn0

sizeof A = 4 a->fn(): B

私たちがすべきだったのはa->operator=(b)

http://ideone.com/Vym3Lp

ただし、これは A を A にコピーしているため、スライスが発生します。

struct A { int i; A(int i_) : i(i_) {} virtual const char* fn() { return "A"; } };
struct B : public A {
    int j;
    B(int i_) : A(i_), j(i_ + 10) {}
    virtual const char* fn() { return "B"; }
};

#include <iostream>
#include <cstring>

int main()
{
    A* a = new A(1);
    B* b = new B(2);
    *a = *b; // aka a->operator=(static_cast<A*>(*b));
    std::cout << "sizeof A = " << sizeof(A)
        << ", a->i = " << a->i << ", a->fn(): " << a->fn() << '\n';
}       

http://ideone.com/DHGwun

(iはコピーされますが、Bjは失われます)

ここでの結論は、元のインスタンスには、コピーが相互作用する可能性のあるメンバーシップ情報が含まれているため、ポインター/参照が必要であるということです。

しかし、そのポリモーフィズムは C++ 内で完全に解決されているわけではなく、スライスを生成する可能性のあるアクションを提供/ブロックする義務があることを認識しておく必要があります。

于 2015-06-21T20:06:26.170 に答える
2

関心のある種類のポリモーフィズム (*) では、動的型が静的型と異なる可能性があるため、ポインターまたは参照が必要です。つまり、オブジェクトの真の型が宣言された型とは異なる必要があります。C++ では、ポインターまたは参照でのみ発生します。


(*) テンプレートによって提供される多態性の一種であるジェネリシティには、ポインターも参照も必要ありません。

于 2013-03-03T18:28:29.257 に答える
0

オブジェクトが値渡しされると、通常はスタックに置かれます。スタックに何かを置くには、それがどれだけ大きいかを知る必要があります。ポリモーフィズムを使用する場合、着信オブジェクトが特定の機能セットを実装していることはわかっていますが、通常、オブジェクトのサイズはわかりません (また、必ずしもそれが利点の一部である必要はありません)。したがって、スタックに置くことはできません。ただし、ポインターのサイズは常にわかっています。

現在、すべてがスタックにあるわけではなく、他にも酌量すべき状況があります。仮想メソッドの場合、オブジェクトへのポインターは、メソッドの場所を示すオブジェクトの vtable へのポインターでもあります。これにより、コンパイラは、操作対象のオブジェクトに関係なく、関数を見つけて呼び出すことができます。

もう 1 つの原因は、オブジェクトが呼び出しライブラリの外部で実装され、完全に異なる (おそらく互換性のない) メモリ マネージャーが割り当てられていることが非常に多いことです。また、コピーできないメンバーや、別のマネージャーでコピーした場合に問題が発生する可能性もあります。コピーには副作用やその他のあらゆる種類の合併症が発生する可能性があります。

その結果、ポインターはオブジェクトに関する唯一の情報であり、必要な他のビットがどこにあるかを把握するのに十分な情報を提供します。

于 2013-03-03T18:09:35.060 に答える