对象的类型:
在这里先来看一下在继承体系中搞的人晕头转向的几个术语:
下面让我们一起进入多态的世界
多态(polymorphism)一词最初来源于希腊语polumorphos,含义是具有多种形式或形态的情形。
我们把具有继承关系的多个类型称为多态类型。
引用或指针的静态类型与动态类型不同这一事实正是c++语言支持多态性的根本所在。
1、多态分为静态多态和动态多态。
静态多态和动态多态还有下面的两种名字,其实意思都一样,但是为了以防我们以后见到这样的名字而不知所措,还是可以了解一下哒(笑脸):
(静态决议和动态决议)
(静态联编和动态联编)
静态多态:
编译器在编译期间完成,编译器根据实参的类型(可能会发生隐式类型转化),可推断出要调用哪个函数,如果没有对应的函数,则会出现编译错误。
动态多态:
在程序执行期间判断所引用对象的实际类型,根据实际类型调用相应的函数。动态多态的绑定:
①基类中必须要有虚函数—->在派生类中必须重写基类中的虚函数;
②必须使用基类的指针或引用去指向派生类的对象。
小结:通过基类的指针或引用调用虚函数时,调用基类还是派生类的虚函数,要在运行时根据指针(引用)指向(引用)的类型确定,当调用非虚函数时,无论指向的是哪种类型,一定调用的是基类的函数。
当且仅当通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。
示例1:
class Base
{
public :
Base()
:_b(3)
{
cout << "Base()" << endl;
}
void Show()
{
cout << _b << endl;
}
private :
int _b;
};
class Derived :public Base
{
public :
Derived()
:_d(4)
{
cout << "Derived()" << endl;
}
void Show()
{
cout << _d << endl;
}
private :
int _d;
};
void Print(Base* p)
{
p->Show();
}
void Test()
{
Base b;//3
Derived d;//4
Print(&b);
Print(&d);
}
int main()
{
Test();
return 0;
}
最终的结果都是3,与我们想要得到的答案不符。这是因为Show函数不是虚函数。从而验证了动态绑定的条件。
2.在派生类中进行重写(覆盖)的虚函数必须满足函数原型相同(参数列表,函数名,返回值类型(协变除外))。
补充下协变的概念:
协变:基类中的虚函数返回值类型为基类类型的指针或引用,并且派生类中重写的虚函数返回值类型为派生类类型的指针或引用。
示例如下:
class Base
{
public :
Base()
:_b(3)
{
cout << "Base()" << endl;
}
virtual Base* Show()//返回值类型为Base*
{
cout << _b << endl;
return this;
}
private :
int _b;
};
class Derived :public Base
{
public :
Derived()
:_d(4)
{
cout << "Derived()" << endl;
}
virtual Derived* Show()//返回值的类型为Derived*
{
cout << _d << endl;
return this;
}
private :
int _d;
};
void Print(Base* p)
{
p->Show();
}
void Test()
{
Base b;//3
Derived d;//4
Print(&b);
Print(&d);
}
结果如下:
3.虚函数注意要点:
①不要在构造函数和析构函数内部调用虚函数,在构造和析构函数中,对象是不完整的,可能会出现未定义的情况。
例如:在构造函数中,类中有3个成员变量需要初始化,假如在初始化了一个变量后就调用了虚函数,则可能会出现未定义情况。
在析构函数中,假如释放了一块空间后,调用虚函数,也会导致未定义的情况。②在基类中定义了虚函数,则在派生类中该函数始终保持虚函数的特性。
在派生类中重写虚函数时也可以不显示写出virtual关键字,这时编译器会默认该函数为虚函数,为了程序看起来更加清晰,则最好加上virtual关键字。③如果在类外定义虚函数,则只在类中声明时加virtual关键字,在类外定义是不能加virtual关键字。
由上引发的思考:
④构造函数为什么不能定义为虚函数?
因为调用虚函数的前提是:对象一定构建成功,获取虚表地址必须通过对象的地址,如果对象还没有构建成功,那么无法获取虚表地址,
所以构造函数不能定义为虚函数。构造函数未执行完时对象是不完整的。
⑤虽然operator=可以声明为虚函数,最好不要这样做。
可能会导致错误(由于和赋值兼容规则所矛盾)。
用下面的例子来进一步说明:
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 Test1()
{
Base b1;
Derived d1;
Base& b2 = b1;
b2 = d1;//会调用基类的赋值运算符重载虚函数
Base& b3 = d1;
d1 = b2;//这样就会编译错误(虽然可能调用派生类的赋值运算符重载虚函数),赋值兼容规则不允许。
}
⑥静态成员函数不能定义为虚函数。
因为静态成员函数是该类的所有对象所共享的,可以通过类名和作用域限定符调用,也就是可以不构建对象,但是虚表地址必须要通过对象的地址才能获取。
⑦在类的六个默认成员函数中,只有析构函数需要定义为虚函数。
示例如下:
class Base
{
public:
Base()
:_b(3)
{
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
private:
int _b;
};
class Derived :public Base
{
public:
Derived()
:_d(4)
{
cout << "Derived()" << endl;
}
~Derived()
{
cout << "~Derived()" << endl;
}
private:
int _d;
};
void Test()
{
Base* pb = new Derived;
delete pb;
}
结果如下:
从结果观察可知,当使用基类指针去指向一个派生类的对象时(即将派生类对象的地址赋给基类的一个指针),这里需要了解赋值兼容规则,相当于构造了一个派生类的对象,先调用派生类的构造函数(在初始化列表中调用基类的构造函数),执行派生类的构造函数,当析构时,只调用了基类的析构函数(因为pb为Base*类型,所以只会调用基类的析构函数(析构函数不是虚函数时))——–所以可能会存在内存泄漏,当派生类中动态申请了一些空间时,资源泄露。
而把析构函数声明为虚函数时,可观察程序的结构:
class Base
{
public:
Base()
:_b(3)
{
cout << "Base()" << endl;
}
virtual ~Base()
{
cout << "~Base()" << endl;
}
private:
int _b;
};
class Derived :public Base
{
public:
Derived()
:_d(4)
{
cout << "Derived()" << endl;
}
virtual ~Derived()
{
cout << "~Derived()" << endl;
}
private:
int _d;
};
void Test()
{
Base* pb = new Derived;
delete pb;
}
结果如下:
由结果可知,把基类的析构函数声明为虚函数后,很好的解决了上面遇到的内存泄漏或者资源泄漏的问题。
可这里还有一个问题:那就是派生类重写的析构函数和基类定义的虚析构函数函数名并不相同,这和重写虚函数的规则好像有点矛盾,其实这里是编译器在派生类在进行重写析构函数时做了处理。
⑧派生类重写基类的虚函数实现多态,要求函数的原型(参数列表,函数名,返回值(协变除外))完全相同。
多态的实现(验证协变):
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Base
{
public :
virtual Base* FunTest1()
{
cout << "Base::FunTest1()" << endl;
return this;
}
};
class Derived:public Base
{
public :
virtual Derived* FunTest1()
{
cout << "Derived::FunTest1()" << endl;
return this;
}
};
void Print(Base& b)
{
b.FunTest1();
}
void Test1()
{
Base b1;
Derived d1;
Print(b1);
Print(d1);
}
int main()
{
Test1();
return 0;
}
结果如下:
4.多态实现的条件:
①必须继承;
②派生类中重写(覆盖)基类中的虚函数。
示例:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Base
{
public :
virtual Base* FunTest1()
{
cout << "Base::FunTest1()" << endl;
return this;
}
virtual ~Base()
{
cout << "~Base()" << endl;
}
};
class Derived:public Base
{
public :
virtual Derived* FunTest1()
{
cout << "Derived::FunTest1()" << endl;
return this;
}
virtual ~Derived()
{
cout << "~Derived()" << endl;
}
};
void Test1()
{
cout << "-------------Test1()----------" << endl;
Base* pb = new Derived;
pb->FunTest1();
delete pb;
}
void Test2()
{
cout << "------------Test2----------" << endl;
Base* pb = (Base*)new Derived;
pb->FunTest1();
delete pb;
}
int main()
{
Test1();
return 0;
}
结果如下:
由结果可知,动态绑定时和类型无关,只和绑定的对象有关,根据绑定的对象去调用绑定对象的类中的虚函数。
5.多态的调用:
1)如果不是虚函数——>直接调用类中函数;
2)如果是虚函数:
①取虚表指针;
②取该虚函数在虚表中的偏移量;
③定位虚函数。
6.探索多态实现的机制:
1)将类的成员函数声明为虚函数时类的大小会怎样变化?(不存在继承):
//在测试时需加上头文件以及using namespace std;
class A
{
public :
A()
:_a(1)
{
cout << "A()" << endl;
}
void Show()
{
cout << "A::Show()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private :
int _a;
};
void Test()
{
A a;
cout << sizeof(A) << endl;
}
结果如下:
可以看出,类的大小是4个字节,只是类中非静态成员变量的大小。而如果在类中加入虚函数,观察类的大小会不会发生改变?
class A
{
public :
A()
:_a(1)
,_b(2)
{
cout << "A()" << endl;
}
virtual void Show()
{
cout << "A::Show()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
virtual void FunTest1()
{
cout << "A::FunTest1()" << endl;
}
private :
int _a;
int _b;
};
void Test()
{
A a;
cout << sizeof(A) << endl;
}
结果如下:
结果分析:
上面的程序中有两个非静态的成员变量,再加上虚表地址(看下图)的大小—–12个字节。
用以下程序来测试虚函数的调用顺序:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class A
{
public :
A()
:_a(1)
,_b(2)
{
//cout << "A()" << endl;
}
virtual void Show()
{
cout << "A::Show()" << endl;
}
~A()
{
//cout << "~A()" << endl;
}
virtual void FunTest1()
{
cout << "A::FunTest1()" << endl;
}
private :
int _a;
int _b;
};
typedef void (*_pFun_t)();//_pFun为一个函数指针
void PrintVfptr(_pFun_t* _pPfun)
{
while (*_pPfun)
{
(*_pPfun)();//调用虚函数
_pPfun = (_pFun_t*)((int*)_pPfun + 1);
}
}
void Test()
{
A a;
cout << sizeof(A) << endl;
_pFun_t* pPFun = (_pFun_t *)(*(int*)&a);定义一个虚表指针(类型为函数指针的指针)
PrintVfptr(pPFun);
}
int main()
{
Test();
return 0;
}
结果如下:
2)单继承中的多态(虚表的形成):
示例1:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Base
{
public :
Base()
{
cout << "Base()" << endl;
}
virtual Base* FunTest1()
{
cout << "Base::FunTest1()" << endl;
return this;
}
virtual ~Base()
{
cout << "~Base()" << endl;
}
};
class Derived:public Base
{
public :
Derived()
{
cout << "Derived()" << endl;
}
virtual Derived* FunTest1()
{
cout << "Derived::FunTest1()" << endl;
return this;
}
virtual ~Derived()
{
cout << "~Derived()" << endl;
}
};
void Test3()
{
Derived d1;
}
int main()
{
Test3();
return 0;
}
在创建派生类的对象时,都会执行哪些操作?(分析下图)
调用派生类的构造函数(在初始化列表调基类的构造函数—–>会在对象的前4个字节中填充基类的虚表地址),在基类构造函数调用完成后会返回到派生类的初始化列表中,然后执行派生类的构造函数(会将对象的前4个字节修改为派生类的虚表指针)。
从这里可以看出:类中如果存在虚函数,那么它的构造函数的主要功能是填充虚表指针。
这里还应该明白虚函数表在程序编译结束时已经形成了。
示例2:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
typedef void(*pFun)();//重命名一个函数指针类型
void PrintVfptr(pFun* _pPfun)
{
while (*_pPfun)
{
(*_pPfun)();//调用虚函数
_pPfun = (pFun*)((int*)_pPfun + 1);
}
}
class Base
{
public:
Base()
{
cout << "Base()" << endl;
}
virtual void FunTest1()
{
cout << "Base::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "Base::FunTest2()" << endl;
}
virtual void FunTest3()
{
cout << "Base::FunTest3()" << endl;
}
};
class Derived :public Base
{
public:
Derived ()
{
cout << "Derived()" << endl;
}
virtual void FunTest1()
{
cout << "Derived::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "Derived::FunTest2()" << endl;
}
virtual void FunTest6()
{
cout << "Derived::FunTest6()" << endl;
}
};
void Test3()
{
Derived d1;
cout << "----------test3()---------" << endl;
pFun* pPfun = (pFun*)*(int*)&d1;
PrintVfptr(pPfun);
}
int main()
{
Test3();
return 0;
}
结果如下:
构建派生类对象的过程解析如下:
下面是根据程序的执行结果所推测的:
派生类的虚函数表的生成过程:
①先拷贝基类的虚函数表;
②在派生类中查看,如果派生类中重写了基类的某些虚函数,那就在同位置进行覆盖(使用派生类中重写的虚函数的地址);
③跟上派生类自己新定义的虚函数(如果有多个,顺序按照在类中的声明顺序)。
虚函数表(基类/派生类)的形成:
3)多重继承:
示例如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class B1
{
public :
B1()
:_b1(1)
{}
virtual void FunTest1()
{
cout << "B1::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "B1::Funtest2()" << endl;
}
virtual void FunTest3()
{
cout << "B1::FunTest3()" << endl;
}
int _b1;
};
class B2
{
public :
B2()
:_b2(2)
{}
virtual void FunTest4()
{
cout << "B2::FunTest4()" << endl;
}
virtual void FunTest5()
{
cout << "B2::Funtest5()" << endl;
}
virtual void FunTest6()
{
cout << "B2::FunTest6()" << endl;
}
int _b2;
};
class C :public B1, public B2//多继承
{
public :
C()
:_c(3)
{}
virtual void FunTest1()//重写B1
{
cout << "C::FunTest1()" << endl;
}
virtual void FunTest6()//重写B2
{
cout << "C::Funtest6()" << endl;
}
virtual void FunTest7()//C类特有的虚函数
{
cout << "C::FunTest7()" << endl;
}
int _c;
};
typedef void(*_pFun_t)();//_pFun为一个函数指针
void PrintVfptr(_pFun_t* _pPfun)
{
while (*_pPfun)
{
(*_pPfun)();//调用虚函数
_pPfun = (_pFun_t*)((int*)_pPfun + 1);
}
}
void Test1()
{
C c;
B1& b1 = c;
_pFun_t* pPFun = (_pFun_t *)(*(int*)&b1);
PrintVfptr(pPFun);
cout << "----------------" << endl;
B2& b2 = c;
pPFun = (_pFun_t *)(*(int*)&b2);
PrintVfptr(pPFun);
}
int main()
{
Test1();
return 0;
}
结果如下:
对C类对象模型的分析:
7.菱形虚拟继承(虚函数):
①菱形继承(类中有虚函数):
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class B
{
public:
virtual void FunTest1()
{
cout << "B::FunTest1()" << endl;
}
int _b;
};
class C1 :public B
{
public:
virtual void FunTest1()//重写B
{
cout << "C1::FunTest1()" << endl;
}
virtual void FunTest2()//新加的虚函数
{
cout << "C1::FunTest2()" << endl;
}
int _c1;
};
class C2 :public B
{
public:
virtual void FunTest1()//重写B中虚函数
{
cout << "C2::FunTest1()" << endl;
}
virtual void FunTest3()//新加虚函数
{
cout << "C2::FunTest3()" << endl;
}
int _c2;
};
class D :public C1, public C2
{
public:
virtual void FunTest1()//重写C1和C2中的虚函数
{
cout << "D::FunTest1()" << endl;
}
virtual void FunTest2()//重写C1中的虚函数
{
cout << "D::FunTest2()" << endl;
}
virtual void FunTest4()//重写C2中的虚函数
{
cout << "D::FunTest4()" << endl;
}
int _d;
};
void Test1()
{
D d;
d.C1::_b = 1;
d._c1 = 2;
d.C2::_b = 3;
d._c2 = 4;
d._d = 5;
}
int main()
{
Test1();
return 0;
}
结果如下:
下面对结果简单的分析下:
由结果可知,D类自己新加入的虚函数存放在了C1(派生类的继承列表中的第一个基类)的虚表中,再将在D类中对C1类和C2类的某些虚函数进行重写,所以就用D类重写的虚函数的地址覆盖了基类C1和C2中的某些虚函数。
②菱形虚拟继承
D类中没有特有的虚函数,只是将C1类和C2类的某些虚函数进行了重写:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class B
{
public :
virtual void FunTest1()
{
cout<<"B::FunTest1()"<<endl;
}
int _b;
};
class C1:virtual public B
{
public :
virtual void FunTest1()//重写B
{
cout<<"C1::FunTest1()"<<endl;
}
virtual void FunTest2()//新加的虚函数
{
cout<<"C1::FunTest2()"<<endl;
}
int _c1;
};
class C2:virtual public B
{
public :
virtual void FunTest1()//重写B中虚函数
{
cout<<"C2::FunTest1()"<<endl;
}
virtual void FunTest3()//新加虚函数
{
cout<<"C2::FunTest3()"<<endl;
}
int _c2;
};
class D:public C1,public C2
{
public :
virtual void FunTest1()//重写C1和C2中的虚函数
{
cout<<"D::FunTest1()"<<endl;
}
virtual void FunTest2()//重写C1中的虚函数
{
cout<<"D::FunTest2()"<<endl;
}
virtual void FunTest3()//重写C2中的虚函数
{
cout<<"D::FunTest3()"<<endl;
}
int _d;
};
typedef void (*Pfun)();
void Print_Vir_Tab(Pfun* pPfun)
{
while (*pPfun)
{
(*pPfun)();
pPfun = (Pfun*)((int*)pPfun + 1);
}
}
void Test1()
{
D d;
d._b = 1;
d._c1 = 2;
d._c2 = 3;
d._d = 4;
Print_Vir_Tab((Pfun*)*(int*)&d);
cout<<"-----------------"<<endl;
Print_Vir_Tab((Pfun*)*((int*)&d + 3 ));
cout<<"--------------------"<<endl;
Print_Vir_Tab((Pfun*)*((int*)&d + 7 ));
}
int main()
{
Test1();
return 0;
}
由上图分析可知:将B类的虚表地址经过重写后只保存了一份,也就是说B类中的虚函数经过重写后成为所有类共有的,
而D将C1和C2的某些虚函数进行了重写。
可以看出,虚菱形继承增加了一个超类(B)的虚表地址。
③当D类中有特有的虚函数时:
如果将上面的代码稍加改动,观察结果有何不同?
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class B
{
public :
virtual void FunTest1()
{
cout<<"B::FunTest1()"<<endl;
}
int _b;
};
class C1:virtual public B
{
public :
virtual void FunTest1()
{
cout<<"C1::FunTest1()"<<endl;
}
virtual void FunTest2()
{
cout<<"C1::FunTest2()"<<endl;
}
int _c1;
};
class C2:virtual public B
{
public :
virtual void FunTest1()
{
cout<<"C2::FunTest1()"<<endl;
}
virtual void FunTest3()
{
cout<<"C2::FunTest3()"<<endl;
}
int _c2;
};
class D:public C1,public C2
{
public :
virtual void FunTest1()
{
cout<<"D::FunTest1()"<<endl;
}
virtual void FunTest2()
{
cout<<"D::FunTest2()"<<endl;
}
virtual void FunTest4()//D类中新加的虚函数
{
cout<<"D::FunTest4()"<<endl;
}
int _d;
};
typedef void (*Pfun)();
void Print_Vir_Tab(Pfun* pPfun)
{
while (*pPfun)
{
(*pPfun)();
pPfun = (Pfun*)((int*)pPfun + 1);
}
}
void Test1()
{
D d;
d._b = 1;
d._c1 = 2;
d._c2 = 3;
d._d = 4;
Print_Vir_Tab((Pfun*)*(int*)&d);
cout<<"-----------------"<<endl;
Print_Vir_Tab((Pfun*)*((int*)&d + 3 ));
cout<<"--------------------"<<endl;
Print_Vir_Tab((Pfun*)*((int*)&d + 7 ));
}
int main()
{
Test1();
return 0;
}
结果如下:
由结果可得:将D类中新加的虚函数存放在第一张虚表的最后。
③当在类中显式给出类的构造函数或者析构函数时,可以测出类的大小增加了四个字节;
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class B
{
public:
B()
{}
virtual void FunTest1()
{
cout << "B::FunTest1()" << endl;
}
int _b;
/*virtual ~B()
{
cout << "B::FunTest1()" << endl;
}*/
};
class C1 :virtual public B
{
public:
C1()
{}
virtual void FunTest1()//重写B
{
cout << "C1::FunTest1()" << endl;
}
virtual void FunTest2()//新加的虚函数
{
cout << "C1::FunTest2()" << endl;
}
int _c1;
/*virtual ~C1()
{
cout << "C1::~C1()" << endl;
}*/
};
class C2 :virtual public B
{
public:
C2()
{}
virtual void FunTest1()//重写B中虚函数
{
cout << "C2::FunTest1()" << endl;
}
virtual void FunTest3()//新加虚函数
{
cout << "C2::FunTest3()" << endl;
}
int _c2;
/*virtual ~C2()
{
cout << "C2::C2()" << endl;
}*/
};
class D :public C1, public C2
{
public:
D()
{}
virtual void FunTest1()//重写C1和C2中的虚函数
{
cout << "D::FunTest1()" << endl;
}
virtual void FunTest2()//重写C1中的虚函数
{
cout << "D::FunTest2()" << endl;
}
virtual void FunTest3()//重写C2中的虚函数
{
cout << "D::FunTest3()" << endl;
}
int _d;
/*virtual ~D()
{
cout << "D::~D()" << endl;
}*/
};
typedef void(*Pfun)();
void Print_Vir_Tab(Pfun* pPfun)
{
while (*pPfun)
{
(*pPfun)();
pPfun = (Pfun*)((int*)pPfun + 1);
}
}
void Test1()
{
D d;
d._b = 1;
d._c1 = 2;
d._c2 = 3;
d._d = 4;
Print_Vir_Tab((Pfun*)*(int*)&d);
cout << "-----------------" << endl;
Print_Vir_Tab((Pfun*)*((int*)&d + 3));
cout << "--------------------" << endl;
Print_Vir_Tab((Pfun*)*((int*)&d + 8));
cout << sizeof(d) << endl;
}
int main()
{
Test1();
return 0;
}
结果如下:
从内存中可以看出,当在继承体系的某个派生类中显式给出构造或者析构函数时,最低层的派生类实例的大小会多四个字节—存放空指针(我自己的猜想是:因为空指针下面是超类B的虚表地址和成员变量,而它们是继承体系中类所共享的,在最低层派生类中只保存一份,空指针上面是C1类和C2类的虚表地址和偏移量表地址以及成员变量,所以空指针应该起到一个标志的作用,说明下面是所有类共享的)。
但是我没有在网上查到空指针和构造以及析构函数有什么关系?
如果有哪位朋友知道,还请不吝赐教。
8.虚表存在的安全隐患:
①效率太差,因为调用一个函数时得去查找两次,第一次得到虚表的地址,然后在虚表中查看虚函数的地址,会造成性能降低。
②存在安全隐患,例如假如程序不允许你去访问一个虚函数,但是你自己仍可以在虚表中查看虚函数。