从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题(中):https://developer.aliyun.com/article/1521916
4. 多继承中的虚函数表
刚才我们看的是单继承,我们现在再看复杂一点的多继承。
代码:Base1 和 Base2 都进行了重写:
#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 = 1; }; class Base2 { public: virtual void func1() { cout << "Base2::func1" << endl; } virtual void func2() { cout << "Base2::func2" << endl; } private: int b2 = 2; }; class Derive : public Base1, public Base2 { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } private: int d1 = 3; }; typedef void(*V_FUNC)(); // 把函数指针 void(*)() typedef 成 V_FUNC void Print_VFTable(V_FUNC* arr/*, size_t n*/) { printf("vfptr:%p\n", arr); for (size_t i = 0; arr[i] != nullptr/*i < n*/; ++i) // 如果编译器在最后无nullptr,就要手动传参,有几个就打印几个 { printf("vfptr[%d]: %p\n", i, arr[i]); V_FUNC Func = arr[i]; Func(); // 函数指针加括号->调用对应的函数 } cout << endl; } int main() { Derive d; Print_VFTable((V_FUNC*)(*(int*)&d)); // 打印Base2的虚表先强转成char* +1 是+一个字节 Print_VFTable((V_FUNC*)(*(int*)((char*)&d + sizeof(Base1)))); return 0; }
这里 Derive 明显会有两张虚表,我们先通过监视简单看一下:
func3 是放哪一个虚表里?是两张都放一份,还是选择一份放呢?
面试时就会问这样的问题,它是放在第一个虚表的,第二个虚表没有。
然后发现Derive::func1 的地址竟然不一样?然后直接打印func1的地址也是不一样的。
这里就是多套了一层,是一种保护机制。虽然不一样但是最后都跳到了函数上面去。
关于菱形继承及菱形虚拟继承:
实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,
另一方面这样的模型,访问基类成员有一定得性能损耗。
所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,
一般我们也不需要研究清楚,因为实际中很少用。
这里放两篇拓展阅读:
5. 继承和多态常见的面试问题
5.1 问答题
1. 什么是多态?
答:通俗来说,就是多种形态,具体点就是去完成某个行为,
当不同的对象去完成时会产生出不同的状态。而多态又分为编译时多态和运行时多态。
2. 什么是重载、重写(覆盖)、重定义(隐藏)?
答:重载是函数在同一作用域内,函数名相同,函数参数的个数,顺序,类型不同,与返回值无关;
重写是函数在子类和父类两个作用域,函数名称,函数参数,函数返回值都相同(协变除外);
重定义是函数在父类和子类在两个作用域,函数名称相同,不构成重写,就是重定义。
另外重定义不仅仅针对函数变量在父类和子类两个作用域名字相同也是构成重定义。
3. 多态的实现原理是什么?
答:多态的实现原理是因为对象中存在虚表指针,通过虚表指针来找到虚表,而虚表中存在着虚函数的地址,不同类的对象找到不同的虚表从而调用到不同的虚函数而实现了多态。
4. inline 函数可以是虚函数吗?
答: 能,因为虚函数要放到虚表中去,内联函数会直接展开成指令。我们在内联函数前加virtual编译器不会报错,因为内联只是一种建议,当加了virtual后编译器会自动舍弃内联属性。
5. 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6. 构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
7. 析构函数可以是虚函数吗?
答:可以,并且最好把基类的析构函数定义成虚函数。比如我们new了一个子类对象用了父类指针来接受,当我们销毁时我们期望调用父类的析构函数来进行销毁,而把父类的析构函数定义成虚函数的好处是编译器会做出特殊处理将父子的析构函数都处理成destructor,从而构成多态方便正确调用。
8. 对象访问普通函数快还是虚函数更快?
答:如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
9. 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况存在代码段(常量区)的。
10. C++菱形继承的问题?虚继承的原理?
答:C++菱形继承会出现数据冗余和二义性的问题。虚继承是为了消除数据冗余的问题;虚继承的原理是将冗余数据单独放在一边,让加了virtual的两个类共享该数据,但是为了方便这两个类都能够找得到该数据就在对象中多存了虚基表指针,通过该指针能够找到一张虚基表,虚基表中存在着偏移量,通过偏移量就能够方便的找到共享数据。
11. 什么是抽象类?抽象类的作用是什么?
答:在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。
5.2 选择题
1. 关于虚函数说法正确的是( )
A.被virtual修饰的函数称为虚函数
B.虚函数的作用是用来实现多态
C.虚函数在类中声明和类外定义时候,都必须加虚拟关键字
D.静态虚成员函数没有this指针
2. 关于多态,说法不正确的是( )
A.C++语言的多态性分为编译时的多态性和运行时的多态性
B.编译时的多态性可通过函数重载实现
C.运行时的多态性可通过模板和虚函数实现
D.实现运行时多态性的机制称为动态绑定
3. 关于重载、重写和重定义的区别说法正确的是( )【不定项选择】
A.重写和重定义都发生在继承体系中
B.重载既可以在一个类中,也可以在继承体系中
C.它们都要求原型相同
D.重写就是重定义
E.重定义就是重写
F.重写比重定义条件更严格
G.以上说法全错误
4. 关于重载和多态正确的是 ( )
A.如果父类和子类都有相同的方法,参数个数不同, 将子类对象赋给父类对象后, 采用父类对象调用该同名方法时,实际调用的是子类的方法
B.选项全部都不正确
C.重载和多态在C++面向对象编程中经常用到的方法,都只在实现子类的方法时才会使用
D.class A{ public: void test(float a) { cout << a; } }; class B :public A{ public: void test(int b){ cout << b; } }; void main() { A *a = new A; B *b = new B; a = b; a->test(1.1); } 结果是1
5. 以下哪项说法时正确的( )
class A
{
public:
void f1(){cout<<"A::f1()"<<endl;}
virtual void f2(){cout<<"A::f2()"<<endl;}
virtual void f3(){cout<<"A::f3()"<<endl;}
};
class B : public A
{
public:
virtual void f1(){cout<<"B::f1()"<<endl;}
virtual void f2(){cout<<"B::f2()"<<endl;}
void f3(){cout<<"B::f3()"<<endl;}
};
A.基类和子类的f1函数构成重写
B.基类和子类的f3函数没有构成重写,因为子类f3前没有增加virtual关键字
C.如果基类指针引用子类对象后,通过基类对象调用f2时,调用的是子类的f2
D.f2和f3都是重写,f1是重定义
6. 关于抽象类和纯虚函数的描述中,错误的是 ( )
A.纯虚函数的声明以“=0;”结束
B.有纯虚函数的类叫抽象类,它不能用来定义对象
C.抽象类的派生类如果不实现纯虚函数,它也是抽象类
D.纯虚函数不能有函数体
7. 假设A为抽象类,下列声明( )是正确的
A.A fun(int);
B.A*p;
C.int fun(A);
D.A obj;
8. 关于不能设置成虚函数的说法正确的是( )
A.友元函数可以作为虚函数,因为友元函数出现在类中
B.成员函数都可以设置为虚函数
C.静态成员函数不能设置成虚函数,因为静态成员函数不能被重写
D.析构函数建议设置成虚函数,因为有时可能利用多态方式通过基类指针调用子类析构函数
9. 要实现多态类型的调用,必须( )
A.基类和派生类原型相同的函数至少有一个是虚函数即可
B.假设重写成功,通过指针或者引用调用虚函数就可以实现多态
C.在编译期间,通过传递不同类的对象,编译器选择调用不同类的虚函数
D.只有在需要实现多态时,才需要将成员函数设置成虚函数,否则没有必要
10. 关于虚表说法正确的是( )
A.一个类只能有一张虚表
B.基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C.虚表是在运行期间动态生成的
D.一个类的不同对象共享该类的虚表
11. 如果类B继承类A,A::x()被声明为虚函数,B::x()重写了A::x()方法,下述语句中哪个x()方法会被调用:( )
B b;
b.x();
A.A::x()
B.B::x()
C.A::x() B::x()
D.B::x() A::x()
12. 以下程序输出结果是( )
class A
{
public:
A ():m_iVal(0){test();}
virtual void func() { std::cout<<m_iVal<<‘ ’;}
void test(){func();}
public:
int m_iVal;
};
class B : public A
{
public:
B(){test();}
virtual void func()
{
++m_iVal;
std::cout<<m_iVal<<‘ ’;
}
};
int main(int argc ,char* argv[])
{
A*p = new B;
p->test();
return 0;
}
13. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
A.A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B.A类对象和B类对象前4个字节存储的都是虚表的地址
C.A类对象和B类对象前4个字节存储的虚表地址相同
D.A类和B类中的内容完全一样,但是A类和B类使用的不是同一张虚表
14. 假设D类先继承B1,然后继承B2,B1和B2基类均包含虚函数,D类对B1和B2基类的虚函数重写了,并且D类增加了新的虚函数,则:( )
A.D类对象模型中包含了3个虚表指针
B.D类对象有两个虚表,D类新增加的虚函数放在第一张虚表最后
C.D类对象有两个虚表,D类新增加的虚函数放在第二张虚表最后
D.以上全部错误
15. 下面函数输出结果是( )
class A
{
public:
virtual void f()
{
cout<<"A::f()"<<endl;
}
};
class B : public A
{
private:
virtual void f()
{
cout<<"B::f()"<<endl;
}
};
A* pa = (A*)new B;
pa->f();
A.B::f()
B.A::f(),因为子类的f()函数是私有的
C.A::f(),因为强制类型转化后,生成一个基类的临时对象,pa实际指向的是一个基类的临时对象
D.编译错误,私有的成员函数不能在类外调用
答案:
1. B
A.被virtual修饰的成员函数称为虚函数
B.正确
C.virtual关键字只在声明时加上,在类外实现时不能加
D.static和virtual是不能同时使用的
2. C
A.多态分为编译时多态和运行时多态,也叫早期绑定和晚期绑定
B.编译时多态是早期绑定,主要通过重载实现
C.模板属于编译时多态,故错误
D.运行时多态是动态绑定,也叫晚期绑定
3. AF
A.重写即覆盖,针对多态, 重定义即隐藏, 两者都发生在继承体系中
B.重载只能在一个范围内,不能在不同的类里
C.只有重写要求原型相同
D.重写和重定义是两码事,重写即覆盖,针对多态, 重定义即隐藏
E.重写和重定义是两码事,重写即覆盖,针对多态, 重定义即隐藏
F.重写要求函数完全相同,重定义只需函数名相同即可
G.很明显有说法正确的答案
4. B
A.使用父类对象调用的方法永远是父类的方法
B.正确
C.重载不涉及子类
D.输入结果为1.1
5. D
A.错误,构成重写是子类重写父类的virtual函数,
B.f3构成重写,重写时子类可以不要求加virtual关键字
C.通过父类对象调用的方法永远只能是父类方法
D.正确
6. D
A.纯虚函数的声明以“=0;”结束,这是语法要求
B.有纯虚函数的类叫抽象类,它不能用来定义对象,一般用于接口的定义
C.子类不实现父类所有的纯虚函数,则子类还属于抽象类,仍然不能实例化对象
D.纯虚函数可以有函数体,只是意义不大
7. B
A.抽象类不能实例化对象,所以以对象返回是错误
B.抽象类可以定义指针,而且经常这样做,其目的就是用父类指针指向子类从而实现多态
C.参数为对象,所以错误
D.直接实例化对象,这是不允许的
8. D
A.友元函数不属于成员函数,不能成为虚函数
B.静态成员函数就不能设置为虚函数
C.静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数
D.尤其是父类的析构函数强力建议设置为虚函数,这样动态释放父类指针所指的子类对象时,能够达到析构的多态
9. D
A.必须是父类的函数设置为虚函数
B.必须通过父类的指针或者引用才可以,子类的不行
C.不是在编译期,而应该在运行期间,编译期间,编译器主要检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用那个类的虚函数。在程序运行时,才知道具体指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。
D.正确,实现多态是要付出代价的,如虚表,虚表指针等,所以不实现多态就不要有虚函数了
10. D
A.多继承的时候,就会可能有多张虚表
B.父类对象的虚表与子类对象的虚表没有任何关系,这是两个不同的对象
C.虚表是在编译期间生成的
D.一个类的不同对象共享该类的虚表,可以自行写代码验证之
11. B
虽然子类重写了父类的虚函数,但只要是用对象去调用,则只能调用相对类型的方法,故B正确
12. C
分析:new B时先调用父类A的构造函数,执行test()函数,在调用func()函数,由于此时还处于对象构造阶段,多态机制还没有生效,所以,此时执行的func函数为父类的func函数,打印0,构造完父类后执行子类构造函数,又调用test函数,然后又执行func(),由于父类已经构造完毕,虚表已经生成,func满足多态的条件,所以调用子类的func函数,对成员m_iVal加1,进行打印,所以打印1, 最终通过父类指针p->test(),也是执行子类的func,所以会增加m_iVal的值,最终打印2, 所以答案为C 0 1 2
13. D
A.父类对象和子类对象的前4字节都是虚表地址
B.A类对象和B类对象前4个字节存储的都是虚表的地址,只是各自指向各自的虚表
C.不相同,各自有各自的虚表
D.A类和B类不是同一类内容不同
14. B
A.D类有几个父类,如果父类有虚函数,则就会有几张虚表,自身子类不会产生多余的虚表,所以只有2张虚表
B.正确
C.子类自己的虚函数只会放到第一个父类的虚表后面,其他父类的虚表不需要存储,因为存储了也不能调用
D.错误
15. A
A.正确
B.虽然子类函数为私有,但是多态仅仅是用子类函数的地址覆盖虚表,最终调用的位置不变,只是执行函数发生变化
C.不强制也可以直接赋值,因为赋值兼容规则作出了保证
D.编译正确
本篇完。
下一篇开始用C++接触高阶数据结构的内容了,所以下一篇放在新开的高阶数据结构专栏和C++专栏里。首先是搜索二叉树,然后是map和set容器。