面向对象特性–多态
文章目录
ps:温馨提示,由于多态的很多知识点是建立在继承的基础上的,所以建议大家先去复习复习或者学习学习继承
可以参考博主的这篇文章:
(190条消息) 彻底搞定面试题–继承篇(C++继承讲解),万字解析,弄透继承!_龟龟不断向前的博客-CSDN博客
🚀1.多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
火车站买票,有学生,普通人,军人,三类对象去买票,明明都是去完成买票这种行为,但是他们最终产生的效果是不同的:
学生–票价优惠,普通人–全价买票,军人–优先买票。这就是现实生活中的多态行为。
🚀2.多态的定义与实现
🍉静态多态
静态的多态是在程序编译时就确定的。其中函数重载与模板就实现了多态。
#include<iostream>
using namespace std;
int main()
{
int a;
double da;
cin >> a;
cin >> da;
cout << a << endl;
cout << da << endl;
int i = 0, j = 1;
double d = 1.1, e = 2.2;
swap(i, j);
swap(d, e);
return 0;
}
上述调用的输入,输出中,a和ch的输入,输出调用的是不同的operator>>
和operator<<
包括int
类型与double
类型的交换,调用的也是不一样的swap
函数。
🍉动态多态
首先我们需要介绍两个概念,虚函数以及虚函数的重写
🍇虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
class Person {
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
BuyTicket()
就是虚函数。
而上一篇文章中,继承时,在继承方式前面加上virtual
关键字是虚继承
🍇虚函数的重写(覆盖)
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
ps:这里我们不要把继承中的隐藏和多态中的重写弄混淆了,下方有总结
class Person {
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket() //完成了父类虚函数BuyTicket的重写
{
cout << "买票-半价" << endl;
}
};
🍇构成动态多态的条件
- 继承下,子类完成父类的虚函数的重写。
- 使用父类的指针或者引用去调用子类重写的虚函数。
ps:如果使用父类对象去调用子类重写的虚函数,是不会构成多态的,最终调用的都是父类的虚函数
代码演示:
#include<iostream>
using namespace std;
class Person {//基类
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }//父类的虚函数
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
class Soldier : public Person
{
public:
virtual void BuyTicket() { cout << "优先-买票" << endl; }
};
void f(Person& p)
{
//传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
p.BuyTicket();
}
void f(Person* p)
{
//传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
p->BuyTicket();
}
int main()
{
Person p; // 普通人
Student st; // 学生
Soldier so; // 军人
f(p);
f(st);
f(so);
cout << endl;
f(&p);
f(&st);
f(&so);
cout << endl;
return 0;
}
上述代码就演示了,明明是不同对象(普通人,学生,军人)调用同一个ButTicket()
函数,最终达到的是不同的效果。
下面演示一下,如果没有构成多态(使用父类对象去调用子类的虚函数),看看是怎样的效果
#include<iostream>
using namespace std;
class Person {//基类
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }//父类的虚函数
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
class Soldier : public Person
{
public:
virtual void BuyTicket() { cout << "优先-买票" << endl; }
};
void f(Person p)//这里我们使用了父类的对象来调用子类的虚函数,不符合构成多态的条件
{
p.BuyTicket();
}
int main()
{
Person p; // 普通人
Student st; // 学生
Soldier so; // 军人
f(p);
f(st);
f(so);
cout << endl;
return 0;
}
最终的效果是调用的都是父类的虚函数
🍇那些同样构成动态多态的例外
1. 协变(了解即可)
协变(基类与派生类虚函数返回值类型不同) ,要求:即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
大家可以看看下面的构不构成多态。
#include<iostream>
using namespace std;
class a{};
class b : public a {};//父子关系--分别做以下的父子关系虚函数的返回值
class person {
public:
virtual a* f() {
cout << "a* person::f()" << endl;
return new a;
}
};
class student : public person {
public:
virtual b* f() {
cout << "b* student::f()" << endl;
return new b;
}
};
void func(person& p)
{
p.f();
}
void func(person* pp)//函数重载
{
pp->f();
}
int main()
{
person p;
student s;
func(p);
func(s);
cout << endl;
func(&p);
func(&s);
return 0;
}
上述的student
的虚函数和person
的虚函数的返回类型不一样,但是person
虚函数的返回类型是A
的指针(父类指针),student
虚函数的返回类型是B
的指针(子类指针)。符合协变情况,所以也构成多态。
ps:建议不要写协变
2.子类的虚函数重写可以不添加virtual
关键字
因为认为,子类从父类继承,将父类的虚属性也继承下来了,就算不写virtual
关键字,也认为其是虚函数,从而构成多态。
同学们可以将上述代码中,子类的重写虚函数virtual
关键字去掉,同样也构成了多态。
3.析构函数的重写((基类与派生类析构函数的名字不同)
如果严格遵守多态的构成条件,那么子类析构函数就算定义成虚函数也无法完成重写了,因为父子类的析构函数是不可能同名的。
编译器做了一件事,凡是父子类的析构函数,都将父子类的析构函数名变成destructor()
,其目的就是为了父子类的析构函数可以构成多态。
这也解决了咱们上篇文章的问题:为什么父类与子类的析构函数构成了隐藏,因为他们的析构函数同名了。如果不构成多态,那么就构成了隐藏。
那么为什么要将父类的析构函数与子类的析构函数构成多态呢?因为会出现以下场景
#include<iostream>
using namespace std;
class Person {
public:
~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
当我们使用new
,申请了一块子类空间,而我们想用父类指针去指向这块子类空间,我们像上述代码一样,不重写析构函数,那么下方的delete p2
将是错误的,运行结果如下
而当我们完成子类的析构函数的重写(在父子类的析构函数前加上virtual),结果才是正确的
ps:我们是建议子类重写父类的析构函数的,这样不仅可以实现多态,而且是不影响正常的使用的
🚀3.如何让父类的虚函数无法被重写–final
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
-
当我们在虚函数的后面加上
final
关键字时,这个虚函数是无法被子类的虚函数重写的所以上述代码会报错!
-
而上一篇文章继承中,在类的后面加上
final
关键字时,这个类时无法被继承的
要分清楚这个关键字的不同用法哦
🚀4.如何强制要求完成虚函数的重写–override
如果C语言
和C++
看作是两兄弟,那么C语言
更像是一个自由自在的孩子,而C++
更像一个有规矩的孩子,例如C++
中有封装,不能随便访问类的成员变量
而override
就像一个监督你有没有完成虚函数的重写的老师一样,如果在函数后面加了override
又没有完成虚函数的重写,程序就会报错
#include<iostream>
using namespace std;
class Car{
public:
virtual void Drive(char ch){}
};
class Benz :public Car {
public:
virtual void Drive(int ch) override { cout << "Benz-舒适" << endl; }
};
int main()
{
return 0;
}
虚函数重写的要求是:子类重写的虚函数必须完全跟子类的虚函数一致(函数的返回类型,函数名,函数参数)
而上述代码子类的虚函数与父类的虚函数的函数参数不同,那么程序就会报错,因为你没有完成虚函数的重写
所以咱们在写子类的析构函数时,可以加一个override
来帮我们检查,是不是完成了虚函数的重写
🚀5.抽象类
现实生活中:
学生是一个具体类,因为可以让学生定义一个对象 小明 小红 小刚等(小学小人组)
狗是一个具体类,因为可以让狗定义对象 小白 旺财 莱温斯基等
但是植物不是一个具体类,因为不会用它来定义对象,太抽象了,太广泛了,严格来说,人和动物也不能叫做具体类,他们定义的对象无法归结成一类
像这种我们通常将其称为抽象类,而抽象类是只用来继承,不用来定义对象的
在介绍抽象类之间,我们先介绍什么叫纯虚函数
🍉纯虚函数
class car
{
public:
virtual void drive() = 0
{
cout << "class car()";
}
};
再虚函数的声明时,添加一个赋值0,那么这个虚函数就是纯虚函数
那么类的虚函数是纯虚函数的类,叫做抽象类
🍉抽象类的目的
#include<iostream>
using namespace std;
class car
{
public:
virtual void drive() = 0;
};
class benz :public car
{
public:
void drive()
{
cout << "benz-舒适" << endl;
}
};
class bmw :public car
{
public:
virtual void drive()
{
cout << "bmw-操控" << endl;
}
};
int main()
{
//car c;//error抽象类car,无法定义对象
//普通对象
benz b;
bmw bm;
b.drive();
bm.drive();
cout << endl;
//多态
car* pbenz = new benz;
pbenz->drive();
car* pbmw = new bmw;
pbmw->drive();
cout << endl;
car& rbanz = b;
car& rbmw = bm;
rbanz.drive();
rbmw.drive();
cout << endl;
return 0;
}
就像我们虚函数的时候说过,就算虚函数的重写中,子类虚函数不加virtual,也可以实现虚函数的重写,因为继承使得子类的对象函数有了虚函数!
同样地,上述代码中,由于子类的继承,父类的纯虚函数是会被继承下来的,如果不重写这个纯虚函数,那么这个子类也将成为一个抽象类,会使得子类无法定义对象!
总结:继承抽象类的子类,一定要完成抽象类的纯虚函数的重写,否则无法使用子类创建对象,像这种继承抽象类,我们通常也称为接口继承,可以理解成,如果玩多态就可以用抽象类,不玩多态,就不玩抽象类
🚀6.虚函数的底层原理
🍉笔试题:当类中有虚函数时,计算这个类的内存大小
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
virtual void Func2()
{
cout << "Func2()" << endl;
}
void Func3()
{
cout << "Func3()" << endl;
}
private:
int _b = 1;
char _ch = '\0';
};
int main()
{
cout << sizeof(Base) << endl;
Base bs;
return 0;
}
大家可以自己运行一下,看看答案是多少?
🍉虚函数表指针(虚表指针)
我们通过vs监视窗口可以观察到对象bs的成员跟我们想的不一样
成员列表的第一个位置多放了一个虚函数表指针(虚表指针)所以上述的答案才会是12(还进行了内存对齐)
🍉虚函数表(虚表)
根据上述指针的名字–虚表指针也知道,它肯定指向的是虚表,下面我们借助vs的内存窗口来看看效果
也就是说,成员变量中的虚表指针(虚表的首元素地址)指向的是虚表(函数指针数组),而虚表里面存放的是虚函数的地址
🍉虚表指针和虚表是在何时生成的
还没进入构造函数:
进入构造函数(初始化列表):
因为虚表指针也算作成员变量,在类与对象中说过,成员变量是在初始化列表中定义的,所以虚表指针是在构造函数中生成的
而虚函数在编译过程中就要确定地址,所以虚表是在编译时确定的
🚀7.多态的底层原理
不知道大家是否有疑问:为什么实现多态非要是父类的引用或者指针去调用子类的虚函数,为什么父类的对象去对用就不可以呢?
🍉虚表的继承与覆盖
🍇父类中只有一个虚函数
咱们继续拿买票的例子来解释
#include<iostream>
using namespace std;
class Person {//基类
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }//父类的虚函数
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
class Soldier : public Person
{
public:
virtual void BuyTicket() { cout << "优先-买票" << endl; }
};
void f(Person& p)
{
//传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
p.BuyTicket();
}
void f(Person* p)
{
//传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
p->BuyTicket();
}
int main()
{
Person p; // 普通人
Student st; // 学生
Soldier so; // 军人
f(p);
f(st);
f(so);
cout << endl;
f(&p);
f(&st);
f(&so);
cout << endl;
return 0;
}
这也是为什么完成重写了,就调用各自的函数,没有完成重写就调用父类的函数的原因之一
🍇父类中有多个虚函数
#include<iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "Base()" << endl;
}
virtual void Func1()
{
cout << "virtual Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "virtual Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "virtual Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
上述代码中,父类中有两个虚函数,但是子类中只重写了其中的一个虚函数,咱们用vs监视窗口给大家演示以下效果!
🍉只能用父类指针或引用才构成多态的原因
#include<iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
int _p = 1;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }//进行了覆盖(重写)
int _s = 2;
};
void Func(Person& p)
{
p.BuyTicket();
}
void Func(Person* p)
{
p->BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Func(Mike);
Student Johnson;
Johnson._p = 10;
Func(Johnson);
Func(Johnson);
return 0;
}
🍇引用调用
🍇指针调用
🍇对象调用
所以父类对象来调用子类的重写虚函数是无法构成多态的,因为这时父类对象中压根就没有子类对象的虚表
🚀8.虚函数在内存存储位置
虚函数是存在内存中的什么位置?请设计一个程序来测试
- 栈
- 堆
- 数据段(静态区)
- 代码段(常量区)
我们可以将栈,堆,数据段,代码段以及虚函数的地址打印以下,进行一个对比
这里给大家复习以下内存分布:(图解)
图片来源:
(157条消息) C++图解模板_龟龟不断向前的博客-CSDN博客
现在主要的问题是如何拿到虚函数的地址,由于虚表指针是放在成员变量中的第一个,而且一个指针的大小是4个字节(32位环境下)
,拿我们可以拿到对象的地址,取出前四个字节的地址内容,即可得到虚表指针的地址,再解引用即可得到第一个虚函数的地址
#include<iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "Base()" << endl;
}
virtual void Func1()
{
cout << "virtual Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "virtual Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "virtual Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int j = 0;
int main()
{
// 取虚表地址打印一下
Base b;
Base* p = &b;//取出其地址,然后取出前四个字节的地址
printf("虚函数的地址:%p\n", *(int*)p);//虚表指针再解引用,得到的才是虚函数的地址
int i = 0;
printf("栈上的地址:%p\n", &i);
printf("数据段的地址:%p\n", &j);
int* pi = new int;
printf("堆上的地址:%p\n", pi);
char* ptr = "abcdefg";
printf("代码段的地址:%p\n", ptr);
return 0;
}
🚀9.如何把虚表打印在屏幕上–并且得知是哪个虚函数的地址
上述给大家展示多态的底层时我们使用了两种方式
- vs监视窗口–但是监视窗口不一定就是真实的
- vs的内存窗口
这里介绍第三种方法:打印虚表
通过上面的学习我们知道,虚表是一个函数指针的数组(函数类型可能不同),既然我们可以得到虚表指针–即虚表的首地址
拿我们也可以像数组一样,将函数指针数组里面的内容(虚函数地址)打印出来,开干开干
#include<iostream>
using namespace std;
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a = 1;
};
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 = 2;
};
//将函数指针类型给他重命名
typedef void(*VFunc)();//由于函数指针名字的复杂性,咱们给他进行一个重命名
void PrintVFT(VFunc* ptr) //存ptr就是虚表指针
{
printf("虚表指针:%p\n", ptr);
for (int i = 0; ptr[i] != nullptr; ++i)
{
printf("VFT[%d]:%p->", i, ptr[i]);
ptr[i]();//函数的底层调用方式
}
printf("\n");
}
int main()
{
//先拿到函数虚表的地址
Base b;
int p = *(int*)&b;//先拿到虚表地址的数值
PrintVFT((VFunc*)p);//再将虚表地址强转以下即可
Derive d;
p = *(int*)&d;
PrintVFT((VFunc*)p);
return 0;
}
上述代码中Derive
继承了Base
,重写了func1
,没有重写func2
所以func1
调用的是自己的,func2
调用的是父类的,然后func3
和func4
都是子类自己的
这种方式真的是很牛,非常的直观,斑爷也称你为最强
🚀10.多继承下虚表的覆盖情况
#include<iostream>
using namespace std;
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
typedef void(*VFunc)();
void PrintVFT(VFunc* ptr) // 存函数指针的数组指针
{
printf("虚表指针:%p\n", &ptr);
printf("虚表地址:%p\n", ptr);
for (int i = 0; ptr[i] != nullptr; ++i)
{
printf("VFT[%d]:%p->", i, ptr[i]);
ptr[i]();
}
printf("\n");
}
int main()
{
Base1 b1;
Base2 b2;
Derive d;
int p = *(int*)&d;//取出第一个虚表地址的数值
PrintVFT((VFunc*)(p));
char* pc = ((char*)&d + sizeof(Base1));//得到第二个虚表指针的地址
p = *(int*)pc;//取出第二个虚表地址的数值
PrintVFT((VFunc*)(p));
return 0;
}
上述代码中:Derive
继承了Base
和 Base
,重写func1
,但是Base1
和Base2
中都有fun1
,子类的虚表会怎么覆盖和继承呢?
先继承的Base1
,再继承的Base2
,所以d成员中有两个虚表指针,Base1
的虚表指针在前,Base2
的虚表指针在后
大家仔细看上述代码,看是如何得到第二个虚表指针的
我们发现,Base1
和Base2
的func1
都被重写了,而Derive自己的虚函数只放在了第一个虚表里面,没有放在第一个虚表里面
🚀11.菱形继承–虚继承与虚函数结合问题(了解即可)
上一篇文章中,我们还遗留了一个问题,就是为什么虚基表中的初始位置放的是0,开始解决问题
#include<iostream>
using namespace std;
class A
{
public:
virtual void func()
{
cout << "A::func()" << endl;
}
public:
int _a;
};
// class B : public A
class B : virtual public A
{
public:
virtual void func()//这个是虚继承A过来的,不会生成虚表
{
cout << "B:func()" << endl;
}
virtual void func1()//这个是B自己的虚函数,会生成虚表
{
cout << "B:func1()" << endl;
}
public:
int _b;
};
// class C : public A
class C : virtual public A
{
public:
virtual void func()//这个是虚继承A过来的,不会生成虚表
{
cout << "C::func()" << endl;
}
virtual void func1()//这个是C自己的虚函数,会生成虚表
{
cout << "C::func1()" << endl;
}
public:
int _c;
};
class D : public B, public C
{
public:
virtual void func()
{
cout << "D::func()" << endl;
}
virtual void func1()
{
cout << "D::func1()" << endl;
}
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
咱们调用监视窗口和内存窗口来看一下底层
虚继承和虚基表同时存在时,那么成员变量中就既有虚表指针也有虚基表指针
而且我们发现:
虚基表的受位置放的是虚表指针对虚基表指针的偏移量,而我们上一篇文章中的例子没有虚表指针,所以首地址放的是0
🚀12.经典例题
🍉菱形继承构造函数的顺序
using namespace std;
class A{
public:
A(char *s)
{
cout << s << endl;
}
~A(){}
};
class B :virtual public A
{
public:
B(char *s1, char*s2)
:A(s1)
{
cout << s2 << endl;
}
};
class C :virtual public A
{
public:
C(char *s1, char*s2)
:A(s1)
{
cout << s2 << endl;
}
};
class D :public B, public C
{
public:
// 初始列表执行顺序跟声明有关,继承成员声明顺序,是按继承顺序算的
D(char *s1, char *s2, char *s3, char *s4)
:B(s1, s2)
, C(s1, s3)
, A(s1)
{
cout << s4 << endl;
}
};
int main()
{
D *p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}
大家思考一下程序的输出结果,答案运行一下即可
🍉虚函数的重写
class A
{
public:
A()
{}
virtual inline void func(int val = 1){ std::cout << "A->" << val << std::endl; }
virtual void test(){ func(); }
};
class B : public A
{
public:
void func(int val = 10000000){ std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
B*p = new B;
p->test();
return 0;
}
上述代码中,B继承下来A,重写时,返回值,函数名,函数参数要和A中的保持一致(其中A的缺省值也给了B,B中的缺省值是无效的)
这道题是一个C++设计的Bug
🚀13.总结
函数重载,隐藏(重定义),重写(覆盖)的区别
-
函数重载
条件:函数名相同函数参数不同,并且两个函数要在同一的作用域中
-
隐藏(重定义)
条件:继承中成员变量同名,成员函数同名即可
-
重写(覆盖)
条件:子类的虚函数要与父类的虚函数完全一致(函数名,函数参数,函数的返回值)
重写的要求比隐藏严格,继承中函数名相同时,不是重写便是隐藏
🚀14.多态的面试题大全
- 什么是多态?
- 什么是重载、重写(覆盖)、重定义(隐藏)?
- 多态的实现原理?
- inline函数可以是虚函数吗?答:不能,因为inline函数没有地址,无法把地址放到虚函数表中。
- 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式 无法访问虚函数表,所以静态成员函数无法放进虚函数表。
- 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始 化的。
- 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义 成虚函数。
- 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是 引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
- 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码 段(常量区)的。
- C++菱形继承的问题?虚继承的原理?注意这里不要把虚函数表和虚基表搞混了。
- 什么是抽象类?抽象类的作用?抽象类强制重写了虚函数,另外抽象类体现出 了接口继承关系。
没具体写清楚答案的,都在文章中