目录
一. 多态的概念
二. 多态的定义及实现
1.多态的构成条件
2 虚函数
3虚函数的重写
虚函数重写的两个例外:
1. 协变(父类与子类虚函数返回值类型不同)
2. 析构函数的重写(父类与子类析构函数的名字不同)
三.C++11 override 和 final
1. final:修饰虚函数,表示该虚函数不能再被重写
2. override: 检查子类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
四.重载、覆盖(重写)、隐藏(重定义)的对比
五.抽象类
接口继承和实现继承
六.多态的原理
虚函数表
实现多态的原理
动态多态与静态多态
为什么父类对象调用虚函数不能多态?
七.单继承与多继承子类虚函数表
多继承中的虚函数表
八.多态的常见问题
1. 什么是多态?
2. 什么是重载、重写(覆盖)、重定义(隐藏)
3.多态的实现原理
4.inline函数可以是虚函数吗?
5. 静态成员可以是虚函数吗?
6. 构造函数可以是虚函数吗?
7 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
8. 对象访问普通函数快还是虚函数更快?
9.什么是抽象类?抽象类的作用?
那么在继承中要构成多态还有两个条件:
1. 必须通过 父类 的 指针或者引用 调用 虚函数2. 被调用的函数必须是虚函数,且子类必须对父类的虚函数进行重写
这里再次强调多态的条件:
必须使父类的指针或引用调用虚函数。 父类的对象调用虚函数不是多态!!!且子类必须重写调用的虚函数,
虚函数:即被virtual修饰的类成员函数称为虚函数(注:这里的vitrtual与虚继承里的virtual没有任何关系)
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
class person
{
public:
virtual void buyTicket()
{
cout << "全价" << endl;
}
};
class student:public person
{
public:
virtual void buyTicket() //对person里的虚函数进行重写
{
cout << "半价" << endl;
}
};
void fun(person& s)
{
s.buyTicket();
}
int main()
{
person a;
student b;
fun(a);
fun(b);
return 0;
}
注:1.这里虚函数重写,重写的是函数实行方式(函数里面的内容),并不会改变原有的函数名与参数。
#include
using namespace std;
class A
{
public:
virtual void print(int a = 1)
{
cout << a << endl;
}
};
class B :public A
{
public:
void print(int b = 2)
{
cout << b << endl;
}
};
int main()
{
A a;
B b;
A& parent = a;
A& parent2 = b;
parent.print();
parent2.print();
return 0;
}
2.在重写父类虚函数时,子类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议 这样使用
class Person
{
public:
virtual Person* f()
{
return new Person;
}
};
class Student : public Person
{
public:
virtual Student* f()
{
return new Student;
}
};
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
//数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
同时:final还可以放在类名后修饰类,被修饰的类无法被其他类继承
注:final要修饰的是虚函数,不是虚函数会报错。
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
注:虽然抽象类不能定义对象,但其还是可以定义指针的。
这里我们先看这段代码:
class B
{
public:
virtual void print()
{
cout << "print" << endl;
}
private:
int _b = 1;
};
int main()
{
cout << sizeof(B);
return 0;
}
结果:
这里我们发现B的大小为8并不为4,那么这个类里除了存放了一个整形还存放了什么吗?这里我们可以通过调试的监视框口。
发现类里面还存了一个_vfptr的指针(
#include
using namespace std;
class A
{
public:
virtual void fun1()
{
cout << "A--fun1" << endl;
}
virtual void fun2()
{
cout << "A--fun2" << endl;
}
private:
int _a = 1;
};
class B :public A
{
public:
void fun1()
{
cout << "B--fun1" << endl;
}
void fun3()
{
cout << "B--fun3" << endl;
}
private:
int _b = 2;
};
int main()
{
A a;
B b;
return 0;
}
然后调试:
A的虚函数表内容:
B虚函数表内容:
1.子类对象中也有一个虚表指针。虚表里的内容一部分继承父类的,一部分是子类重写父类虚函数的。
3.另外fun2继承下来后是虚函数,所以放进了虚表,子类func3也是虚函数也放到了虚表中了,
注:
同类的对象:共享一张虚表
虚表在代码区,虚函数也在代码区
代码大致验证:
class B
{
public:
virtual void print()
{
cout << "print" << endl;
}
private:
int _b = 1;
};
void fun()
{
cout << "fun" << endl;
}
int main()
{
B b1;
cout << sizeof(B)<
1. 先将父类中的虚表内容拷贝一份到子类虚表中。2. 如果子类重写了基类中某个虚函数,用子类自己的虚函数覆盖虚表中父类的虚函数3. 子类自己新增加的虚函数按其在子类中的声明次序增加到派生类虚表的最后
首先我们要知道,对象调用虚函数的大致过程:
通过虚表指针,找到虚表,然后再虚表上找到对应虚函数的地址,调用。
同时我们知道父类的指针或引用指向子类时,会指向对应子类中父类的那一部分(恰巧这部分包含了对应的子类虚表)。
再上面我们说过子类虚函数重写时会进行虚函数的覆盖。
所以当父类对象的指针或引用指向父类时,调用的时父类的虚表。
指向子类时,调用子类的虚表。
由于子类的虚表是拷贝父类的虚表,并进行了虚函数的重写,改变了虚表的内容,这导致了多态。
这里结合一个例子说明:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
我们知道实现多态的重点是虚表的改变与调用的是父类还是子类的虚表。
而再C++中规定,子类赋值给父类时,通过切片给父类赋值,这个过程中并不会将子类的虚表赋值给父类对象,所以父类对象的虚表还是原来的,调用虚函数时还是找父类的虚表。因此不能实现多态。
那为什么子类的虚表为什么不给赋值给父类呢?
这里我们可以通过反证法举一个特殊的例子说明:
当父类的析构函数是虚函数,并且子类也进行了重写。
这时如果子类的虚表赋值给父类,这是当使用delete释放父类对象时,会调用子类的析构函数。那么父类对象在析构时,就会出现大问题。
单继承子类的虚表:先将父类的虚表拷贝下来,修改重写的虚函数,在把自己的虚函数加入。
这里我们看到子类的虚函数表里并没有少了两个函数指针,这是为什么呢?
这里我们要了解,我们从监视框口看到的并不是真实的内容,而是编译器修饰过的。这里我们要从调试的内存框口中去看。
这里我们根据虚函数表最后一个存放nullptr可以判断,虚函数表中有四个函数地址。
这里我们还可以以一种更加直观的方法观察:
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Base b;
Derive d;
// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数
//指针的指针数组,这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最
//后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再
//编译就好了。
VFPTR * vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
VFPTR* vTabled = (VFPTR*)(*(int*)&d);
PrintVTable(vTabled);
return 0;
}
子类分别将父类的虚表拷贝下来,若子类重写虚函数,就将对应的虚表改写。
多继承子类的未重写的虚函数放在第一个继承父类部分的虚函数表中
函数重载:在同一作用域内,函数名命相同,参数的个数,类型,数量,以及参数的顺序不同。
重写:继承中。两个函数分别在子类与父类里,且函数名,函数参数,返回值相同,且父类的函数被virtual修饰。
重定义(隐藏):在继承中,两个函数一个在父类一个在子类里,函数名相同。
构成条件:父类的指针或引用调用虚函数,子类对虚函数必须进行重写
实现原理:子类进行虚函数重写会改变虚表,而当父类指向父类是,调用虚函数,要调用父类的虚表,父类指向子类是,调用虚函数,要调用子类的虚表。由于子类一改写虚表,所以导致父类指向不同类的对象是,调用同一虚函数,会有不同的结果。
不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表
不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。(先有鸡还是先有蛋的问题)
可以,当父类针织指向一个用new开辟子类的空间时,最好把基类的析构函数定义成虚函数