提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
在生活中,我们会有很多这种场景,不同的角色执行相同一件事,得到不同的结果,在编程上实现这种面对不同的角色执行同一件事,得到不同的结果时,就需要用到多态来实现。
1、多态的定义及实现
多态就是声明一个基类对象的指针或者引用,指向派生类的对象,妙的是这个指针或者引用的类型是基类的,但是内容(指针或者引用所指向的东西)却是派生类的。好处就是可以在运行的时候根据环境的变化修改它所指向的派生类,根据条件的变化、用户的选择等等等等修改它的所指。比如可以在基类里定义一些虚拟抽象方法(不必去实现),而在每一个派生类里各自去实现自己的同名方法,这样使用一个基类指针或者引用,当修改了指针或者引用所指的派生类后,通过相同名字的方法调用不同的方法实现。
#include <iostream>
using namespace std;
class Colour
{
public:
//可以不用实现 虚函数关键字 virtual 必须加
virtual void Print()
{
}
};
class Black : public Colour
{
public:
//派生类,可以不加关键字 virtual 建议加上
virtual void Print()
{
cout << "颜色是:Black" << endl;
}
};
class Blue : public Colour
{
public:
//派生类,可以不加关键字 virtual 建议加上
void Print()
{
cout << "颜色是:Blue"<< endl;
}
};
//通过基类的引用或指针,编译器在运行时通过基类所指向
//调用这个派生类所实现的重写
//必须是基类的引用或指针 否则就是普通调用
void Test(Colour& member)
{
member.Print();
}
int main()
{
Black bk;
Blue bl;
Test(bk);
Test(bl);
return 0;
}
编译结果
**通过以上的代码,我们得知,多态的构成条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写**
1.1、虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
class Colour
{
public:
//可以不用实现 虚函数关键字 virtual 必须加
virtual void Print()
{
}
};
1.2、虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。
class Colour
{
public:
//可以不用实现 虚函数关键字 virtual 必须加
virtual void Print()
{
}
};
class Black : public Colour
{
public:
//重写基类的print函数
virtual void Print()
{
cout << "颜色是:Black" << endl;
}
};
1.3、虚函数重写的例外协变
协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型可以不同。即派生类和基类,返回值类型必须是一个父子类关系的指针或引用(其他类的也行),称为协变(了解即可)。
class A
{
};
class B : public A
{
};
class Colour
{
public:
virtual A* Print()
{
return nullptr;
}
};
class Black : public Colour
{
public:
virtual B* Print()
{
return nullptr;
}
};
1.4 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person
{
public:
//virtual ~Person()
~Person()
{
cout << "~Person()" << endl;
delete[] _p;
}
int* _p = new int[10];
};
class Student : public Person
{
public:
virtual ~Student()
{
cout << "~Student()" << endl;
delete[] _s;
}
int* _s = new int[10];
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
//如果基类的析构不是虚函数的话
//这里的delete释放资源,就会造成内存泄漏
delete p1;
delete p2;
return 0;
}
对象模型是这样的
这里Student的资源,并没有回收,如果基类的析构是虚函数的话,基类所指向的是派生类,就会调用派生类的析构函数,这时才不会造成内存泄漏。
1.5 C++11 新增关键字 override 和 final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
- final:修饰虚函数,表示该虚函数不能再被重写,修饰类表示该类不能被继承。
class Person
{
public:
//加final代表不能被重写,如果重写了编译就会报错
virtual void print() final {
}
};
- override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Student : public Person
{
public:
//在重写的函数后加override 判断是否构成重写,未构成则编译报错
virtual ~Student() override
{
cout << "~Student()" << endl;
delete[] student_ptr;
}
int* student_ptr = new int[10];
};
1.6 重载、覆盖(重写)、隐藏(重定义)的对比
1.7 纯虚函数(抽象类)
虚函数的后面写上 = 0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Person
{
public:
virtual void print() = 0;
};
class Student : public Person
{
public:
virtual void print()
{
cout << "Student 重写成功" << endl;
}
};
int main()
{
//纯虚函数,是无法实例出对象的
//Person p;
//必须由派生类重写了虚函数,派生类才能实例出对象
//派生类也没有重写,那派生类也成了纯虚函数,无法实例化对象
Student s;
s.print();
return 0;
}
纯虚函数,跟 override 关键字的区别就是,纯虚函数不重写,无法实例化对象,override是检查是否重写成功了。
2、虚函数表
下面这个类的大小为多少?
class A
{
public:
virtual void func()
{
cout << "A::func()" << endl;
}
protected:
int _a;
};
int main()
{
//在64位系统下的指针大小为8个字节
cout << "64位系统下指针大小:" << sizeof(void*) << endl;
//A类的大小由于有虚函数表的存在,会隐藏生成一个虚函数表指针
//再加一个int 4个字节大小,由于内存对齐,4+8不等于最大数8的倍数
//所以系统会默认增加多四个字节,所以最终大小为16个字节
cout << "类A的大小为:" << sizeof (A) << endl;
return 0;
}
由于有虚函数的存在,每一个类都会在初始化列表后生成一个虚函数表指针,指向虚函数的首地址,如果有多个虚函数,则是继续添加到虚函数指针表里,虚函数指针大小也是没有改变的,普通调用函数,在编译时已经确定好了函数地址,这是静态绑定,多态是在运行时根据,不同的基类指针所指,再通过虚函数表调用不同的重写函数。
3、多态的原理
class Person
{
public:
virtual void func1()
{
cout << "Person::func1()" << endl;
}
protected:
int _p = 1;
};
class Student : public Person
{
public:
//派生类继承了基类,也同样继承了基类的虚函数表,完成了重写新的地址
//将基类的虚函数覆盖
//而派生类的func2不是虚函数,所以不进虚表
virtual void func1()
{
cout << "Student::func1()" << endl;
}
//
void func2()
{
cout << "Student:: func2()" << endl;
}
protected:
int _s = 2;
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
p1->func1();
p2->func1();
return 0;
}
虚函数的对象模型:
所以多态也可以理解为,传递不同的虚表,执行不同的函数。
3.1 多继承关系的虚函数表
代码如下(示例):
class A1
{
public:
virtual void func1()
{
cout << "A1::func1():";
}
virtual void func2()
{
cout << "A1::func2():";
}
protected:
int _a1 = 1;
};
class A2
{
public:
virtual void func1()
{
cout << "A2::func1():";
}
virtual void func2()
{
cout << "A2::func2():";
}
protected:
int _a2 = 2;
};
class B : public A1,public A2
{
public:
virtual void func1()
{
cout << "B::func1():";
}
//如果B类中,也有一个独自的虚函数,有可能会保存在第一个虚表中的最后
virtual void func3()
{
cout << "B::func3():";
}
protected:
int _b = 3;
};
typedef void(*VFptr)();
void PrintVTable(VFptr vfptr[],int n)
{
for(int i = 0;i < n;++i)
{
vfptr[i]();
cout << (void*)vfptr[i] << endl;
}
cout << endl;
}
int main()
{
A1 a1;
//依次打印a1中的虚函数表
PrintVTable((VFptr*)*((void**)&a1),2);
A2 a2;
//依次打印a2中的虚函数表
PrintVTable((VFptr*)*((void**)&a2),2);
//由于B继承了两个有虚函数的类,所以B有两个虚函数表
B b;
//依次打印b中A1部份的虚函数表
PrintVTable((VFptr*)*((void**)&b),3);
A2* aptr = &b;
//依次打印b中A2部份的虚函数表
PrintVTable((VFptr*)*((void**)aptr),2);
return 0;
}
虚函数对象模型
代码执行结果
问答题
- 什么是多态?1, 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
2 , 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态 - 什么是重载、重写(覆盖)、重定义(隐藏)?参考:当前目录1.6的对比
- 多态的实现原理?通过传递不同的虚函数表,实现不同的形态。
- inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
- 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
- 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
- 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。参考当前目录1.4的详解
- 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
- 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,在构造函数的初始化列表中初始化虚表指针,一般情况下存在代码段(常量区)的。
- C++菱形继承的问题?虚继承的原理?答:参考继承博客。注意这里不要把虚函数表和虚基表搞混了。
- 什么是抽象类?抽象类的作用?答:参考(3.抽象类)。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。
概念查考
- 下面哪种面向对象的方法可以让你变得富有( )
A: 继承 B: 封装 C: 多态 D: 抽象 - ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定 - 面向对象设计中的继承和组合,下面说法错误的是?()
A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
C:优先使用继承,而不是组合,是面向对象设计的第二原则
D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现 - 以下关于纯虚函数的说法,正确的是( )
A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类
C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数 - 关于虚函数的描述正确的是( )
A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B:内联函数不能是虚函数
C:派生类必须重新定义基类的虚函数 D:虚函数可以是一个static型的函数 - 关于虚表说法正确的是( )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表 - 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同
D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表
答案:A,D,C,A,C,D,D。