多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
一、基本概念
1.定义:不同对象收到相同的消息时,从而产生不同的动作,即一个名字定义的不同的函数
2.对象类型:(1)静态类型:对象声明时的类型,是在编译时确定的
(2)动态类型:目前所指对象的类型,是在运行确定的
3.静态多态(早绑定)
包含:函数重载、泛型函数(模板)
概念:编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推出要调用哪个函数,如果有对应的函数就调用,没有的话就编译错误。
有关函数重载:即在同一作用域内,函数名相同,参数列表不同,但函数的返回类型不作为判断函数是否重载的准则,函数重载避免了函数名的污染
int Sub(int a, int b)
{
return a - b;
}
int Sub(char a, int b)
{
return a - b;
}
int main()
{
Sub(1, 2);
Sub('1', 1);//形成函数重载,避免名字污染
return 0;
}
4.动态多态(晚绑定)
包含:虚函数
概念:在程序执行(非编译期间)判断所引用对象的实际类型,根据其实际类型调用相应的方法
动态绑定的条件:1.必须是虚函数(在派生类中必须对基类的虚函数进行重写)。2.通过基类类型的引用或者指针调用虚函数。
#include<iostream>
using namespace std;
class Base
{
public:
Base(int x, int y)
{
a = x;
b = y;
}
void show()
{
cout << "Base:";
cout << a << " " << b << endl;
}
private:
int a;
int b;
};
class Derived :public Base
{
public:
Derived(int x, int y, int z)
:Base(x, y)
{
c = z;
}
void show()
{
cout << "Derived:";
cout << "c=" << c << endl;
}
private:
int c;
};
int main()
{
Base mb(60, 60);//定义基类对象
Base *pc;//定义基类对象的指针
Derived mc(10, 20, 30);//定义派生类对象
pc = &mb;//利用基类对象的指针指向基类对象
pc->show();//调用基类对象的show函数
pc = &mc;//让基类对象的指针指向派生类的对象
pc->show();//这个指针在此调用的是基类中的show函数
system("pause");
return 0;
}
注:原因是:基类的对象指针可以指向他公有派生类的对象,但是当其指向公有派生类对象时,他只能访问派生类中从基类继承来的成员,而不能访问公有派生类中定义的成员(详细参考继承中的对象模型)
如若想要定义一个对象,也可以访问派生类中的成员,则需要用到虚函数,或者进行强转
二、虚函数
1.定义:虚函数定义是在基类中进行的,他是在基类中需要定义为虚函数的成员函数的声明中冠以关键字virtual,从而形成一种接口。
2.关键字:virtual
3.形式:
virtual 返回类型 函数名(参数列表)
{
函数体
}
注:基类中的某个成员函数被声明为虚函数之后,此虚函数就可以在一个或多个派生类中被重新定义。虚函数在重新定义时,其函数原型(返回类型或者满足赋值兼容规则的指针、引用的返回,函数名,参数个数,参数类型的顺序),都必须与基类中的原型相同。
必须在基类中定义虚函数
只在基类中声明
class Base
{
public:
Base(int x, int y)
{
a = x;
b = y;
}
virtual void show()
{
cout << "Base:";
cout << a << " " << b << endl;
}
private:
int a;
int b;
};
原因:虚函数,是实现动态绑定的,在函数运行时确定所要调用的函数,而此时pc指向派生类的对象,因此调用派生类的函数
4.在基类中声明虚函数,在派生类中,可以不再需要写虚函数关键字
5.只有通过基类指针访问虚函数时才能获得运行时的多态性
6.虚函数必须是类的成员函数,不能是友元函数与静态成员函数( 虚函数调用要靠特定的对象来决定激活哪个函数)
7.内联函数不能是虚函数()
8.构造函数不能是虚函数,但是析构函数可以是虚函数,而且通常说明为虚函数
在基类中用virtual声明成员函数为虚函数,在类外定义虚函数时,不必在加虚函数关键字
int main()
{
Base *p;//定义基类对象的指针
p = new Derived;//用运算符new为派生类的无名对象动态地分配了一个存储空间,并将地址赋给对象指针p
delete p;
system("pause");
return 0;
}
注意:运行此函数,在调用析构函数时,采用的是静态连接的方式,因此只调用了基类的析构函数。因为p是基类的对象,就是为了防止动态开辟时
析构函数定义为虚函数的形式:
virtual ~ 类名
{
函数体
}
三、纯虚函数与抽象类
(一)纯虚函数
1.作用:考虑到派生类的需要,在基类中预留一个函数名,具体功能留给派生类去实现,以便派生类根据需要对它进行重新定义,在基类中不需要实现
2.形式:
virtual 函数类型 函数名(参数表)=0;
3.举个例子
class A
{
public:
void set(int x)
{
r = x;
}
virtual void show() = 0;//纯虚函数的定义
protected:
int r;
};
class B
{
public:
void show()
{
cout << "A is" << r;
}
};
class C :public A
{
public:
void show()
{
cout << "C is " << 2 * r << endl;
}
};
(二)抽象类
1.定义:一个类中至少有一个虚函数,那么就称为该类为抽象类
2.几点规定:(1)由于抽象类中至少包含一个没有定义功能的纯虚函数。因此=,抽象类只能作为其他类的基类来使用,不能建立抽象类对象
(2)不允许从具体类派生出抽象类
(3)抽象类不能做函数的参数类型、函数的返回类型或显示转换类型
(4)可以声明指向抽象类的指针或引用,此指针可以指向他的派生类,进而实现多态
(5)如果派生类中没有定义纯虚函数的实现,而派生类只是继承基类的纯虚函数,则这个派生类仍然是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则派生类就不再是抽象类了,他是一个可以建立对象的具体类了。
以下函数可以通过编译吗?编译的结果是什么?
#include<iostream>
using namespace std;
class Base
{
public:
//在基类中对其声明为虚函数,在派生类中对这个函数进行了重写
virtual void FunTest1(int i)
{
cout << "Base::FunTest1()" << endl;
}
//没有对其进行重写,因为没有在基类中将其声明为虚函数,即使在派生类中已是虚函数,也不进行重写
void FunTest2(int i)
{
cout << "Base::FunTest2()" << endl;
}
//在基类中对其声明为虚函数,在派生类中对这个函数进行了重写
virtual void FunTest3(int i)
{
cout << "Base::FunTest3()" << endl;
}
//即使在基类中对其声明为虚函数,但是在虚类中,两个函数的原型不同(参数列表的个数不一样)因此没有对其形成重写
virtual void FunTest4(int i)
{
cout << "Base::FunTest4()" << endl;
}
};
class Derived :public Base
{
public:
virtual void FunTest1(int i)
{
cout << "Derived::FunTest1()" << endl;
}
virtual void FunTest2(int i)
{
cout << "Derived::FunTest2()" << endl;
}
void FunTest3(int i)
{
cout << "Derived::FunTest3()" << endl;
}
virtual void FunTest4(int i,int j)
{
cout << "Derived::FunTest4()" << endl;
}
};
int main()
{
Base b;//定义基类的对象
Base*pb;//定义基类对象的指针
Derived d;//定义派生类对象
pb = &d;//利用派生类为基类赋值(赋值兼容规则)
pb->FunTest1(0);//调用派生类函数
pb->FunTest2(0);//调用基类函数
pb->FunTest3(0);//调用派生类函数
pb->FunTest4(0);//调用基类函数
//pb->FunTest4(0, 0);//编译出错
system("pause");
return 0;
}
3.有关函数重载,同名隐藏,函数重写(覆盖)的区别
函数重载:同一作用域,函数名字相同,参数列表不同,与返回值是否相同没有关系
同名隐藏:在继承体系中,如果基类和派生类有相同名称的成员。如果使用派生类对象调用基类和派生类中的同名函数或成员,优先调用派生类说的同名函数。
同名成员变量,和类型无关。同名函数,和原型是否相同没有关系
函数重写:继承体系中,函数原型相同(返回值,参数列表,函数名)协变除外以及析构函数除外,基类函数必须有virtual关键字,但是访问限定符可以不相同
协变:在基类与派生类中,函数的返回值不同(基类返回基类类型的引用或者指针,派生类返回派生类类型的引用或者指针)
4.总结:
1、派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外)——重写概念
2、基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
3、只有类的非静态成员函数才能定义为虚函数,静态成员函数不能定义为虚函数。(需要利用this指针调用)
4、如果在类外定义虚函数,只能在声明函数时加virtual关键字,定义时不用加。
5、构造函数不能定义为虚函数,虽然可以将operator=定义为虚函数,但最好不要这么做,使用时容易混淆
6、不要在构造函数和析构函数中调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会出现未定义的行为。
7、最好将基类的析构函数声明为虚函数。(析构函数比较特殊,因为派生类的析构函数跟基类的析构函数名称不一样,但是构成覆盖,这里编译器做了特殊处理)
8、虚表是所有类对象实例共用的
四、有关虚表
(一)单继承
(1)派生类中有对基类中的虚函数重写
#include<iostream>
using namespace std;
typedef void(*FuncPer)();
class Base
{
public:
virtual void FunTest1()
{
cout << "Base::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "Base::FunTest2()" << endl;
}
virtual void FunTest3()
{
cout << "Base::FunTest3()" << endl;
}
int _b;
};
class Derived :public Base
{
public:
virtual void FunTest1()
{
cout << "Derived::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "Derived::FunTest2()" << endl;
}
//新增的虚函数
virtual void FunTest4()
{
cout << "Derived::FunTest4()" << endl;
}
int _d;
};
void PrintVF(Base& b)
{
FuncPer*pFun = (FuncPer*)(*(int*)&b);//(*(int*)&b)相当于取出虚表的前四个字节中的内容,
//(FuncPer*)(*(int*)&b)取出这个虚表中保存的内容,即由虚表查询虚函数
while (*pFun)
{
(*pFun)();
pFun++;
}
}
void Funtest(Base&b)
{
PrintVF(b);
}
int main()
{
Base b;//定义基类的对象
b._b = 0;
cout << sizeof(b) << endl;
Derived d;//定义派生类对象
Funtest(b);
Funtest(d);
system("pause");
return 0;
}
分析过程:
下面改变基类中虚函数的次序:
则发现虚函数的调用,与基类中的虚函数的次序有关
具体分析过程:
在基类中,定义虚函数,并在派生类中进行重写,可以发现在最后的结果中多了四个字节,通过分析可得,这个字节中保存的是一个地址,通过进一步的调用,发现这四个字节保存的是三个地址,从而的知,这三个地址保存的是基类虚函数中的地址,而在派生类中,对基类中的部分虚函数进行重写,从而用派生类中重写的虚函数,顶替相同偏移量中的基类虚函数,如若没有重写,则调用基类中的虚函数表,最后将派生类中新增的虚函数添加到虚表之后。
(2)派生类中没有对基类中的虚函数进行重写
#include<iostream>
using namespace std;
typedef void(*FuncPer)();
class Base
{
public:
virtual void FunTest1()
{
cout << "Base::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "Base::FunTest2()" << endl;
}
int _b;
};
class Derived :public Base
{
public:
virtual void FunTest3()
{
cout << "Derived::FunTest3()" << endl;
}
virtual void FunTest5()
{
cout << "Derived::FunTest5()" << endl;
}
//新增的虚函数
virtual void FunTest4()
{
cout << "Derived::FunTest4()" << endl;
}
int _d;
};
void PrintVF(Base& b)
{
FuncPer*pFun = (FuncPer*)(*(int*)&b);//(*(int*)&b)相当于取出虚表的前四个字节中的内容,
//(FuncPer*)(*(int*)&b)取出这个虚表中保存的内容,即由虚表查询虚函数
while (*pFun)
{
(*pFun)();
pFun++;
}
}
void Funtest(Base&b)
{
PrintVF(b);
}
int main()
{
Base b;//定义基类的对象
b._b = 0;
cout << sizeof(b) << endl;
Derived d;//定义派生类对象
Funtest(b);
cout << sizeof(d) << endl;
Funtest(d);
system("pause");
return 0;
}
总结:(派生类中虚表的构造)
1.基类中的虚表拷贝一份
2.检测派生类中是否对基类中的虚函数进行重写——>用派生类中重写的虚函数来替换相同偏移量位置的基类函数
3.在虚表之后添加派生类自己的虚函数
(二)多继承
由于在派生类中是否对基类中的虚函数的重写,只是替换相同偏移量的函数,则在多继承中不对在不进行重写的虚函数进行讨论
#include<iostream>
using namespace std;
typedef void(*FuncPer)();
class A
{
public:
virtual void FunTest1()
{
cout << "A::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "A::FunTest2()" << endl;
}
virtual void FunTest3()
{
cout << "A::FunTest3()" << endl;
}
int _a;
};
class B
{
public:
virtual void FunTest1()
{
cout << "B::FunTest1()" << endl;
}
//新增的虚函数
virtual void FunTest4()
{
cout << "B::FunTest4()" << endl;
}
virtual void FunTest5()
{
cout << "B::FunTest5()" << endl;
}
int _b;
};
class D :public A, public B
{
public:
virtual void FunTest1()
{
cout << "D::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "D::FunTest2()" << endl;
}
virtual void FunTest4()
{
cout << "D::FunTest4()" << endl;
}
virtual void FunTest6()
{
cout << "D::FunTest6()" << endl;
}
int _d;
};
void PrintVF(A& a)
{
FuncPer*pFun = (FuncPer*)(*(int*)&a);//(*(int*)&b)相当于取出虚表的前四个字节中的内容,
//(FuncPer*)(*(int*)&b)取出这个虚表中保存的内容,即由虚表查询虚函数
while (*pFun)
{
(*pFun)();
pFun++;
}
}
void PrintVF(B& b)
{
FuncPer*pFun = (FuncPer*)(*(int*)&b);//(*(int*)&b)相当于取出虚表的前四个字节中的内容,
//(FuncPer*)(*(int*)&b)取出这个虚表中保存的内容,即由虚表查询虚函数
while (*pFun)
{
(*pFun)();
pFun++;
}
}
int main()
{
//cout << sizeof(D) << endl;
A a;//定义基类的对象
B b;
D d;//定义派生类对象
d._a = 0;
d._b = 1;
d._d = 2;
A&pa = d;
B&pb = d;
PrintVF(pa);
PrintVF(pb);
system("pause");
return 0;
}
具体调用过程:先继承的基类的虚表在前,将派生类自己新增的虚表放在第一张的虚表之后(共有两张虚表,则两个基类各自一张)
(三)菱形继承
有关带虚函数的虚拟继承
//菱形继承+虚继承+虚函数
#include<iostream>
using namespace std;
typedef void(*FuncPer)();
class A
{
public:
virtual void FunTest1()
{
cout << "A::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "A::FunTest2()" << endl;
}
int _a;
};
class C1 :virtual public A
{
public:
virtual void FunTest1()
{
cout << "C1::FunTest1()" << endl;
}
virtual void FunTest3()
{
cout << "C1::FunTest3()" << endl;
}
int _c1;
};
class C2 :virtual public A
{
public:
virtual void FunTest1()
{
cout << "C2::FunTest1()" << endl;
}
virtual void FunTest4()
{
cout << "C2::FunTest4()" << endl;
}
int _c2;
};
class D :public C1, public C2
{
public:
virtual void FunTest1()
{
cout << "D::FunTest1()" << endl;
}
virtual void FunTest3()
{
cout << "D::FunTest3()" << endl;
}
virtual void FunTest4()
{
cout << "D::FunTest4()" << endl;
}
virtual void FunTest5()
{
cout << "D::FunTest5()" << endl;
}
int _d;
};
void PrintVF(A& a)
{
FuncPer*pFun = (FuncPer*)(*(int*)&a);//(*(int*)&b)相当于取出虚表的前四个字节中的内容,
//(FuncPer*)(*(int*)&b)取出这个虚表中保存的内容,即由虚表查询
while (*pFun)
{
(*pFun)();
pFun++;
}
}
void PrintVF(C1& c1)
{
FuncPer*pFun = (FuncPer*)(*(int*)&c1);//(*(int*)&b)相当于取出虚表的前四个字节中的内容,
//(FuncPer*)(*(int*)&b)取出这个虚表中保存的内容,即由虚表查询
while (*pFun)
{
(*pFun)();
pFun++;
}
}
void PrintVF(C2& c2)
{
FuncPer*pFun = (FuncPer*)(*(int*)&c2);//(*(int*)&b)相当于取出虚表的前四个字节中的内容,
//(FuncPer*)(*(int*)&b)取出这个虚表中保存的内容,即由虚表查询虚函数
while (*pFun)
{
(*pFun)();
pFun++;
}
}
int main()
{
cout << sizeof(A) << endl;
cout << sizeof(C1) << endl;
cout << sizeof(C2) << endl;
cout << sizeof(D) << endl;
A a;//定义基类的对象
C1 c1;
C2 c2;
D d;
d._a = 0;
d._c1 = 1;
d._c2 = 2;
d._d = 3;
A&pa = d;
C1&pc1 = d;
C2&pc2 = d;
PrintVF(pa);
PrintVF(pc1);
PrintVF(pc2);
system("pause");
return 0;
}
总结:在菱形继承+虚函数继承中,在派生类中,最底层只有一个对最底层基类中虚函数的重写(A),在C1与C2中,里面分别有自己的偏移量表格与虚函数,在派生中,如果有对其里面的重写了,则在打印虚函数表示,将其替换即可。