1、什么是多态?
通俗来说就是不同的对象接收到相同的消息时,产生不同的行为。在C++程序设计中,多态性是指用一个名字定义不同的函数,这些函数执行不同但又类似的操作,这样就可以用同一个函数名调用不同函数的内容。其实我们之前学习的运算符就使用了多态,例如“+”,它可以实现int型,float型,double型各自的加法操作,但是它在加法的过程中却是不同的,是有不同的函数去实现的。
2、多态分为哪些?
多态分为静多态和动多态。
详解:一个源程序经过编译,连接成为可执行文件的过程是把可执行文件代码联编(也叫绑定)在一起的过程。其中在运行之前就完成的称为静态联编,又叫前期联编;而在运行时才完成的联编称为动态联编,也称后期联编。静态联编是指系统在编译时就决定如何实现某一动作。静态联编要求程序在编译时就知道调用函数的全部信息。因此,这种联编类型的函数调用速度很快。效率高是静态联编的主要优点。动态联编是指系统在运行时动态实现某一动作,采用这种方式联编,一直要到程序运行时才能确定调用哪个函数。动态联编的主要优点是:提供了更好的灵活性、问题抽象性和程序易维护性。
静态联编支持的多态性称为编译时多态性,也称静态多态性。在C++中,编译时多态性是通过函数重载(包括运算符重载)和模板实现的。而动态联编所支持的多态性称为运行时多态性,也称动态多态性。
3、使用多态有什么作用?动多态产生的条件是什么?
使用多态可以消除类型之间的耦合关系,通过分离做什么和怎么做,从另一角度将接口和实现分离开来。(就比如同一个自主购票机,相同的接口,它却可以有成人票、儿童票两种类型);
条件:
下面通过代码来详细解释多态产生过程:
这个还是没加虚特性的:
class Person
{
public:
Person(int x, int y)
{
a = x;
b = y;
}
void show()
{
cout << "调用基类show()" << endl;
cout << a << b << endl;
}
private:
int a, b;
};
class my_Person :public Person
{
public:
my_Person(int x, int y, int z) :Person(x, y)
{
c = z;
}
void show()
{
cout << "调用子类show()" << endl;
}
private:
int c;
};
int main()
{
Person mb(23, 34),*mp;
my_Person mc(12, 23, 56);
mp = &mb;
mp->show();
mp = &mc;
mp->show();
return 0;
}
该代码的执行结果:
问题1:为什么这里调用的都是基类的show函数?
答:C++中规定,基类的对象指针可以指向它的公有派生的对象,但是当其指向公有派生类是,它只能访问派生类中从基类继承来的成员,而不能访问公有派生类中定义的成员。
问题2:既然都是基类的show函数,为什么打印的结果不一样?
答:当程序执行到mp = &mc;
这一步时,基类指针指向子类对象,通过子类提供的参数给基类的对象赋值。
加虚特性之后
class Person
{
public:
Person(int x, int y)
{
a = x;
b = y;
}
virtual void show()
{
cout << "调用基类show()" << endl;
cout << a <<" "<< b << endl;
}
private:
int a, b;
};
class my_Person :public Person
{
public:
my_Person(int x, int y, int z) :Person(x, y)
{
c = z;
}
virtual void show()//这里也可不写virtual,因为同名函数会直接继承为虚函数
{
cout << "调用子类show()" << endl;
cout << c << endl;
}
private:
int c;
};
int main()
{
Person mb(23, 34),*mp;
my_Person mc(12, 23, 56);
mp = &mb;
mp->show();
mp = &mc;
mp->show();
return 0;
}
要想详细了解调用过程,在这里需要引入一个新概念:虚函数表(vftable)
虚函数表是在编译期产生的,用来存储虚函数指针,vfptr是虚函数表指针(注意区别)
虚函数表指针是程序编译时就产生的,而虚函数指针是在创建对象的时候才写进虚函数表的
图解:
问题:什么是RTTI,RTTI在什么时候产生?RTTI信息存储在哪里?有什么作用?
运行时期的类型信息,是一个指向类型信息的指针。
编译期产生,RTTI指针放在vftable里面,类型信息放在rodata段
在父类指针转化为子类指针时会提供一个特殊的操作符(拓展部分有解释)。
再联合上边的流程图看这幅监视图:
在监视图可以清晰的看到,子类把从父类继承过来的虚函数表进行覆盖
注意:虚函数表其实就是函数指针的地址,并不是直接把虚函数写入虚函数表,函数调用的时候,是通过函数指针所指向的函数来调用函数。虚函数表存放在只读数据段,虚函数存放在代码段
由图可以看出,当调用不同的show()函数时,mp就指向不同的对象,对象里的vfptr再指向虚函数表,在虚函数表中找出对应的虚函数。
覆盖:子类中的成员方法会覆盖父类中相同(同返回值,同函数名,同参数列表)的虚函数
虚函数具有传递性:父类中如果有虚函数,那么子类中对应的相同的函数会被传递为虚函数,相同的指的是同返回值,同函数名,同参数列表
我们再来看一个代码:
class Base
{
public:
Base()
{
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
void fun1(int a)
{
cout << "Base::void fun1()" << endl;
}
protected:
private:
int _a;
};
class Derive :public Base
{
public:
Derive()
{
cout << "Derive()" << endl;
}
~Derive()
{
cout << "~Derive()" << endl;
}
void fun1()
{
cout << "Derive::void fun1()"<<endl;
}
void fun1(int a,int b)
{
cout << "Derive::void fun1(int a)" << endl;
}
protected:
private:
int _b;
};
int main()
{
Base *pb = new Derive();//父类指针指向子类对象
pb->fun1(10);
delete pb;
}
我们发现同时调用了基类的构造函数和子类的构造函数,但是却只调用了父类析构函数,却没有调用子类的析构函数,那么这个问题如何解决呢?
这个问题要解决很简单,只需要给父类析构函数前边加virtual:
class Base
{
public:
Base()
{
cout << "Base()" << endl;
}
virtual ~Base()
{
cout << "~Base()" << endl;
}
void fun1(int a)
{
cout << "Base::void fun1()" << endl;
}
protected:
private:
int _a;
};
再看执行结果:
这里需要注意一个问题:我们可能会疑惑析构函数的函数名并不相同,为什么还会覆盖?
C++中规定,析构函数就是它的函数名字。
对上述知识点再作一简单拓展:
4、动多态的产生过程?
使用指针或者引用调用虚函数
在对象中找到vfptr
找到vftable 在表中找到对应的虚函数指针
通过虚函数指针找到虚函数
5、vftable什么时候产生?在哪里存储?
编译期产生
放在rodata(只读数据段)
6、 构造函数能不能写成虚函数
不能
构造函数无法通过指针或者引用调用,所以写成虚函数没有意义
vfptr是在构造时候写入对象,而动态调用虚函数需要用到vfptr
7、 静态函数能不能写成虚函数
不能,静态函数不依赖于对象,无法产生动多态
8、析构函数能不能写成虚函数
能
9、虚函数能不能被处理成内联?
不能,虚函数需要将函数指针放到vftable,而内联函数在编译期展开
在release版本没有地址。
10、什么情况下析构函数必须写成虚函数?
当存在父类指针指向堆上的子类对象的时候,
就必须把父类的析构函数写成虚函数
11、父子类/组合类 的构造顺序?
类的编译顺序
先编译类名
再编译成员名
最后编译成员函数体
12、父类指针如何转化成子类指针?转化有什么条件?
Derive* pd = dynamic_cast<Derive*>(p);
dynamic_cast 父类指针转为子类指针专用的类型强转
要求:1.必须有RTTI 2.父类指针指向的对象中的RTTI确实是子类的