SOと同様のタイトルでいくつかの質問をすでに見つけましたが、答えを読んだとき、彼らは本当に特定の質問の異なる部分に焦点を合わせていました(例えば、STL /コンテナ)。
ポリモーフィズムを実装するためにポインタ/参照を使用する必要がある理由を誰か教えてください。ポインタが役立つかもしれないと理解できますが、参照は確かに値渡しと参照渡しを区別するだけですか??
動的にバインディングできるように、ヒープにメモリを割り当てる限り、これで十分でした-明らかにそうではありませんでした。
C++では、オブジェクトは常にコンパイル時に既知の固定のタイプとサイズを持ち、(そのアドレスを取得できる場合は)その存続期間中、常に固定アドレスに存在します。これらは、Cから継承された機能で、両方の言語を低レベルのシステムプログラミングに適したものにします。 (ただし、これらはすべてas-ifルールの対象です:適合コンパイラは、保証されている適合プログラムの動作に検出可能な影響がないことが証明できる限り、コードを自由に実行できます。標準で。)
C++のvirtual
関数は、オブジェクトの実行時の型に基づいて実行するものとして定義されています(多かれ少なかれ、極端な言語弁護士の必要はありません)。オブジェクトで直接呼び出された場合、これは常にオブジェクトのコンパイル時の型になるため、virtual
関数がこの方法で呼び出されてもポリモーフィズムはありません。
これは必ずしもそうである必要はないことに注意してください:virtual
関数を持つオブジェクト型は、通常、それぞれに固有のvirtual
関数のテーブルへのオブジェクトごとのポインターでC++に実装されますタイプ。そのような傾向がある場合、C++の仮想バリアントのコンパイラは、オブジェクトの内容とvirtual
テーブルポインターの両方をコピーするオブジェクト(Base b; b = Derived()
など)の割り当てを実装できます。 Base
とDerived
の両方が同じサイズであれば、簡単に機能します。 2つのサイズが同じではない場合、コンパイラは、プログラム内のメモリを再配置し、そのメモリへの可能な参照をすべて更新するために、プログラムを任意の時間停止するコードを挿入することさえできますプログラムのセマンティクスに検出可能な効果がないことが証明されており、そのような再配置が見つからない場合はプログラムを終了します:これは非常に非効率的ですが、停止することは保証できず、割り当て演算子にとって明らかに望ましくない機能です持ってる。
したがって、上記の代わりに、C++のポリモーフィズムは、オブジェクトへの参照とポインタが、宣言されたコンパイル時型およびそのサブタイプのオブジェクトを参照およびポイントできるようにすることで実現されます。 virtual
関数が参照またはポインターを介して呼び出され、コンパイラが、参照または指定されたオブジェクトが、そのvirtual
関数の特定の既知の実装を持つ実行時型であることを証明できない場合、コンパイラはランタイムを呼び出すために正しいvirtual
関数を検索するコードを挿入します。参照とポインタは非ポリモーフィックであると定義されている可能性があり(宣言されたタイプのサブタイプを参照またはポイントできないように)、プログラマにポリモーフィズムを実装する代替方法を考えさせる。後者はCで常に行われているため、明らかに可能ですが、その時点で新しい言語を使用する理由はほとんどありません。
要するに、C++のセマンティクスは、オブジェクト指向ポリモーフィズムの高レベルの抽象化とカプセル化を可能にするように設計されていますが、C++に適した機能(低レベルアクセスや明示的なメモリ管理など)低レベルの開発。他のセマンティクスを持つ言語を簡単に設計できますが、C++ではなく、さまざまな利点と欠点があります。
「ヒープにメモリを割り当てる限り」-メモリが割り当てられる場所は、それとは何の関係もありません。それはすべてセマンティクスに関するものです。例えば:
Derived d;
Base* b = &d;
d
はスタック(自動メモリ)にありますが、ポリモーフィズムはb
でも機能します。
基本クラスポインターまたは派生クラスへの参照がない場合、派生クラスがないため、ポリモーフィズムは機能しません。取る
Base c = Derived();
c
オブジェクトはDerived
ではなく、スライスのため、Base
です。したがって、技術的には、ポリモーフィズムは引き続き機能します。それは、あなたがDerived
オブジェクトについて話す必要がなくなったということです。
さあ
Base* c = new Derived();
c
はメモリ内のある場所を指しているだけで、実際にBase
であるかDerived
であるかは気にしませんが、virtual
メソッドの呼び出しは動的に解決されました。
このように割り当てると、コピーコンストラクターが呼び出されることを理解しておくと、非常に役立ちます。
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の関数です。
リトルエンディアンアーキテクチャを検討してください。値は最初に低位バイトに格納されます。したがって、指定された符号なし整数の場合、値の最初のバイトに0〜255の値が格納されます。任意の値の下位8ビットにアクセスするには、単にそのアドレスへのポインターが必要です。
したがって、クラスとして_uint8
_を実装できます。 _uint8
_のインスタンスは... 1バイトであることを知っています。それから派生して_uint16
_、_uint32
_などを生成する場合、interfaceは抽象化のために同じままですが、最も重要な変更の1つは、オブジェクトの具体的なインスタンスのサイズです。
もちろん、_uint8
_とchar
を実装した場合、サイズは同じである可能性があり、同様に_sint8
_です。
ただし、_operator=
_の_uint8
_と_uint16
_は、異なる量のデータを移動します。
多態性関数を作成するには、次のいずれかが可能でなければなりません。
a /データを正しいサイズとレイアウトの新しい場所にコピーして値で引数を受け取る、b /オブジェクトの場所へのポインタを取得する、c /オブジェクトインスタンスへの参照を取得する、
テンプレートを使用してaを実現できるため、ポリモーフィズムcanはポインターと参照なしで機能しますが、テンプレートをカウントしていない場合は、 _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
から派生しているため、ポリモーフィズムにより、fn
を使用してB
を呼び出すことができます。ただし、返されるobjectは、単一のintで構成されるA
である必要があります。
B
のインスタンスをこの関数に渡す場合、返されるのはa、b、cなしの_{ int x; }
_だけです。
これが「スライス」です。
ポインターと参照を使用しても、これを無料で回避することはありません。考慮してください:
_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';
}
_
_sizeof A = 4 a->fn(): B
_
すべきことはa->operator=(b)
を使用することです
ただし、これも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';
}
_
(i
はコピーされますが、Bのj
は失われます)
ここでの結論は、元のインスタンスにはmembership情報が含まれているため、コピーが相互作用する可能性があるため、ポインター/参照が必要であるということです。
しかし、そのポリモーフィズムはC++内で完全に解決されるわけではなく、スライスを生成する可能性のあるアクションを提供/ブロックする義務を認識している必要があります。
興味のあるポリモーフィズムの種類(*)の場合、動的型が静的型と異なる可能性がある、つまりオブジェクトの真の型が宣言された型と異なることが必要なため、ポインターまたは参照が必要です。 C++では、ポインターまたは参照でのみ発生します。
(*)テンプレートによって提供されるポリモーフィズムのタイプである汎用性には、ポインターも参照も必要ありません。
オブジェクトが値で渡されると、通常はスタックに置かれます。スタックに何かを置くには、それがどれだけ大きいかについての知識が必要です。ポリモーフィズムを使用する場合、着信オブジェクトが特定の機能セットを実装することを知っていますが、通常、オブジェクトのサイズはわかりません(また、必ずしもそれがメリットの一部である必要はありません)。したがって、スタックに置くことはできません。ただし、ポインタのサイズは常に知っています。
さて、すべてがスタック上にあるわけではなく、他の厄介な状況があります。仮想メソッドの場合、オブジェクトへのポインターは、メソッドの場所を示すオブジェクトのvtableへのポインターでもあります。これにより、コンパイラは、どのオブジェクトを使用しているかに関係なく、関数を見つけて呼び出すことができます。
別の原因は、オブジェクトが呼び出しライブラリの外部で実装され、完全に異なる(場合によっては互換性のない)メモリマネージャで割り当てられることが非常に多いことです。また、コピーできないメンバーを持つことも、別のマネージャーでコピーされた場合に問題が発生することもあります。コピーやその他のあらゆる種類の合併症に副作用が生じる可能性があります。
その結果、ポインターは、オブジェクト上で実際に適切に理解している唯一の情報であり、必要な他のビットがどこにあるかを把握するのに十分な情報を提供します。