文章目录
- 1、多态的概念
- 2、多态的定义及实现
- 2-1、多态的构成条件
- 2-2、虚函数
- 2-3、虚函数的重写
- 2-4 多态样例
- 2-5、协变
- 2-6、 析构函数与virtual
- 2-7、函数重载、函数隐藏(重定义)与虚函数重写(覆盖)的对比
- 2-8、override 和 final(C++11提供)
- 3、抽象类
- 3-1、概念
- 3-2、接口继承和实现继承
- 4、多态的原理
- 4-1、虚函数表(虚表)
- 4-2、多态的原理
- 4-3、动态绑定与静态绑定
- 5、单继承和多继承关系的虚函数表
- 5-1、单继承中的虚函数表
- 5-2、多继承中的虚函数表
- 5-3、菱形继承、菱形虚拟继承
- 6、总结(重点)
1、多态的概念
多态的概念:
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
举例:
比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
再举个例子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块…,而有人扫的红包都是1毛,5毛…。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 = random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为
注:以上支付宝红包问题等例子纯属瞎编,大家仅供娱
2、多态的定义及实现
2-1、多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(下面都会讲到,先记住这个结论)
2-2、虚函数
虚函数
:即被virtual修饰的类成员函数称为虚函数。 这里的虚函数和前面的虚拟继承没有任何关系,只不过是同时用了virtual关键字。虚拟继承是为了解决数据冗余和二义性的!
class Person
{
public:
virtual void BuyTicket() //这里的函数就是虚函数
{
cout << "买票-全价" << endl;
}
};
2-3、虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
虚函数的重写又叫做虚函数的覆盖;
函数的隐藏又叫做函数的重定义:基类与派生类的成员函数名称相同,那么基类的成员函数在派生类中被隐藏
简单理解为:重写(覆盖)比隐藏(重定义)更加复杂——关键字virtual和三个相同
三个相同:
1、函数名相同
2、函数参数的类型和数量相同
2、函数返回值类型相同
2-4 多态样例
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;
}
};
//多态条件
//1、虚函数重写
//2、由父类/基类指针或者引用调用虚函数
void Func(Person& p){//父类的引用调用虚函数
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Soldier sd;
Func(ps);
Func(st);
Func(sd);
return 0;
}
//void Func(Person* p) {
// p->BuyTicket();
//}
//
//int main()
//{
// Person* ps = new Person;
// Student* st = new Student;
// Soldier* sd = new Soldier;
//
// Func(ps);
// Func(st);
// Func(sd);
// return 0;
//}
上面就是多态的调用。
传入参数的类型是派生类类型,而函数参数是基类类型的引用。即便函数参数类型是父类的引用/指针,但是本质上参数引用/指针,引用的对象/指向的对象是派生类中,包含父类成员的那一部分(切片/切割)
基类指针/引用调用虚函数:指针/引用的内容是子类中父类的那一部分!
得出结论:
1、普通调用:跟对象调用类型有关(也就是将子类中包含父类的成员,切割拷贝给父类)
2、多态调用:跟基类指针/引用,指向的对象/引用的对象,的类型有关(子类包含的父类成员,不用给父类,直接由父类的指针/引用指向)
不满足多态条件的情况:
1、不是由父类/基类的指针/引用对虚函数的调用:
这里就是把子类中包含父类的成员切割/切片给给父类了
2、没有形成虚函数重写
情况一:父类和子类都不是虚函数
情况二:父类不是虚函数,子类是虚函数
情况三:父类是虚函数,子类不是虚函数
这里的情况三居然能够调用正确,这是为什么呢?
我们可以理解为:派生类继承了父类的虚函数,所以,派生类中继承下来的函数就是具有虚函数属性的。然后又对函数体重写,这样就构成了虚函数重写了
虚函数重写的本质就是:对函数体进行重写,函数体也就是函数的实现!!!
因此,子类的虚函数可以不加virtual关键字,但是父类必须加上virtual关键字(这里推荐都加上virtual)
那么,出了以上的情况之外,还有什么特殊情况也是构成多态的呢?
特例——协变
2-5、协变
上面的三个相同条件中,返回值可以不同,但是返回值必须是一个父子类关系的指针/引用
甚至下面情况也可以:
那么,到现在为止,除了上面两个特殊情况以外,再不满足多态的条件就构成不了多态了!!!
2-6、 析构函数与virtual
对于普普通调用析构函数而言:
class Person
{
public:
//virtual ~Person()
~Person()
{
cout << "Person delete:" << _p << endl;
delete[] _p;
}
protected:
int* _p = new int[10];
};
class Student : public Person
{
public:
~Student()//一般情况下,子类析构结束,会自动调用父类的析构
{
cout << "Student delete:" << _s << endl;
delete[] _s;
}
protected:
int* _s = new int[20];
};
int main()
{
Person p;
Student s;
return 0;
}
确实,上面调用的确析构函数是不是虚函数无所谓,都是调用正确的
但是,如果调用变成下面情况了呢?
int main()
{
//Person p;
//Student s;
Person* ptr1 = new Person;
Person* ptr2 = new Student;
delete ptr1;
delete ptr2;
return 0;
}
delete的具体行为:
1、使用指针调用析构函数
2、operator delete(指针)
调用方式:
1、普通调用:跟对象调用类型有关
2、多态调用:跟基类指针/引用,指向的对象/引用的对象,的类型有关
所以,原因就是:
那么,如果是多态调用是不是就避免内存泄漏了呢?
我们在析构函数前面加上virtual关键字,变成虚函数;
析构函数没有参数和返回值,最后只剩下一个函数名了,这里的析构函数名我们看着好像不相同,其实是相同的:
析构函数的函数名会被自动处理成为
destructor
最终,通过多态调用,我们避免了内存泄漏;也了解了为什么析构函数的函数名都被转换成了destructor
,不这样做就发生了内存泄漏了
所以,析构函数无脑加上virtual关键字就行了
注意:
构造函数不能无脑加virtual,因为虚函数的运行是建立在对象之上的,我们把构造函数变成虚函数,在执行构造函数时,对象都没有生成。所以构造函数不能加上virtual关键字
2-7、函数重载、函数隐藏(重定义)与虚函数重写(覆盖)的对比
2-8、override 和 final(C++11提供)
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写
1、 final:修饰虚函数,表示该虚函数不能再被重写
class A final//c11之后的方法——final关键字
{
//private://c11之前的方法——构造函数私有
// A()
// {}
public:
A()
{}
};
class B : public A
{
};
int main()
{
B bb;
B* ptr = new B;
return 0;
}
两种方法使基类不能被继承:
1、构造函数私有
2、定义类后面加上final关键字
2.、override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
3、抽象类
3-1、概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
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();
}
int main()
{
Test();
return 0;
}
异常现象:
当不需要基类生成对象的时候,可以把基类写成抽象类!
3-2、接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
所以如果不实现多态,不要把函数定义成虚函数
例题1:
class A
{
public:
virtual void func(int val = 1)
{
std::cout << "A->" << val << std::endl;
}
virtual void test()
{
func();
}
};
class B : public A
{
public:
void func(int val = 0)
{
std::cout << "B->" << val << std::endl;
}
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
//选什么?
//A: A->0 B : B->1 C : A->1 D : B->0 E : 编译出错 F : 以上都不正确
p的类型是B*,当我们执行p->test()时,会使用继承下来的test()函数,但是test函数来自于A,而对于指针/引用来说都不会发生强转,所以B原封不动的继承了A的test()函数,但是B继承的test()函数中的this指针仍然是A*。也就是说func()函数的调用this->func()中的this是A*。这个时候,A是父类的指针,func()函数构成了虚函数重写,所以,A->func()就是一个多态调用,先打印B->,这个时候重点又来了,虚函数重写,重写的只是函数体,函数接口没有被重写!所以,A*->func()用着A中的函数接口,调用B中的函数体,也就是函数实现!
例题2:
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main() {
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
//选什么? A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
这里的p3和p1虽然相同但是意义不一样。p3是整体,而p1是局部
4、多态的原理
4-1、虚函数表(虚表)
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
cout << sizeof(Base) << endl;
return 0;
}
我们前面学过,如果知道类的成员变量,采用内存对齐就能够算出类的大小,但是这里真的和我们想的一样吗?
为什么是8呢?
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr
放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析:
// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
可以看到,子类继承父类的虚函数之后,发生了虚函数重写,那么子类的虚表继承下来的虚函数地址就不是原来继承下来的地址,而是由重写的虚函数进行覆盖,变成了一个新的虚函数
通过观察和测试,我们发现了以下几点问题:
- 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
- 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表 中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数 的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
- 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函 数,所以不会放进虚表。
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
- 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生 类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
- 这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在 虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是 他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的,Linux g++下大家自己去验证?
所以,__vfptr是一个指针,全名为虚函数表指针(虚表指针),指向虚函数表(虚表),虚函数表里面存放的就是我们的虚函数地址!
我们来看看虚表是不是存放在代码段的:
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
private:
int _b = 1;
char _ch;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
void Func2()
{
cout << "Derive::Func2()" << endl;
}
private:
int _d = 2;
};
int main()
{
int a = 0;
cout << "栈:" << &a << endl;
int* p1 = new int;
cout << "堆:" << p1 << endl;
const char* str = "hello world";
cout << "代码段/常量区:" << (void*)str << endl;
static int b = 0;
cout << "静态区/数据段:" << &b << endl;
Base be;
cout << "虚表:" << (void*)*((int*)&be) << endl;//拿到前面4个字节地址
Base* ptr1 = &be;
int* ptr2 = (int*)ptr1;
printf("虚表:%p\n", *ptr2);
Derive de;
cout << "虚表:" << (void*)*((int*)&de) << endl;
Base b1;
Base b2;
return 0;
}
对上面代码进行改进:
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
char _ch;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
void Func3()
{
cout << "Derive::Func3()" << endl;
}
private:
int _d = 2;
};
int main()
{
cout << sizeof(Base) << endl;
Base b;
Derive d;
// 普通调用 -- 编译时/静态 绑定
Base* ptr = &b;
ptr->Func3();
ptr = &d;
ptr->Func3();
// 多态调用 -- 运行时/动态 绑定
ptr = &b;
ptr->Func1();
ptr = &d;
ptr->Func1();
return 0;
}
普通调用在编译的时候,通过类型就能够锁定函数是谁,直接call该函数地址,进行调用
多态调用确定不了,因为多态调用不确定函数是调用父类的还是子类的,虽然看到的都是一个父类的对象,但是存在两种情况:
1、父类对象;2、子类中父类的那一部分 无论是什么情况,多态调用都是通过指向的对象的内部取虚表指针,再到虚表里面找到对应的函数进行调用(指向谁,调用谁)
简单来说就是:编译器将子类虚表中重写的虚函数地址覆盖完成之后,再一次性把所有虚表放到代码段/常量区当中!
同一个类的对象共享同一个虚表!
4-2、多态的原理
上面分析了这个半天了那么多态的原理到底是什么?还记得这里Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket
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;
}
- 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚 函数是Person::BuyTicket。
- 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中 找到虚函数是Student::BuyTicket。
- 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
- 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调 用虚函数。反思一下为什么?
- 再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行 起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的
void Func(Person* p)
{
p->BuyTicket();
}
int main()
{
Person mike;
Func(&mike);
mike.BuyTicket();
return 0;
}
// 以下汇编代码中跟你这个问题不相关的都被去掉了
void Func(Person* p)
{
...
p->BuyTicket();
// p中存的是mike对象的指针,将p移动到eax中
001940DE mov eax,dword ptr [p]
// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
001940E1 mov edx,dword ptr [eax]
// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
00B823EE mov eax,dword ptr [edx]
// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的。
001940EA call eax
00头1940EC cmp esi,esp
}
int main()
{
...
// 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址
mike.BuyTicket();
00195182 lea ecx,[mike]
00195185 call Person::BuyTicket (01914F6h)
...
}
4-3、动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。
5、单继承和多继承关系的虚函数表
需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,没什么需要特别研究的
5-1、单继承中的虚函数表
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; }
void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
int main()
{
Base b;
Derive d;
return 0;
}
我们可以看到子类的func1函数进行了重写,func2函数没有进行重写,但是子类的func3函数也是虚函数啊,怎么子类虚表里面没有func3函数的地址呢?
这是因为vs的监视窗口对原代码进行了处理,我们监视窗口看到的并不是原生的内容
我们通过内存窗口来看看:
linux下虚表不是以空结束!
我们直接来打印出这两个虚表指针:
typedef void(*func_t)();
void Print(func_t f[])
{
for (int i = 0; f[i] != nullptr; ++i)
{
printf("[%d]:%p\n", i, f[i]);
f[i]();
}
cout << endl;
}
int main()
{
Base b;
//Print((func_t*)(*(int*)&b));//int*在x86下面才是4个字节,x64下面是8个字节
//Print((func_t*)(*(long long*)&b));
Print((func_t*)(*(void**)&b));//指针都是4个字节大小,void*不能解引用,void**可以,*(void**)一样是看4个字节
Print((func_t*)(*(long long**)&b));//*(long long**)一样是看4个字节
Derive d;
//Print((func_t*)(*(int*)&d));
//Print((func_t*)(*(long long*)&d));
Print((func_t*)(*(void**)&b));
Print((func_t*)(*(long long**)&b));
return 0;
}
所有的虚函数都是会进虚表的!
单继承中,派生类自己的虚函数在虚表中继承下来的基类虚函数的后面
5-2、多继承中的虚函数表
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(*func_t)();
void Print(func_t f[])
{
for (int i = 0; f[i] != nullptr; ++i)
{
printf("[%d]:%p\n", i, f[i]);
f[i]();
}
cout << endl;
}
int main()
{
Base1 b1;
Base2 b2;
Print((func_t*)(*(void**)&b1));
Print((func_t*)(*(void**)&b2));
Derive d;
return 0;
}
也就是放到第一个继承的类里面
接下来打印虚表地址:
int main()
{
Base1 b1;
Base2 b2;
//Print((func_t*)(*(void**)&b1));
//Print((func_t*)(*(void**)&b2));
Derive d;
Print((func_t*)(*(void**)&d));//第一张虚表
Print((func_t*)(*(void**)((char*)&d + sizeof(Base1))));//char*不能少,不然一次加Derive大小
Base2* p = &d;
Print((func_t*)(*(void**)p));//自动偏移,找到子类中,Base2类的一部分
return 0;
}
所以,多继承中,如果子类有未重写的虚函数,会放在第一个继承的父类的虚表中!
5-3、菱形继承、菱形虚拟继承
这个就不多讲了吧,实际中菱形继承本来用的就是,更何况是菱形虚拟继承
C++虚函数表解析
C++对象的内存布局
想深入了解可以观看这两篇文章
6、总结(重点)
- 什么是多态?
多态分为静态多态和动态多态。
静态多态是在编译时绑定(比如说:函数重载,根据函数名修饰规则等等可以直接确定调用函数);
动态多态是运行时绑定,通过虚函数重写之后,父类的指针/引用来调用重写的虚函数,指向父类调用父类,指向子类调用子类(与指针/引用的类型无关,与指向的对象类型有关)。通过虚表指针找到代码段/常量区对应的虚表中的函数地址,拿到虚函数之后调用!
- 什么是重载、重写(覆盖)、重定义(隐藏)?
函数重载:两个函数在同一个作用域内,并且函数名,参数(参数个数,参数类型,参数类型的顺序)相同
虚函数重写(覆盖):两个函数分别在基类(父类)和派生类(子类)的作用域中,都是虚函数(有virtual关键字修饰),并且两个函数的函数名、参数、返回值都相同(协变除外)
函数的重定义(隐藏):两个函数分别在基类(父类)和派生类(子类)的作用域中,函数名相同就构成函数的重定义。并且在基类和子类的两个同名函数不构成重写就构成重定义
- 多态的实现原理?
一个接口,多种方法
- 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。
- 存在虚函数的类都有一个一维的虚函数表,简称为虚表。当类中声明虚函数时,编译器会在类中生成一个虚函数表。
- 类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
- 虚函数表是一个存储类成员函数指针的数据结构——函数指针数组结构。
- 虚函数表是由编译器自动生成与维护的(这也说明了虚函数的重写/覆盖是编译器帮助我们完成的)。
- virtual成员函数会被编译器放入虚函数表中。
- 当存在虚函数时,每个对象中都有一个指向虚函数的指针(C++编译器给父类对象,子类对象提前布局vptr指针),当进行test(parent *base)函数的时候,C++编译器不需要区分子类或者父类对象,只需要再base指针中,找到vptr指针即可)。
- vptr一般作为类对象的第一个成员。
注:vs的监视窗口vptr指针指向的虚表一般只存放两个虚函数地址,分别是是vptr[0]和vptr[1],这是编译器自主处理的结果,我们要通过内存窗口观察
- inline函数可以是虚函数吗?
对于多态调用而言:
1、语法层面(理论上):理论上来说是不可以的,inline是函数在类中展开,将代码保存在了类里面,但是这就与虚函数相违背了;一个是编译时就将函数展开;一个是在运行时通过虚表指针,找到虚表,拿到里面虚函数的地址,最后再调用虚函数;两种情况不可能同时存在。
2、实际操作:实际操作我们会发生,编译器(vs系列)只是发生警告,并不会报错,并且还能够正确编译。因为编译器在遇到inline和virtual两个关键字的时候,自动的忽略了inline属性,使得inline失效
但对于普通调用而言:
是可以的,普通调用会继续保存inline属性,因为普通调用没有虚表指针,虚表这些东西
- 静态成员可以是虚函数吗?
不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数的地址无法放进虚函数表。
静态成员函数不属于类中的任何一个对象和实例,属于类共有的一个函数。也就是说,它不能用this指针来访问,因为this指针指向的是每一个对象和实例
对于virtual虚函数,它的调用恰恰使用this指针。在有虚函数的类实例中,this指针调用vptr指针,指向的是vtable(虚函数列表),通过虚函数列表找到需要调用的虚函数的地址。总体来说虚函数的调用关系是:this指针->vptr(4字节)->vtable ->virtual虚函数。
所以说,static静态函数没有this指针,也就无法找到虚函数了。所以静态成员函数不能是虚函数。他们的关键区别就是this指针。
- 构造函数可以是虚函数吗?
不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。我们把构造函数变成虚函数放到虚表之后,虚表指针无法得到初始化,这样虚表指针和虚表就断开连接了
- 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
可以,并且最好把基类的析构函数定义成虚函数。
场景:Person* ptr2 = new Student;
这种父类指针指向子类的时候,就需要将析构函数定义为虚函数
- 对象访问普通函数快还是虚函数更快?
首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。
- 虚函数表是在什么阶段生成的,存在哪的?
虚函数表是在编译阶段就生成的,一般情况 下存在代码段(常量区)的。(虚表指针的初始化是在构造函数初始化列表阶段完成初始化的)
- C++菱形继承的问题?虚继承的原理?
1、数据冗余:虚基类的成员在派生类中会保存两份,这样多保存就产生了数据冗余
2、二义性:虚基类的成员在派生类中会保存两份,在调用的时候如果不指明基类就会产生二义性,从而报错
注意这里不要把虚函数表和虚基表搞混了:
虚函数表:里面存放的是虚函数的地址,用来构建多态
虚基表:存放着类的偏移量,为了防止数据冗余和二义性
- 什么是抽象类?抽象类的作用?
抽象类:在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)
抽象类作用:抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
抽象类强制重写了虚函数,另外抽 象类体现出了接口继承关系