文字通り、複数の形式を意味します。ポリモーフィズムという言葉はもともとギリシャ語に由来しており、その意味は「多くの形式」であり、C++ 言語ではより広い意味を持ちます。 C++ 入門書では、継承関係を持つ複数の型をポリモーフィック型と呼びます。これは、これらの型の違いを気にせずに「複数の形式」を使用できるためです。 Baidu Encyclopedia には、オブジェクト指向言語では、インターフェースのさまざまな実装がポリモーフィズムであると記載されています。引用Charlie Calverts によるポリモーフィズムの説明 - ポリモーフィズムは、親オブジェクトを 1 つ以上の子オブジェクトと等しくなるように設定できる技術であり、代入後、現在のオブジェクトに従って親オブジェクトをその子に割り当てることができます。値。オブジェクトのプロパティはさまざまな方法で動作します。簡単に言うと、「サブクラス型のポインタを親クラス型のポインタに代入することが許可されている」という一文です。ポリモーフィズムは、ObjectPascal および C++ で仮想関数を介して実装されます。
概念的な説明だけでは深く明確に理解することは不可能です。以下で詳しく分析してみましょう。
ここで説明するオブジェクト型は、次の図に反映されます:コード例を通して説明しましょう
1 class Derived1:public Base 2 {}; 3 class Derived2:public Base 4 {}: 5 int main() 6 { 7 Derived1* p1 = new Derived1; 8 Base = p1; 9 Derived2* p2 = new Derived1;10 Base = p2;11 return = p212 }
static型はコンパイル時に常に認識されます。variableが宣言されている場合の型、またはexpressionによって生成された型; 動的型は、変数または式によって表されるメモリ内のオブジェクトの型です。動的型は実行時までわかりません。
静的ポリモーフィズムと動的ポリモーフィズムの違いは、次の図に反映されています:
静的ポリモーフィズムは、静的バインディングまたは早期バインディング タイとも呼ばれます。コンパイラは、コンパイル中にそれを完了します。コンパイラは、関数の実際のパラメータの型に基づいて呼び出す関数を推測できます (対応する関数がある場合は、その関数を呼び出します。そうでない場合は、コンパイル エラーが発生します)。起こる。 。
1 int Add(int left,int right) 2 { 3 return left + right; 4 } 5 float Add(float left, float right) 6 { 7 return left + right; 8 } 9 int main()10 { 11 cout< ログイン後にコピー
を静的ポリモーフィズムとして分類します。オーバーロードの実装は次のとおりです。コンパイラは、関数の異なるパラメータ リストに基づいて同じ名前の関数の名前を変更し、その後、同じ名前のこれらの関数は (少なくともコンパイラにとっては) 別の関数になります。関数呼び出しはコンパイラ間で決定され、静的です (静的であることに注意してください)。言い換えれば、それらのアドレスはコンパイル時にバインドされます (早期バインディング)。まさにこのオーバーロードの性質のため、オーバーロードは単なる言語機能であり、ポリモーフィズムやオブジェクト指向とは何の関係もないと結論付ける人もいます。オブジェクト指向プログラミングをベースにしているため、オーバーロードの概念は「オブジェクト指向プログラミング」には属しません。ただし、ポリモーフィズムは比較的広い概念です。理解を容易にするために、ここではそれらの違いについては詳しく分析しません。多態性の他の部分の詳細な分析が完了した後で説明します。動的ポリモーフィズム動的バインディング: プログラムの実行中 (コンパイル中ではなく)、参照されるオブジェクトの実際の型を決定し、その実際の型に従って対応するメソッドを呼び出します。 virtual キーワードを使用してクラスのメンバー関数を変更する場合、その関数が仮想関数であり、派生クラスを再実装する必要があることを示し、コンパイラは動的バインディングを実装します。
FunTest1( cout << << FunTest2( cout << << FunTest3( cout << << FunTest4( cout << << CDerived : FunTest1( cout << << FunTest2( cout << << FunTest3( _iTest1) { cout << << FunTest4( _iTest1, cout << << CBase* pBase = pBase->FunTest1( pBase->FunTest2( pBase->FunTest3( pBase->FunTest4( }
、静的型関数、フレンド関数、インライン関数、仮想関数
CTest& =( CTest& * fri end voi d FunTestFri end() ; 12 }
其实在前面的虚拟继承中我们已经用到了虚函数这个概念,在那里我们是为了解决菱形普通继承中访问二义性的问题,但在多态中,他有更大的作用。百度百科中对虚函数是这么说的:在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数,用法格式为:virtual 函数返回类型 函数名(参数表) {函数体};实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。形象的解释为“求同存异”,它的作用就是实现多态性。
简单地说,那些被virtual关键字修饰的成员函数,就是虚函数。虚函数的作用,用专业术语来解释就是实现多态性(Polymorphism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异,而采用不同的策略。
1 class A 2 { 3 public: 4 virtual void print(){cout<<"This is A"<print();18 p2->print();19 return 0;20 }
毫无疑问,class A的成员函数print()已经成了虚函数,那么class B的print()成了虚函数了吗?回答是Yes,我们只需在把基类的成员函数设为virtual,其派生类的相应的函数也会自动变为虚函数。所以,class B的print()也成了虚函数。那么对于在派生类的相应函数前是否需要用virtual关键字修饰,那就是你自己的问题了(语法上可加可不加,不加的话编译器会自动加上,但为了阅读方便和规范性,建议加上)。
运行代码,输出的结果是This is A和This is B。
总结:指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。
当在析构函数前面加virtual关键字时报错:,我们来分析一下原因。
1、虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初 始化,将无法进行。
2、构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象 的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。
虚函数的意思就是开启动态绑定,程序会根据对象的动态类型来选择要调用的方法。然而在构造函数运行的时候,这个对象的动态类型还不完整,没有办法确定它到底是什么类型,故构造函数不能动态绑定。(动态绑定是根据对象的动态类型而不是函数名,在调用构造函数之前,这个对象根本就不存在,它怎么动态绑定?)
编译器在调用基类的构造函数的时候并不知道你要构造的是一个基类的对象还是一个派生类的对象。
当我们在静态类型函数前加virtual关键字时报错:分析:
1、static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。
2、static函数没有this指针,并且不会进入虚函数表的。当通过指针或者引用调用时根本无法把this指针传递给static函数,从而无法体现出多态。静态成员函数与普通成员函数的差别就在于缺少this指针,没有这个this指针自然也就无从知道name是哪一个对象的成员了。
当我们在友元函数前加virtual关键字时报错:
因为C++
不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。
内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline
函数在编译时被展开,虚函数在运行时才能动态的邦定函数)
当我们把赋值运算符的重载定义为虚函数时编译可以通过,但是一般不建议这么做虽然可以将operator=定义为虚函数, 但使用时容易混淆。
1、无法给派生类的自有成员赋值;
2、调用虚函数要进行查虚表等一系列操作,效率下降。
析构函数设为虚函数的作用:在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义成虚函数,派生类中派生的那部分无法析构。
1 #include2 #include 3 class A 4 { 5 public: 6 A(); 7 virtual~A(); 8 }; 9 A::A()10 {}11 A::~A()12 {13 printf("Delete class APn");14 }15 class B : public A16 {17 public:18 B();19 ~B();20 };21 B::B()22 { }23 B::~B()24 {25 printf("Delete class BPn");26 }27 int main(int argc, char* argv[])28 {29 A *b=new B;30 delete b;31 return 0;32 }
输出结果为:Delete class B Delete class A
如果把A的virtual去掉:那就变成了Delete class A也就是说不会删除派生类里的剩余部分内容,也即不调用派生类的虚函数
1. 如果我们定义了一个构造函数,编译器就不会再为我们生成默认构造函数了。
2. 编译器生成的析构函数是非虚的,除非是一个子类,其父类有个虚析构,此时的函数虚特性来自父类。
3. 有虚函数的类,几乎可以确定要有个虚析构函数。
4. 如果一个类不可能是基类就不要申明析构函数为虚函数,虚函数是要耗费空间的。
5. 析构函数的异常退出会导致析构不完全,从而有内存泄露。最好是提供一个管理类,在管理类中提供一个方法来析构,调用者再根据这个方法的结果决定下一步的操作。
6. 在构造函数不要调用虚函数。在基类构造的时候,虚函数是非虚,不会走到派生类中,既是采用的静态绑定。显然的是:当我们构造一个子类的对象时,先调用基类的构造函数,构造子类中基类部分,子类还没有构造,还没有初始化,如果在基类的构造中调用虚函数,如果可以的话就是调用一个还没有被初始化的对象,那是很危险的,所以C++中是不可以在构造父类对象部分的时候调用子类的虚函数实现。但是不是说你不可以那么写程序,你这么写,编译器也不会报错。只是你如果这么写的话编译器不会给你调用子类的实现,而是还是调用基类的实现。
7.在析构函数中也不要调用虚函数。在析构的时候会首先调用子类的析构函数,析构掉对象中的子类部分,然后在调用基类的析构函数析构基类部分,如果在基类的析构函数里面调用虚函数,会导致其调用已经析构了的子类对象里面的函数,这是非常危险的。
8. 记得在写派生类的拷贝函数时,调用基类的拷贝函数拷贝基类的部分。
1、 派生类重写基类的虚函数实现多态, 要求函数名 、 参数列表、 返回值完全相同。 (协变除外)。
2、 基类中定义了 虚函数, 在派生类中该函数始终保持虚函数的特性。
3、 只 有类的非静态成员 函数才能定义为虚函数, 静态成员 函数不能定义为虚函数。
4、 如果在类外定义虚函数, 只 能在声明函数时加virtual关键字, 定义时不用加。
5、 构造函数不能定义为虚函数, 虽然可以将operator=定义为虚函数, 但最好不要这么做, 使用时容易混淆。
6、 不要在构造函数和析构函数中调用虚函数, 在构造函数和析构函数中, 对象是不完整的, 可能会出现未定义的行为。
7、 最好将基类的析构函数声明为虚函数。 ( 因为派生类的析构函数跟基类的析构函数名称不一样, 但是构成覆盖, 这里编译器做了特殊处理)
8、 虚表是所有类对象实例共用的。
以上が静的多態性は動的多態性と仮想関数に関連していますの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。