1. 多态的概念
1.1 概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
2. 多态的定义及实现
静态多态与动态多态
静态多态:函数重载(编译的时候决定了调用哪一个函数)。
动态多态:下面写的(运行的时候决定了调用哪一个函数)。
2.1多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
2.2 虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
class Person { public:Person(const string& name):_name(name){}virtual void BuyTicket()//虚函数{cout << _name << " Person买票 -- 全价-¥100 " << endl;} protected:string _name; };
2.3虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表(参数只看类型不看缺省值)完全相同),称子类的虚函数重写了基类的虚函数。
class Student : public Person
{
public:Student(const string& name):Person(name){}virtual void BuyTicket(){cout << _name <<" Student买票 -- 半价-¥50 " << endl;}
};
class Solider : public Person
{
public:Solider(const string& name):Person(name){}virtual void BuyTicket(){cout << _name <<" Solider买票:优先买预留票 -- 全价-¥100 " << endl;}
};
注意:当父类写了虚函数时,子类继承下来的函数也将是虚函数,无论子类在重写时是否在函数返回值前面加上了virtual。例如:
class Person { public:virtual void f() { cout << "virtual void f() " << endl;} }; class Student : public Person { public:void f() { cout << "virtual void f() " << endl;} };
子类中的f函数的前面虽然没有写virtual关键字,但是子类的f函数也是虚函数,为什么?
因为子类先继承了父类的函数接口声明,然后子类对f函数重写了实现,就相当于子类从父类继承过来的是:
virtual void f()
,子类对f重写的部分是:{cout << "virtual void f()" << endl; }
2.4 基类的指针去调用
void Pay(Person* ptr)//ptr就是基类的指针
{ptr->BuyTicket();//基类的指针去调用delete ptr;
}
int main()
{int option = 0;string name;do{cout << "请选择身份:";cout << "1.普通人 2.学生 3.军人" << endl;cin >> option;if (option == -1)break;cout << "请输入名字:";cin >> name;switch (option){case 1:Pay(new Person(name));break;case 2:Pay(new Student(name));break;case 3:Pay(new Solider(name));break;default:cout << "身份输入错误,请重新输入!" << endl;break;}cout << "=======================" << endl;} while (option != -1);return 0;
}
总结两个条件:
- 子类虚函数重写父类虚函数(重写:三同(函数名 + 参数 + 返回值) + 虚函数)
- 父类指针或者引用去调用虚函数
虚函数重写的两个例外:
-
协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)
class A {}; class B : public A {}; class Person { public:virtual A* f() { cout << "virtual A* f() " << endl;return nullptr; } }; //虚函数对返回值有一个例外:协变,协变的类型必须是父子关系类的指针或者,上面的A是父类,B是子类 class Student : public Person { public:virtual B* f() { cout << "virtual B* f() " << endl;return nullptr; } }; int main() {Person p;Student s;Person* ptr = &p;ptr->f();ptr = &s;ptr->f();return 0; }
执行结果:
-
析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person { public://Person析构函数加了virtual,关系就变了//重定义(隐藏)关系 -> 重写(覆盖)关系virtual ~Person(){cout << "~Person()" << endl;} }; class Student : public Person { public:~Student(){cout << "~Student()" << endl;} };int main() {//普通对象没有影响Person p;Student s;Person* ptr = new Person;delete ptr;//ptr->destructor() + operator delete(ptr)//子类Student的析构函数对Person的析构函数构成了重写函数名都被处理成destructor,所以此时能够构成多态ptr = new Student;delete ptr;//ptr->destructor() + operator delete(ptr)return 0; }
结论:如果设计一个类,可能会作为基类,那么析构函数最好被定义为虚函数,与子类形成多态。
2.4 C++11 override 和 final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有 得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
-
- final: 修饰虚函数,不能被重写
class Person { public:virtual void fun() final{} }; class Student : public Person { public:void fun(){cout << "Student" << endl;} };
- 修饰类,表示该类不能被继承
class Person final { }; class Student : public Person { public: };
注意:截至目前就有两种不能被子类继承的方式:
方法一:将父类的构造函数后面或者父类的类名后面加上final修饰
方法二:将父类的构造函数私有化,这样子类即使继承了也无法实例化子类对象
-
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Person { public:virtual void fun(){} }; class Student : public Person { public:virtual void fun()override{cout << "Student" << endl;} };
此时子类完成了对从父类继承而来的fun函数的重写,此时编译不报错。
class Person { public:virtual void fun(){} }; class Student : public Person { public:void func()override{cout << "Student" << endl;} };
此时子类中的函数func并不是从父类继承而来的,所以没有完成重写(注意重写的要求:三同+虚),此时编译器会报错:
2.5 重载、覆盖(重写)、隐藏(重定义)的对比
3. 抽象类
3.1 概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。抽象类虽然不能定义对象,但是可以定义抽象类的指针,并且可以通过抽象类的指针实现多态。
class Person
{virtual void fun() = 0;//纯虚函数
};
int main()
{Person p;return 0;
}
注意:只有虚函数的后面允许添加=0,如果是普通的函数,就无法添加。
class Person
{
public:void fun() = 0;
};
注意:继承了抽象父类的子类必须对纯虚函数进行重写(不是重定义),将把从父类继承而来的纯虚函数覆盖掉,不然子类也是一个抽象类,无法实例化对象。
class Person
{
public:virtual void fun() = 0;
};
class Student : public Person
{
public://必须对从抽象父类继承来的纯虚函数进行重写,不然子类也是一个抽象类,即无法实例化对象virtual void fun()//对父类纯虚函数进行了重写,此时能够实例化对象{cout << "Student" << endl;}
};
int main()
{Student s;//程序不会报错return 0;
}
间接功能:要求子类必须对父类中的纯虚函数进行重写,不然将无法实例化子类对象。
定义抽象类的指针来实现多态:
int main()
{Student s;Person* ptr = &s;ptr->fun();return 0;
}
运行结果:
问:纯虚函数是否可以进行定义?
答:可以,但是没有任何的意义,因为父类作为抽象类中无法实例化对象,并且纯虚函数继承给子类的只有接口,定义并没有被继承过去,并且纯虚函数必须被重写,所以纯虚函数就永远都没有被调用到的可能。
3.2 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
所以如果不实现多态,不要把函数定义成虚函数。
4.多态的原理
4.1虚函数表
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};int main()
{Base b;cout << sizeof(b) << endl;return 0;
}
通过观察测试我们发现b对象的大小是8个字节,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析
通过观察和测试,我们发现了以下几点问题:
-
派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
-
基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表 中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
虚函数重写:语法层的概念,派生类对继承类虚函数的实现进行了重写
虚函数覆盖:原理层的概念,子类的虚表拷贝父类的虚表进行了修改,覆盖了重写的虚函数
-
另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。结论:非虚函数不会被放在虚表中。
-
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
-
总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生 类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
-
这里还有很容易混淆的问题:虚函数存在哪的?虚表存在哪的?
答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。注意:虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?
虚表:一个类型对应一个虚表,所有这个类型的对象都存这个虚表指针,即同一个类型的对象公用一个虚表。
注意:在构造函数的初始化阶段就将初始化对象的虚表指针了。
下面通过代码进行验证:
int c = 0; int main() {int a = 0;static int b = 1;const char* str = "hello world";int* p = new int[10];Derive d;printf("栈:%p\n", &a);printf("静态区/数据段:%p\n", &b);printf("静态区/数据段:%p\n", &c);printf("常量区/代码段:%p\n", str);printf("常量区/代码段:%p\n", main);printf("堆:%p\n", p);printf("虚表:%p\n", *((int*)&d));return 0; }
运行结果:
从上面代码可以看到,虚表存储的位置位于main函数的地址和字符串常量之间,所以结论如下:
结论:虚表存储的位置是常量区即代码段。
-
区分虚表和虚基表,虚表又称虚函数表,存储的是虚函数的地址。虚基表存储的是偏移量。
代码:
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;}void Func3(){cout << "Derive::Base::Func3()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;Base* p = &b;//指向父类对象p->Func1();//调用父类的Func1p->Func3();cout << endl;p = &d;//指向子类对象p->Func1();//调用子类的Func1p->Func3();return 0;
}
运行截图:
汇编查看对比:
问:父类对象赋值给子类对象,也会发生切片,但是为什么子类对象就不能实现多态?
答:
代码:
int main()
{Base b;Derive d;Base r1 = b;r1.Func1();r1.Func3();cout << endl;Base r2 = d;r2.Func1();r2.Func3();return 0;
}
执行结果:
从原理的角度来看,编译器看是否符合多态的条件,如果符合,就实现多态(运行时决议),如果不符合,就实现编译时决议,在编译阶段就根据是什么对象就确定调用函数的地址,所以无法实现多态。
从编译器设计者角度来看:
注意:子类对象赋值给父类对象进行切片时,子类对象的虚表指针并没有拷贝到父类对象的虚表指针中。
执行代码r2 = d
前
执行r2 = d
后:
从上图中可以看到:赋值前后,虚表指针并没有发生改变。
结论:对象切片的时候,子类只会拷贝成员给子类对象,不会拷贝虚表指针。如果拷贝就会发生混乱,一个父类类型的对象中到底存储的是父类的虚表指针还是子类的虚表指针就都有可能,那下面的调用是调用父类的虚函数还是子类的虚函数就不确定了。
Base* ptr = &b;
ptr->Func1();
问:虚函数如果只声明,但是不定义会怎样?使用上面的知识进行解答?
答:首先,虚函数的意义就是被子类重写形成多态,如果只声明的话是无法实例化对象的,对于虚函数,类对象中需要维护一张虚函数表vtable,表中存放各个虚函数的地址,如果没有进行定义的话,虚函数表中的地址就没有确切且有意义的值。
问:为什么普通的函数实例化类对象就不会报错?
答:首先明确一点,普通函数是所有类对象共享的,普通函数是存放在进程虚拟地址空间的.text即程序段中的,所以在实例化类对象时,在类对象中不存储普通函数的地址。
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。
- 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
4.3 动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
5.单继承和多继承关系的虚函数表
5.1 单继承中的虚函数表
代码:
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;}void Func3(){cout << "Derive::Base::Func3()" << endl;}virtual void Func4(){cout << "Derive::Func4()" << endl;}
private:int _d = 2;
};
int main()
{Derive d;return 0;
}
监视窗口查看虚表:
在虚函数表中并没有看到虚函数Func的虚函数指针!
此时查看内存窗口:
我们推测007f150a
这个位置的地址可能会是虚函数Func4的地址,接下来进行验证,取内存的值,打印并调用,确认是否是Func4(当然,我们可以再写一个类继承Derive,来查看这个类对象的Func4地址):
//取内存的值,打印并调用,确认是否是Func4//typedef void(*)() V_FUNC;//这样语法是错误的typedef void(*V_FUNC)();void PrintVFTable(V_FUNC* a)
{for (size_t i = 0; a[i] != nullptr; ++i){printf("[%d]:%p->", i, a[i]);//打印虚表V_FUNC f = a[i];f();}
}
int main()
{//Base b;Derive d;PrintVFTable((V_FUNC*)(*((int*)&d)));return 0;
}
执行结果:
我们可以看到我们猜想的那个地址的值确实就是Func4函数的地址。
结论:VS监视窗口下看到的虚函数表不一定是真实的,可能被处理过。
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(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%p,->", i, vTable[i]);VFPTR f = vTable[i];f();//此处虽然我们是直接调用的,但是编译器还是认为我们是通过某个对象调用的,所以该有的操作都有,这也是为什么会多次调用的原因}cout << endl;
}
int main()
{Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVTable(vTableb2);return 0;
}
运行结果:
观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
代码:
int main()
{Derive d;Base1* ptr1 = &d;Base2* ptr2 = &d;Derive* ptr3 = &d;cout << ptr1 << endl;cout << ptr2 << endl;cout << ptr3 << endl;return 0;
}
问:观察下面的输出结果,为什么两个func1的地址是不一样的?
答:因为上面打印出来的地址都不是函数真正的地址即虚表中存储的并不是函数func1真正的地址,而是VS编译器进行一次或者多次封装之后的地址,经过多次跳转之后就能到达真正函数的地址(通过汇编指令可以查看)。
问:在Base2类型的指针调用fun1时,为什么会出现这样一条汇编指令?
sub ecx,8
答:我们在调用f1的时候,采取了一种类似卡bug的方式进行调用,并不是一种正常的方式,正常的成员函数调用的方式应该是通过某个对象去调用函数,但是虽然我们使用了一种卡bug的方式,但是编译器该做的事情还是都做了,即还是使用了一种对象指针调用的方式:
f() ------> ptr->func1() ptr是Base1类型的指针 f() ------> ptr->func2() ptr是Base2类型的指针
即类似采用下面的方式去调用:
Derive d; Base1* ptr1 = &d; Base2* ptr2 = &d; ptr1->func1(); ptr2->func1(); //上面调用的都是Derive::func1(),但是在两个虚表中
在汇编指令中,当我们执行代码
ptr2->func1()
时又出现了汇编指令sub ecx 8
结论:Derive对象Base2虚表中调用fun1时,是Base2指针ptr2去调用。但是这时ptr2发生了切片指针偏移,需要修正,中途需要修正存储this指针(this指针指向的必须是Derive类型的对象,所以需要-8)ecx的值。
注意:我们调用的始终都是Derive的func1,所以调用函数的this指针的类型必须都是Derive*,所以才需要进行修正。
图示:
5.3. 菱形继承、菱形虚拟继承
此处只需要注意下面的情况:
class A
{
public:virtual void f(){}
};
class B : public A
{
public:virtual void f(){}
};
class C : public A
{
public:virtual void f(){}
};
class D : public B, public C
{
public:virtual void f(){}
};
注意:此时必须对D类中的f()函数进行重写,不然继承来的f函数并不明确,不知道是从B还是从C继承来的。
6.继承和多态常见的面试问题
6.1 继承和多态常见的面试问题
-
下面程序输出结果是什么? (A)
#include<iostream> using namespace std; class A { public:A(const char* s) { cout << s << endl; }~A() {} }; class B :virtual public A { public:B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; } }; class C :virtual public A { public:C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; } }; class D :public B, public C { public:D(const char* s1, const char* s2, const char* s3, const 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; }
A:class A class B class C class D
B:class D class B class C class A
C:class D class C class B class A
D:class A class C class B class D
解析:初始化的顺序跟声明的顺序有关,而和定义的顺序无关,在类中,父类先构造,然后是子类,所以上面class D的初始化列表中先对A初始化,然后是B、C。
打印截图:
-
以下程序输出结果是什么(B)
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: 以上都不正确
解析:首先子类能够调用test是因为作为子类的B类继承了A类中的test函数(注意:test虽然被继承下来的,但是this指针的类型依旧是A*,所以p->test()在调用的时候实际发生了切片操作,即作为this指针的B*被切片成了A(父类指针),所以此时触发了多态的情况,即指向谁就调用谁,p指向的是B类型的对象,所以调用的是B类型的对象,即B类从A类中继承下来的func)*。
B类func是对A类func的继承,同时重写又是接口继承,所以B类中的func函数实际上是:
virtual void func(int val = 1)//从A类中继承过来的接口,virtual关键字、返回值、函数名以及参数和缺省参数都是接口 { std::cout << "B->" << val << std::endl; }
所以最终的输出结果是B->1,选B。
题目改动,下面的代码的执行结果是什么?
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->func();return 0; }
执行结果:
问:为什么是这样的结果,而不是B->1?
答:因为此处只是普通的函数调用即B类型的指针调用B类中的func函数,并不涉及多态,必须只有父类的指针或者引用去调用子类才属于多态。
6.2 问答题
-
什么是多态?
-
什么是重载、重写(覆盖)、重定义(隐藏)?
-
多态的实现原理?
-
inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是虚函数,因为虚函数要放到虚表中去,即多态和内联无法同时共存。
代码验证:
#include<iostream> using namespace std; class A { public://内联virtual inline void f1(){cout << "A::f1()" << endl;}//非内联virtual void f2(); }; void A::f2() {cout << "A::f2()" << endl; } //B类对A类中的f1和f2进行重写 class B : public A { public:virtual void f1()//f1依旧是内联函数{cout << "B::f1()" << endl;}virtual void f2();//f2为非内联函数 }; void B::f2() {cout << "B::f2()" << endl; }void Func1(A* ptr) {ptr->f1();ptr->f2(); } void Func2(A ptr) {ptr.f1();ptr.f2(); } using namespace std; int main() {A aa;B bb;Func2(bb);//普通调用Func1(&bb);//多态调用return 0; }
普通调用:
多态调用:
-
静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。 (注意:语法上virtual和static不能同时使用)
-
构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表 阶段才初始化的。 虚表的在编译阶段生成,在调用构造函数阶段才进行初始化(虚函数的意义就是为了多态,但是多态调用时要到虚函数表中去找,构造函数之前都没有初始化,如果把构造函数给定义成虚函数了,那么构造函数将无法找到)。(注意:语法上强制限定了virtual不能用来修饰构造函数)
-
析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。
-
对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针 对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。
-
虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况 下存在代码段(常量区)的。
-
C++菱形继承的问题?虚继承的原理?答:注意这里不要把虚函数表(存的是虚函数的地址,是为了实现多态)和虚基表(存的是偏移量,是为了解决数据冗余和二义性)搞混了。
-
什么是抽象类?抽象类的作用?答:参考(3.抽象类)。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。