C++中的多态问题

    xiaoxiao2021-04-14  79

    多态:即多种形态。

    我们把具有继承关系的多个类型称为多态类型。 引用或指针的静态类型与动态类型不同这一事实正是c++语言支持多态性的根本所在。

    对象类型分为静态类型和动态类型。

    静态类型:对象编译阶段确定类型(又叫早绑定、静态绑定) 动态类型:在程序执行阶段确定对象的类型(又叫晚绑定、动态绑定)

    静态多态:编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推 断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。

    动态绑定:在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方 法。 使用virtual关键字修饰类的成员函数时,指明该函数为虚函数,派生类需要重新实现,编译器将实 现动态绑定。

    动态多态绑定的条件:

    1.必须是虚函数 (在基类中是虚函数->接着在派生类中重写该虚函数) 2.通过基类类型的引用或者指针调用虚函数

    也就是说: 通过基类的指针或引用调用虚函数时,调用基类还是派生类的虚函数,要在运行时根据指针(引用)指向(引用)的类型确定,当调用非虚函数时,无论指向的是哪种类型,一定调用的是基类的函数。

    #define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; class Base { public: /*virtual*/ void Test() { cout << "Base.Test()" << endl; } private: int _b; }; class Derived :public Base { public: /*virtual*/ void Test() { cout << "Derived.Test()" << endl; } private: int _d; }; void test(Base& b) { b.Test(); } int main() { Base b; Derived d; test(b); test(d); return 0; }

    可以看出,由于Test不是虚函数,所以两次输出的结果都是Base.Test()。 当把Test改为虚函数后:

    在进行重写时,在派生类中重写的函数必须满足函数原形相同(包括函数返回值、函数名、函数参数列表),协变除外。 【协变: 基类中的虚函数返回值类型为基类类型的指针或引用,并且派生类中重写的虚函数返回值类型为派生类类型的指针或引用。】 接下来举例验证协变:

    #define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; class Base { public: virtual void Test() { cout << "Base.Test()" << endl; } virtual Base& Test1() { cout << "Base.Test1()" << endl; return *this; } private: int _b; }; class Derived :public Base { public: virtual void Test() { cout << "Derived.Test()" << endl; } virtual Derived& Test1() { cout << "Derived.Test1()" << endl; return *this; } private: int _d; }; void test(Base& b) { b.Test(); b.Test1(); } int main() { Base b; Derived d; test(b); test(d); return 0; }


    虚函数构造的几个情况:

    1. 不要在构造函数和析构函数内部调用虚函数,在构造和析构函数中,对象是不完整的,可能会出现未定义的情况。 2. 例如:在构造函数中,类中有3个成员变量需要初始化,假如在初始化了一个变量后就调用了虚函数,则可能会出现未定义情况。 在析构函数中,假如释放了一块空间后,调用析构函数,也会导致未定义的情况【调用虚函数的前提是对象一定构建成功,获取虚表地址必须通过对象的地址,如果对象还没有创建成功,则无法获取虚表地址,因此,构造函数不能定义成为虚函数。】 3. 在基类中定义了虚函数,则在派生类中该函数始终保持虚函数的特性。 在派生类中重写虚函数时也可以不显示写出virtual关键字,这时编译器会默认该函数为虚函数,为了程序看起来更加清晰,则最好加上virtual关键字。 4. 如果在类外定义虚函数,则只在类中声明时加virtual关键字,在类外定义是不能加virtual关键字。 5. static静态函数不能定义为虚函数 【静态成员时该类的所有对象所共有的,(可以通过类名或作用域限定符调用)可以不构建对象调用其,但是虚表地址必须通过对象的地址才能获取】 6.友元函数不重写 7. 赋值运算符可以重写,但是最好不要对其进行重写 8. 如果在类外定义虚函数,只能在声明函数时加virtual关键字,定义时不用加。 class Base { public: virtual Base& operator=(const Base& b) {} int _b; }; class Derived :public Base { public: virtual Derived& operator=(const Derived& b) { } int _d; }; void Test() { Base b1; Derived d1; Base& b2 = b1; b2 = d1;//会调用基类的赋值运算符重载虚函数 Base& b3 = d1; d1 = b2;//这样就会编译错误(虽然可能调用派生类的赋值运算符重载虚函数),赋值兼容规则不允许。 } int main() { Test(); return 0; }

    纯虚函数

    在成员函数的列表后面写上=0,则成员函数为纯虚函数。 包含纯虚函数的类叫做抽象类(也 叫接口类),抽象类不能实例化出对象。纯虚函数在派生类中重新定义以后,派生类才能实例化出对象。

    那么,有虚表函数的类的大小是多少呢?

    #define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; class Base { public: virtual void Test() { cout << "Base.Test()" << endl; } virtual Base& Test1() { cout << "Base.Test1()" << endl; return *this; } private: int _b; }; class Derived :public Base { public: virtual void Test() { cout << "Derived.Test()" << endl; } virtual Derived& Test1() { cout << "Derived.Test1()" << endl; return *this; } private: int _d; }; void test(Base& b) { b.Test(); b.Test1(); } int main() { Base b; Derived d; test(b); test(d); cout << sizeof(Base) << endl; return 0; }

    我们知道,类的大小是类中成员变量的大小总和,Base类中,成员变量只有_b一个,若忽视虚函数,则该类的大小应为4个字节,可是这里求出来的是8个字节,那另外4个字节是什么?

    只要类中有虚函数,就会有虚表指针的存在,虚表指针指向虚表。 也因此,上例中类的大小之所有为8,除了类中本身的成员变量所占的4个字节外,另外的四个字节为虚表指针。 对于有虚函数的类,编译器都会维护一张虚表,对象的前四个字节就是指向虚表的指针。 由上例也可看出,虚函数的调用顺序即声明顺序。在派生类中,前面是基类的虚函数,后面是派生类的虚函数。


    虚表地址是在构造对象的时候填充进去的。具体看下例:

    #define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; class Base { public: Base() { cout << "Base.Base()"<<endl; } virtual void Test() { cout << "Base.Test()" << endl; } virtual Base& Test1() { cout << "Base.Test1()" << endl; return *this; } public: int _b; }; class Derived :public Base { public: Derived() { cout << "Derived.Derived()" << endl; } virtual void Test() { cout << "Derived.Test()" << endl; } virtual Derived& Test1() { cout << "Derived.Test1()" << endl; return *this; } private: int _d; }; int main() { Derived d; return 0; }

    在创建派生类类对象时,首先调用基类的构造函数,再调用派生类的调用函数,由上图可见,在调用基类构造函数后填充一个虚表指针,调用派生类构造函数后,填充的虚表指针发生变化。

    typedef void(*pfun)(); class Base { public: virtual void Test() { cout << "Base.Test()" << endl; } virtual void Test1() { cout << "Base.Test1()" << endl; } public: int _b; }; class Derived :public Base { public: virtual void Test1() { cout << "Derived.Test1()" << endl; } virtual void Test2() { cout << "Derived.Test2()" << endl; } private: int _d; }; void Print()//打印 { Derived d; Base& b = d; pfun* fun = (pfun*)(*(int *)&b); while (*fun) { (*fun)(); ++fun; } } int main() { Print(); return 0; }

    Base.Test() //基类中本就存Test,派生类中未重写,因此调用基类中Test函数 Derived.Test1() //基类中存在Test1,但在派生类中进行了重写,因此调用派生类中的Test() Derived.Test2() //Test2是派生类中新增加的,因此直接调用派生类中的Test2()

    即:基类中的虚函数,若未在派生类中重写,则调用基类中的,重写了则调用派生类中的该函数(替换相同偏移量处的函数)。派生类中新增加的虚函数则放在后面(声明次序)。


    上例所说为单继承的情况,接下来我们讨论多继承:

    typedef void(*pfun)(); class Base1 { public: virtual void Test1() { cout << "Base1.Test1()" << endl; } virtual void Test2() { cout << "Base1.Test2()" << endl; } public: int _b1; }; class Base2 { public: virtual void Test3() { cout << "Base2.Test3()" << endl; } virtual void Test4() { cout << "Base2.Test4()" << endl; } private: int _b2; }; class Derived :public Base1, public Base2 { public: virtual void Test1() { cout << "Derived.Test1()" << endl; } virtual void Test3() { cout << "Derived.Test3()" << endl; } virtual void Test5() { cout << "Derived.Test5()" << endl; } public: int _d; }; void Print()//打印 { Derived d; Base1& b = d; pfun* fun = (pfun*)(*(int *)&b); while (*fun) { (*fun)(); ++fun; } cout << endl; cout << endl; Base2& b1 = d; fun = (pfun*)(*(int *)&b1); while (*fun) { (*fun)(); ++fun; } } int main() { Print(); return 0; }

    Derived.Test1() //基类中的Test1在派生类中被重写, 因此调用派生类中的Test2函数 Base1.Test2() //基类1中本就存Test2,派生类中未重写,因此调用基类中Test2函数 Derived.Test5() //派生类中所特有的Test5函数

    Derived.Test3() //基类中的Test3在派生类中被重写, 因此调用派生类中的Test3函数 Base2.Test4() //基类2中本就存Test4,派生类中未重写,因此调用基类中Test4函数

    对象模型


    菱形继承

    typedef void(*pfun)(); class Base { public: virtual void Test1() { cout << "Base.Test1()" << endl; } public: int _b; }; class C1:public Base { public: virtual void Test1() { cout << "C1.Test1()" << endl; } virtual void Test2() { cout << "C1.Test2()" << endl; } public: int _c1; }; class C2:public Base { public: virtual void Test1() { cout << "C2.Test1()" << endl; } virtual void Test3() { cout << "C2.Test3()" << endl; } public: int _c2; }; class Derived :public C1, public C2 { public: virtual void Test1() { cout << "Derived.Test1()" << endl; } virtual void Test2() { cout << "Derived.Test2()" << endl; } virtual void Test4() { cout << "Derived.Test4()" << endl; } public: int _d; }; void Print()//打印 { Derived d; C1& c1 = d; pfun* fun = (pfun*)(*(int *)&c1); while (*fun) { (*fun)(); ++fun; } cout << endl; cout << endl; C2& c2 = d; fun = (pfun*)(*(int *)&c2); while (*fun) { (*fun)(); ++fun; } } int main() { Print(); return 0; }

    Derived.Test1() //Base类中有Test1函数,先在C1类中重写,后又在派生类中重写,调用派生类中的Test1函数 Derived.Test2() //C1类中的函数,在派生类中重写 Derived.Test4() //派生类中所特有的Test4函数

    Derived.Test1() //Base类中有Test1函数,先在C2类中重写,后又在派生类中重写,调用派生类中的Test1函数 C2.Test3() //C2类中的函数,未在派生类中重写

    对象模型

    int main() { //Print(); Derived d; d.C1::_b = 1; d._c1 = 2; d.C2::_b = 3; d._c2 = 4; d._d = 5; return 0; }

    但是在这种继承方式下,在给_b赋值时,若是没有进行作用域限定,会产生二义性,因此,引入菱形虚拟继承

    转载请注明原文地址: https://ju.6miu.com/read-669995.html

    最新回复(0)