C++的三大特点,封装、继承和多态。封装可以使代码模块化,继承可以扩展以前的代码,他们的目的都实现了代码重用,而多态呢,它其实是为了实现接口的重用!对于不同的对象,函数能够通过同一个接口使他们找到自己各自的实现方法,已完成他们所需要完成的不同任务 。
多态的分类:
静态多态:编译器在编译期间完成,编译器根据函数实参的类型,推断出要调用那个函数,有对应的函数就调用它,否则编译出错。
静态多态的实例:
动态多态:
在程序执行期间判断所引用的对象的实际类型,根据其实际类型调用相应的方法。
要了解动态多态就要引入虚函数这个概念!
虚函数:使用virtual关键字修饰的成员函数称为虚函数,虚函数允许子类重新定义成员函数,称为覆盖或者重写。
动态绑定:通过基类的引用或指针调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。
动态绑定条件:①基类有成员函数为虚函数,并且在派生类中重写了基类中的虚函数。②通过基类类型的引用或者指针调用虚函数。
示例1:当函数没有定义为虚函数会发生什么?
class A
{
public :
void Add()
{
cout << "A::Add()" << endl;
}
public :
int a;
};
class B :public A
{
public:
void Add()
{
cout << "B::Add()" << endl;
}
public :
int b;
};
int main()
{
A *pa;
A a1;
B B1;
pa = &a1;
pa->Add();
pa = &B1;
pa->Add();
system("pause");
return 0;
}
会发现执行的都是基类的函数,一个基类指针指向一个派生类对象,那么通过该指针,你只能访问基类定义的成员函数,因为在这个指针看来,你所指针的就是一个基类对象,而不是一个派生类对象,虚函数就是为了这个规则而设计的。
当基类中的函数被定义为虚函数时,并且派生类中的函数也被重写,这时看一下执行结果:
发现结果和我们的预期相同。
那虚函数是怎么实现的呢?
我们来看一个实例:
class A
{
public:
A()
:a(1)
{}
virtual void Add()
{
cout << "A::Add()" << endl;
}
virtual void Add2()
{
cout << "A::Add2()" << endl;
}
public:
int a;
};
class B :public A
{
public:
B()
:b(2)
{}
virtual void Add()
{
cout << "B::Add()" << endl;
}
virtual void Add2()
{
cout << "B::Add2()" << endl;
}
public:
int b;
};
int main()
{
A *pa;
A a1;
B b1;
cout << "sizeof(a1): " << sizeof(a1) << endl;
cout << "sizeof(b1): " << sizeof(b1) << endl;
pa = &a1;
pa->Add();
pa->Add2();
pa = &b1;
pa->Add();
pa->Add2();
system("pause");
return 0;
}
当函数不是虚函数时,我们知道A类对象a1的大小是4,因为它只有一个int类型的成员,同理B类对象b1大小为8,因为它不仅有自己的一个成员,还继承了A类的成员。但是当我们把成员函数定义为虚函数时,计算出他们的大小发现发生了改变,如下:
a1的大小变为8,b1的大小变为12,他们都增加了4个字节的大小,那4个字节被用来干了什么呢?
我们打开监视窗口进一步观察:
发现a1里面多了一个指针,而指针指向的空间存放了A类定义的虚函数的地址;b1继承的A类成员里面也多了一个指针,指针指向的空间存放了B类重写虚函数的地址 ,可以调出内存窗口进一步观察:
a1:
b1:
可以发现多出来的指针指向的空间里面存放着当前类自己定义的虚函数的地址,并且以0作为虚函数指针的结束标志。当确定对象的实际类型后,根据对象的实际类型去访问增加的指针,然后通过这个指针去访问虚函数。
这个指针就叫做虚表指针,虚表指针指向的空间就叫做虚表。
我们可以看一下打印结果:
从上面我们了解了虚表指针和虚表,进一步讨论几个问题:
通过实例进行分析:
① 虚表指针是什么时候,怎样添加进去的?
解决①:
通过上面的图片可以清楚看到,在执行构造函数体之前,也就是在执行初始化列表的时候在对象的前四个字节中添加了虚表指针。
② 普通成员函数会不会将他们的地址加进虚表里面?
③ 如果基类定义了虚函数而派生类没有重写,会是怎样的呢?
解决②、③:
通过观察监视窗口和内存窗口,可以发现,没有被定义为虚函数的成员函数是不会加入到虚表中去的;并且会发现虽然Fun4()这个函数虽然没有被B类继承但是仍然会添加进虚表中。
④ 如果派生类定义了虚函数,而基类没有定义会是怎样呢?
解决④:
观察监视窗口发现B类定义的Fun5()并没有添加进虚表中,但是打开内存窗口发现,其实这个函数真正上是被添加进了派生类对象的虚表,并且添加在虚表的最后。
注意:A类定义的虚函数Fun4(),虽然没有在派生类B类中重写,但是可以发现其实B类从A类中继承了,可以发现他们的函数地址是一样的。
⑤派生类重写基类虚函数实现多态的要求:
要求重写基类虚函数时,与基类的函数名、参数列表、返回值完全相同,(协变除外)。
基类和派生类的虚表指针、虚表和虚函数指针的模型如下:
注意:
- 构造函数不能定义为虚函数:
通过上面的问题①我们可以发现虚表指针是在构造函数的初始化列表部分就添加进了对象的前四个字节,但是在对象还没有创造之前不能调用虚函数,如果把构造函数定义为写虚函数的话,会发现你要创建对象就要调用被定义为虚函数的构造函数,但是对象还没创建成功不能抵用虚函数。所以,就规定构造函数不能定义为虚函数。
- 静态函数不能定义为虚函数:
静态成员函数没有this指针,不能访问对象,而虚表指针存放在对象的前四个字节当中,不能访问对象就导致不能使用虚表指针,导致无法访问函数。
- 赋值符重载可以定义为虚函数,但不建议定义为虚函数:
赋值符重载定义为虚函数其实是无意义的。
- 析构函数最好定义为虚函数。
class A
{
public:
~A()
{
cout<< "~A()" <<endl;
}
public :
int a;
};
class B :public A
{
public:
~B()
{
cout << "~B()" << endl;
}
public :
int b;
};
void FunTest()
{
A *pa =new B;
delete pa;
}
int main()
{
FunTest();
system("pause");
return 0;
}
使用一个基类指针指向一个派生类对象时,相当于构造了一个派生类对象(在指行派生类构造函数的初始化列表之前去执行基类的构造函数,然后继续执行派生类的构造函数),但是在析构时发现只调用了基类的析构函数,并没有调用派生类的析构函数,就有可能发生资源泄漏。当把上面的析构函数给成虚函数时:
发现在析构时先调用了派生类的析构函数,然后在调用了基类的析构函数,这样就不会出现资源泄漏的情况了。
- 纯虚函数
在成员函数的形参列表后面写上=0,则成员函数就称为了虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。纯虚函数在派生类中定义之后才能真正实例化出对象。
几个学习总结:
- 基类中定义了虚函数,在派生类中该函数始终保持虚函数特性
- 虚表是所有对象实例公用的
- 不要在构造函数和析构函数中调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会出现未定义的行为。