马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
目次
一、多态的概念
二、多态的界说和实现
1. 多态的构成条件
2. 虚函数
3.虚函数的重写
3.1 析构函数的重写
4. override 和 final (C++11)
5. 重载、重界说(潜伏)、重写(覆盖)的对比
三、抽象类
1. 概念
2. 接口继承和实现继承
四、多态原理
1. 再次分析多态条件
2. 虚函数表
3. 动态绑定与静态绑定
四、单继承与多继承的虚函数表
1. 单继承中的虚函数表
2. 多继承中的虚函数表
五、常晤口试题
一、多态的概念
多态,顾名思义,有多种形态。具体来说就是去完成某个活动,当差别的对象去完成时会产生出差别的状态
举个栗子:好比买票这个活动,当平凡人买票时,是全价买票;弟子买票时,是半价买票;武士买票时是优先买票。
二、多态的界说和实现
1. 多态的构成条件
多态是在差别继承关系的类对象,去调用同一函数,产生了差别的活动。好比Student继承了 Person 。 Person对象买票全价, Student 对象买票半价。 在继承中要构成多态尚有两个条件:
- 必须通过基类的指针大概引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数举行重写
2. 虚函数
虚函数界说:被virtual修饰的类成员函数被称为虚函数,全局的函数不能加。虚函数是为了重写而生,它的意义就是云云。
虚函数就是虚函数,和虚继承没关系,只是用了同一个关键字。别的我们还学了仿函数,这是重载()的operator()函数
- class Person {
- public:
- virtual void BuyTicket()
- {
- cout << "买票-全价" << endl;
- }
- };
复制代码 虚函数重写的一些细节:
- 派生类的重写虚函数可以不加virtual,由于基类写了,派生类继承后直接重写了。但是发起都加上virtual
- 协变——返回值可以差别,但是返回值必须是具有父子关系的指针或引用(而且必须同时是指针或引用,其他类的父子关系也可以)不常用
多态条件:
- 调用函数是重写的虚函数
- 必须通过基类的指针大概引用调用虚函数
- int main()
- {
- Person* p = new Person;
- delete p;
- p = new Student;
- delete p; //p->destructor() + operator delete(p)
- return 0;
- }
复制代码 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 ps;
- Student st;
- Func(ps);
- Func(st);
- return 0;
- }
复制代码 可以看到我们两次调用Func函数通报的参数分别为父类Person类的对象和子类Sudent类的对象,而在Func的形参处,我们利用Person类变量p吸收两参数,这就意味着在吸收st时会发生切片(上转型),但是编译器是怎样通过切出的父类的那一部门成员变量(成员函数不在类内部),而能调用子类Student的BuyTicket函数呢?当Func形参范例由引用改为平凡范例时为什么都打印全价呢?
这里就表现了多态的寄义,当差别的对象通报已往时,会调用差别的函数,即多态调用看的是指向的对象
对于第二个题目,是语法规定,我们必须要同时满足多态的两个条件:
- 调用函数是重写的虚函数 (假如父类没有virtual函数,纵然子类有虚函数也不能实现多态,但是假如父类有virtual而子类重写时不加virtual,这种情况是可以实现多态)
- 通过基类的指针大概引用调用虚函数(比方在Func的型参处,用引用吸收和平凡变量吸收效果差别)
以是当Func形参范例由引用改为平凡范例,不构成多态条件,那么形参是平凡对象,它就只看当前范例是Person对象,就调用Person的函数
虚函数重写的细节:
- 派生类的重写虚函数可以不加virtual,但是不发起如许利用
- 协变,返回值的范例可以差别,到当时要求返回值必须是父子关系指针或引用,必须同时是指针或引用,而且父子关系也不能交换(不常用)(下面的代码就是协变返回值差别的样例)
- class A
- {};
- class B : public A
- {};
- class Person {
- public:
- virtual A* BuyTicket() const {
- cout << "买票-全价" << endl;
- return 0;
- }
- };
- class Student : public Person {
- public:
- virtual B* BuyTicket() const {
- cout << "买票-半价" << endl;
- return 0;
- }
- };
复制代码 3.1 析构函数的重写
析构函数可以是虚函数吗?为什么须要是虚函数?
假如基类的析构函数为虚函数,此时派生类析构函数只要界说,无论是否加virtual关键字,都与基类的析构函数构成重写,固然基类与派生类析构函数名字差别。
固然函数名不类似,看起来违反了重写的规则,实在否则,由于编译器对析构函数的名称做了特殊处理处罚,编译后析构函数的名称同一处理处罚成destructor
析构函数加virtual,是不是虚函数重写?
是,由于类析构函数都被处理处罚成destructor这个同一的名字,而且没有返回值,形参类似,构成了重写条件。
为什么要这么处理处罚呢?
由于要让他们构成重写
为什么要让它们构成重写?
为了下面场景的需求,记取这个场景,这就是 destructor 的缘故因由
- class Person {
- public:
- ~Person()
- {
- cout << "~Person()" << endl;
- }
- };
- class Student : public Person {
- public:
- ~Student()
- {
- cout << "~Student()" << endl;
- delete[] ptr;
- }
- protected:
- int* ptr = new int[10];
- };
- int main()
- {
- Person* p = new Person;
- delete p;
- p = new Student;
- delete p; // p->destructor() + operator delete(p)
- return 0;
- }
复制代码 第二次p被赋值为Student实例化的对象,而开释p时调用的是Person的析构函数,但是我们要的应该是Student的析构函数,那么这就意味着new出的Student内部的10个int型数组内存走漏!
在学习delete时,我们解说了delete是分为两步举行的,第一步调用其地点类的析构函数,第二部调用operator delete函数开释空间,在delete时看的是delete反面的范例(平凡调用),delete p的第一步也就成为了Person类析构函数p->destructor()
由于上转型对象的影响,析构时就有了题目。我们所盼望的是p指向谁,就调用谁的析构函数,即p->destructor()是一个多态调用,而不是平凡调用,那么怎么办理这个题目?
将它们构成重写!但是重写须要三同,由于它们的析构函数名差别,以是析构函数都被编译器处理处罚成destructor这个同一的名字,加上virtual形成多态,终极构成重写
只要基类的析构加上virtual,由于子类的virtual是可以省略的,那么也就意味着编写者大概在不知情的情况下,在子类编写的析构已经构成了虚函数重写,这就会办理对于上转型对象析构时会有内存泄漏的情况,以是父类写virtual,子类可以省略virtual可以以为是为了此情况计划的。以是在大多数会被继承的基类的析构函数加上virtual是有很多长处的,但是也会付出一些代价
4. override 和 final (C++11)
final:修饰虚函数,表现该虚函数不能再被继承(情况少)
- class Car
- {
- public:
- virtual void Drive() final
- {}
- };
- class Benz :public Car
- {
- public:
- virtual void Drive()
- {
- cout << "Benz-舒适" << endl;
- 报错,因为基类Drive虚函数被final修饰,不能再被继承重写
- }
- };
复制代码 override:资助派生查抄是否完成重写,假如没有就会报错,它是暴力查抄,与纯虚函数差别,override是放在派生类中,纯虚函数是在基类中
- class Car
- {
- public:
- virtual void Drive()
- {}
- };
- class Benz :public Car
- {
- public:
- virtual void Drive() override
- {
- cout << "Benz-舒适" << endl;
- }
- };
复制代码 怎样计划一个不想被继承的类?
方法一:基类构造函数私有(C++98)
在C++98中,我们一样寻常将构造函数私有,那么该类就不能被继承了,这是由于派生类的构造函数必须要先调用基类的构造函数
同理,也可以将析构函数私有,同样也不能被继承,由于无法调用析构,以是在类外不能创建对象,但是可以new出来一个,如许开释就归我们管了
- class A
- {
- public:
- 用static变成静态成员函数,避免用对象调用函数,因为A类不能在外部实例化对象
- static A CreateObj()
- {
- 构造函数私有了,那么只能在这里实例化对象后返回
- return A();
- }
- private:
- A()
- {}
- };
- class B : public A
- {
- };
复制代码 方法二:加final(C++11)
- class A final
- {
- public:
- private:
- };
- 错误,不能用被final修饰的类作为基类
- class B : public A
- {};
复制代码
5. 重载、重界说(潜伏)、重写(覆盖)的对比
三、抽象类
1. 概念
在虚函数的反面写上 =0 ,则这个函数为纯虚函数。包罗纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才华实例化出对象。纯虚函数规范了派生类必须重写,别的纯虚函数更表现出了接口继承。但是可以界说指针,Car* p;
2. 接口继承和实现继承
- class Car
- {
- public:
- inline virtual void Drive() = 0;
- //int Func();
- };
- class Benz :public Car
- {
- public:
- inline virtual void Drive()
- {
- cout << "Benz-舒适" << endl;
- }
- };
- class BMW :public Car
- {
- public:
- inline virtual void Drive()
- {
- cout << "BMW-操控" << endl;
- }
- };
- class BYD :public Car
- {
- public:
- inline virtual void Drive()
- {
- cout << "BYD-build your dream" << endl;
- }
- };
- void Func(Car* p)
- {
- p->Drive();
- }
- int main()
- {
- Func(new Benz);
- Func(new BMW);
- Func(new BYD);
- return 0;
- }
复制代码 平凡函数的继承是一种实现继承,派生类继承了基类函数,可以利用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,告竣多态,继承的是接口。以是假如不实现多态,不要把函数界说成虚函数。
四、多态原理
1. 再次分析多态条件
多态的条件为什么不能是子类指针或引用?
由于只有父类可以即可指向子类又可指向父类(切片)而子类不可,子类只能指向子类,只有上转型没有下转型
那么多态条件能不能是父类对象呢?
不能!由于对象的切片和指针/引用的切片是差别的!子类的虚函数表是先拷贝父类的虚函数表,假如有虚函数重写,那么就将新的地点覆盖原先拷贝的父类的虚函数地点
- class Person {
- public:
- virtual void BuyTicket() { cout << "买票-全价" << endl; }
- virtual void Func1()
- {
- cout << "Person::Func1()" << endl;
- }
- virtual void Func2()
- {
- cout << "Person::Func2()" << endl;
- }
- //protected:
- int _a = 0;
- };
- class Student : public Person {
- public:
- virtual void BuyTicket() { cout << "买票-半价" << endl; }
- private:
- virtual void Func3()
- {
- //_b++;
- cout << "Student::Func3()" << endl;
- }
- protected:
- int _b = 1;
- };
复制代码
指针和引用是不存在拷贝题目的,父类指针指向父类就检察父类的虚函数表,指向子类就检察子类当中属于父类的那一部门数据,再检察子类的虚函数表。
而假如利用对象就不可了,起首子类的虚函数表不会被拷贝进父类对象中,由于假如连虚函数表都拷贝了,那么当我们利用父类指针指向该父类对象时,调用虚函数,将会调用子类的虚函数!现在酿成父类指针不管指向父类还是子类都是调用子类虚函数,这就乱套了!
结论:子类对象赋值给父类对象切片,不会拷贝虚函数表。假如拷贝虚表,那么父类虚表中时父类函数还是子类对象就不确定了。
为什么子类要重写父类的虚函数?
由于只有子类重写了父类的虚函数,子类拷贝父类的虚函数表中,子类才华修改虚函数的地点,从而到达父类指针/引用指向父类调父类虚函数,指向子类调子类虚函数
平凡的继承都是实现继承,虚函数继承是接口继承
2. 虚函数表
这里常考一道笔试题:sizeof(Base)是多少? - class Base
- {
- public:
- virtual void Func1()
- {
- cout << "Func1()" << endl;
- }
- private:
- int _b = 1;
- };
复制代码 通过观察测试我们发现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. 派生类自己新增长的虚函数按其在派生类中的声明序次增长到派生类虚表的末了。
奇妙验证:
- class Person {
- public:
- virtual void BuyTicket() { cout << "买票-全价" << endl; }
- virtual void Func1()
- {
- cout << "Person::Func1()" << endl;
- }
- virtual void Func2()
- {
- cout << "Person::Func2()" << endl;
- }
- //protected:
- int _a = 0;
- };
- class Student : public Person {
- public:
- virtual void BuyTicket() { cout << "买票-半价" << endl; }
- private:
- virtual void Func3()
- {
- //_b++;
- cout << "Student::Func3()" << endl;
- }
- protected:
- int _b = 1;
- };typedef void(*FUNC_PTR) ();// 打印函数指针数组
- // void PrintVFT(FUNC_PTR table[])
- void PrintVFT(FUNC_PTR* table)
- {
- for (size_t i = 0; table[i] != nullptr; i++)
- {
- printf("[%d]:%p->", i, table[i]);
- FUNC_PTR f = table[i];
- f();
- }
- printf("\n");
- }
- int main()
- {
- Person ps;
- Student st;
- 强转,取出类的前四个字节,就是虚表的地址,该地址是一个函数指针数组
- int vft1 = *((int*)&ps);
- PrintVFT((FUNC_PTR*)vft1);
- int vft2 = *((int*)&st);
- PrintVFT((FUNC_PTR*)vft2);
- return 0;
- }
复制代码 这里尚有一个童鞋们很轻易肴杂的题目:虚函数存在哪?
虚函数存在虚表,虚表存在对象中。留意上面的答复的错的。但是很多童鞋都是如许深以为然的。留意虚表存的是虚函数指针,不是虚函数,虚函数和平凡函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。别的对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的,Linux g++下各人自己去验证
3. 动态绑定与静态绑定
- //父类
- class Person
- {
- public:
- virtual void BuyTicket()
- {
- cout << "买票-全价" << endl;
- }
- };
- //子类
- class Student : public Person
- {
- public:
- virtual void BuyTicket()
- {
- cout << "买票-半价" << endl;
- }
- };
- int main()
- {
- //不构成多态
- Student Johnson;
- Person p = Johnson;
- p.BuyTicket();
- //构成多态
- Student Johnson;
- Person& p = Johnson;
- p.BuyTicket();
-
- return 0;
- }
复制代码
- 静态绑定又称为前期绑定(早绑定),在步伐编译期间确定了步伐的活动,也称为静态多态,好比:函数重载,也就是直接call函数地点
- 动态绑定又称后期绑定(晚绑定),是在步伐运行期间,根据具体拿到的范例确定步伐的具体活动,调用具体的函数,也称为动态多态。也就是先到指定对象的虚表中找到要调用的虚函数,然后才华举行函数的待用
- 买票的汇编代码很好的表明确什么是静态(编译器)绑定和动态(运行时)绑定
四、单继承与多继承的虚函数表
1. 单继承中的虚函数表
- 先将基类中的虚表内容拷贝一份到派生类虚表中
- 假如派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
- 派生类自己新增长的虚函数按其在派生类中的声明序次增长到派生类虚表的末了。
在多态原理部门已经表明。假如在虚函数表中查找不到派生类自己的虚函数,可以试试强转获取虚函数表的地点,再举行输出打印(大概会出现权限题目)
- // 打印函数指针数组
- // void PrintVFT(FUNC_PTR table[])
- void PrintVFT(FUNC_PTR* table)
- {
- for (size_t i = 0; table[i] != nullptr; i++)
- {
- printf("[%d]:%p->", i, table[i]);
- FUNC_PTR f = table[i];
- f();
- }
- printf("\n");
- }
- int main()
- {
- Person ps;
- Student st;
- 强转,取出类的前四个字节,就是虚表的地址,该地址是一个函数指针数组
- int vft1 = *((int*)&ps);
- PrintVFT((FUNC_PTR*)vft1);
- int vft2 = *((int*)&st);
- PrintVFT((FUNC_PTR*)vft2);
- return 0;
- }
复制代码 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;
- };
- int main()
- {
- Derive d;
- Base1* ptr1 = &d;
- ptr1->func1();
- Base2* ptr2 = &d;
- ptr2->func1();
- Derive* ptr3 = &d;
- ptr3->func1();
- return 0;
- }
复制代码
继承两个基类,就会有两个虚函数表,即继承几个类,子类就有几个虚函数表
通过内存监督可以看到,derive类多继承base1和base2类之后,内存巨细为20字节,这是由于base1类的sizeof包罗了4字节的虚表指针(32位)和4字节的int型成员变量,同理base2类也是,再加上derive类自己的int型变量,总计20字节。这表明,在子类的虚函数表这方面, 它指挥继承父类的虚函数表,而不会自己单独创建一个而虚函数表!
但是derive子类自身的虚函数会放在那边呢?
子类的虚函数被放在了base1的虚函数表内
为什么同样都是被子类derive重写了func1函数,在利用base1指针和base2指针分别指向derive类对象的时间,调用的也是同样的函数,为什么base1和base2的func1函数地点不一样呢?
是编译器为了处理处罚this指针做的一个封装,实际上假如将Derive类对象上转为Base1或Base2多态调用func1时,地点都是一样的,都是子类Derive重写的func1的地点,只是由于多继承中,由于基类继承序次差别,第一个之后继承的基类会被夹在子类中(比方Base2),以是上转时Base2吸收的地点会被默认处理处罚为 子类地点 + sizeof(Base1),又由于调用子类的成员函数须要子类的this指针,以是对于夹在中心的基类,多态调用子类重写的虚函数须要修改this的地点,以是上面的征象就是编译器为了修改this做出的活动。
答案是一个为真,一个为假! 多态调用子类重写的虚函数func1或子类自己调用自身的func1函数,它们调用的都是同一个函数,那就是子类的func1函数。从汇编指令可以看出,在利用多态调用时,编译器先传ecx(this指针),再call虚函数的地点。
为什么base2的func1地点要举行修改,而base1的func1函数地点不须要修改呢?
这是由于它们调用的都是子类derive重写的虚函数func1,要利用子类的函数就须要子类的this指针,假如不利用子类的this指针,那么假如在子类重写的虚函数中利用了子类的成员信息,如许就会堕落。而base1是先继承的类,它的地点恰好就是derive类的首地点,它们类似!以是base1直接就call对了,base2是后继承的类,它被夹在了中心,以是它假如像转为子类的this指针,就须要sub 8字节,修正this的位置,转为子类的this指针,从而调用子类重写的虚函数
五、常晤口试题
下面的步伐将会输出什么?
- 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;
- }
复制代码 起首,p调用test函数,在编译视角看,test() 在B类内部找不到,以是向上找到父类A,A内部有test函数,完备链接,此时test函数内部调用func函数,那么题目来了,是谁调的func函数?
我们要明确一个条件,派生类继承时,只继继承拷贝了父类的成员变量,在内存对齐后再加上自己的成员变量,如许就构成了派生类的成员变量,而基类的成员函数派生类没有专门向自己类内部拷贝一份!由于成员函数都是放在代码段的!以是,test函数内部,this->func() 这里的this还是A*,B*切片为A*
其次,我们再来分析是否满足多态条件,此时已经满足了第一个条件:利用父类指针调用,那么此时的func函数构成重写吗?
我们大概存疑的地方就是形参的缺省值差别,但是重写要求的条件是参数类似,这里的类似表现的是范例、数量、序次类似,以是这里的缺省值不影响,此时就构成了多态的条件
末了,由于p指向的是B类范例,以是多态调用的是B类的func函数,那么答案显而易见:B->0,但是很遗憾,答案错误
这里又有一个重点,重写的潜伏细节,为什么父类加了virtual后子类就可以省略呢?这是由于重写是重写的函数内部,它会复用基类的函数返回值、参数列表,以是此时多态调用的func函数利用的是A类的 virtual void func (int val = 1) 的壳子,以是精确答案是B->1
1. 什么是多态?
多态是指差别继承关系的类对象,去调用同一函数,产生了差别的活动。多态又分为静态的多态和动态的多态。更方便、机动的多种形态的调用。
2. 什么是重载、重写(覆盖)、重界说(潜伏)?
3. 多态的实现原理?
- 静态多态(重载):函数名修饰规则
- 动态多态:虚函数表
4. inline函数可以是虚函数吗?
答:不能,由于inline函数没有地点,无法把地点放到虚函数表中。但是实际实行时是可以的,由于编译器会直接忽略掉inline的内联属性,这个函数就不再是inline函数
留意:在类内部直接界说成员函数会被默认带有inline属性
5. 静态成员可以是虚函数吗?
答:不能,由于静态成员函数没有this指针,利用范例::成员函数的调用方式,无法访问虚函数表,以是静态成员函数无法放进虚函数表。
静态成员函数不依赖于对象实例,也不到场多态性,因此不能是虚函数
6. 构造函数可以是虚函数吗?
不能,由于虚表是在编译的时间天生的,对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
抵牾点:虚函数的调用依赖虚表,但虚表的初始化由构造函数完成。
- 若构造函数是虚函数,调用它时须要依赖虚表查找函数地点,但此时虚表尚未初始化(vptr未精确指向)。
逻辑悖论:虚表尚未构建 → 无法找到假造造函数的地点 → 无法调用构造函数 → 对象无法构造。
语言计划限定:
- 对象构造过程必须静态确定范例。编译器需明确知道调用哪个构造函数来分配内存和初始化对象。
- 若答应假造造函数,则需在运行时动态确定构造函数,导致无法预知对象巨细和内存布局(与C++静态范例体系辩说)。
焦点结论
- 子类虚表中覆盖父类虚函数地点的操纵在编译期完成,即一个类的虚表是在编译期间天生的,一个类的对象共享同一个虚表,虚表存放在代码段。
- 子类对象的vptr在运行期初始化为编译期天生的虚表地点。
具体分析
1. 虚表的天生与覆盖(编译期)
- 虚表的静态天生:
全部类的虚表(包罗基类和派生类)在编译期由编译器天生。
- 若子类重写(override)父类虚函数,编译器会直接在子类虚表中更换对应条目为子类函数的地点。
- 比方:
- class Base
- {
- virtual void foo();
- };
- class Derived : public Base
- {
- void foo() override;
- };
复制代码
- Base虚表条目:&Base::foo
- Derived虚表条目:&Derived::foo(编译期直接覆盖)
- 虚表内容固化:
虚表本质是编译器天生的静态数据(存储在步伐只读内存段,如.rodata),其内容在编译后不再改变。
2. vptr的初始化(运行期)
- 运行期活动:
- 每个对象的vptr是对象的潜伏成员,其初始化由构造函数在运行期完成。
- 构造对象时,构造函数隐式插入代码,将vptr赋值为当前类的虚表地点(编译期已确定该地点)。
- 构造过程示例:
当创建Derived对象时:
- 调用Base构造函数(编译器插入代码,将vptr临时指向Base的虚表)。
- 实行Base构造函数体。
- 调用Derived构造函数(将vptr重新指向Derived的虚表)。
- 实行Derived构造函数体。
终极,对象的vptr指向Derived的虚表。
- 关键点:
- 虚表地点是编译期已知常量,但vptr的赋值是运行期实行的。
- 多态的动态绑定通过vptr间接访问虚表实现,而虚表自己是静态数据
验证示例
- #include <iostream>
- class Base
- {
- public:
- virtual void foo()
- {
- std::cout << "Base::foo\n";
- }
- };
- class Derived : public Base
- {
- public:
- void foo() override
- {
- std::cout << "Derived::foo\n";
- }
- };
- int main()
- {
- Derived d;
- Base* p = &d;
- p->foo(); // 输出 "Derived::foo"
- }
复制代码
- 编译期活动:
- 编译器为Base天生虚表[&Base::foo]。
- 编译器为Derived天生虚表[&Derived::foo](覆盖基类条目)。
- 运行期活动:
- 构造d时,Derived构造函数将d的vptr指向Derived的虚表。
- 通过Base*调用foo()时,通过vptr找到Derived的虚表,终极调用Derived::foo。
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
可以,而且最好把基类的析构函数界说成虚函数。由于多态调用析构函数时,分析对象已经创建,也就表明虚表指针已经被初始化,可以根据虚表找到虚析构函数,从而动态的举行多态调用。参考本节内容
8. 对象访问平凡函数快还是虚函数更快?
起首假如是平凡对象,是一样快的。假如是指针对象大概是引用对象,则调用的平凡函数快,由于构成多态,运行时调用虚函数须要到虚函数表中去查找。
9. 虚函数表是在什么阶段天生的,存在哪的?
虚函数表是在编译阶段就天生的,一样寻常情况下存在代码段(常量区)的。
10. C++菱形继承的题目?虚继承的原理?
答:数据冗余和二义性,虚基表。详情参考继承文章。
<C++> 继承-CSDN博客
留意:这里不要把虚函数表和虚基表搞混了。
11. 什么是抽象类?抽象类的作用?
抽象类逼迫重写了虚函数,别的抽象类表现出了接口继承关系。抽象类很好的表现了虚函数的继承是一种接口继承,逼迫子类去抽象纯虚函数,由于子类如果不抽象从父类继承下来的纯虚函数,那么子类也是抽象类也不能实例化出对象。其次,抽象类可以很好的去表现实际天下中没有示例对象对应的抽象范例
|