一、概念考查
1、下面哪种面向对象的方法可以让你变得富有( A )
A. 继承 B. 封装 C. 多态 D. 抽象
继承机制是面向对象程序设计使代码可以复用的最重要手段,继承是类设计层次的复用。
2、( D )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A. 继承 B. 模板 C. 对象的自身引用 D. 动态绑定
动态绑定又称后期绑定或晚绑定,就是多态。
3、面向对象设计中的继承和组合,下面说法错误的是?( C )
A. 继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用。
B. 组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用。
C. 优先使用继承,而不是组合,是面向对象设计的第二原则。
D. 继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现。
优先使用组合,而不是继承。
4、以下关于纯虚函数的说法,正确的是( A )
A. 声明纯虚函数的类不能实例化对象 B. 声明纯虚函数的类是虚基类
C. 子类必须实现基类的纯虚函数 D. 纯虚函数必须是空函数
虽然声明纯虚函数的类不能实例化对象,但声明纯虚函数的类可以定义指针。
5、关于虚函数的描述正确的是( B )
A. 派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B. 内联函数不能是虚函数
C. 派生类必须重新定义基类的虚函数 D. 虚函数可以是一个 static 型的函数
首先排除 A 和 C 选项,其次虚函数的地址是放在对象的虚表中,如果要形成多态,就必需要用对象的指针或引用来调用,而 static 就意味着是静态的,你连 this 指针都没有,那就不合理了。
内联函数不能是虚函数其实是一个存疑的选项,己验证。在 VS2019 下,内联函数加上虚函数后依然能编译通过,但是我们得知道内联函数对编译器而言只是一个建议,实际上一个函数真的成为内联函数,它就不可能是虚函数,因为内联函数是没有地址的,它直接在调用的地方展开,而虚函数是要把地址放到虚函数表中,所以这里一定会把 inline 给忽略掉。
6、关于虚表说法正确的是( D )
A. 一个类只能有一张虚表。
B. 基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表。
C. 虚表是在运行期间动态生成的。
D. 一个类的不同对象共享该类的虚表。
上面的多继承中就有两张虚表,且严格来说虚表不是在类,而是在对象,所以 A 选项错误;
不管是否完成重写,父子类的对象都是有独立的虚表,所以排除 B 选项;
虚表如果是运行时动态生成,虚表是需要空间的,且运行起来只能在堆上申请,而虚表是在常量区或代码段,所以虚表是在在编译阶段生成的, C 选项也错。
正确答案为 D。
7、假设A类中有虚函数,B 继承自 A,B 重写 A 中的虚函数,也没有定义任何虚函数,则( D )
A. A 类对象的前 4 个字节存储虚表地址,B 类对象前 4 个字节不是虚表地址
B. A 类对象和 B 类对象前 4 个字节存储的都是虚基表的地址
C. A 类对象和 B 类对象前 4 个字节存储的虚表地址相同
D. A 类和 B 类虚表中虚函数个数相同,但 A 类和 B 类使用的不是同一张虚表
A 类有虚函数,A 类对象的前 4 个字节当然是存储虚表地址,只要 B 类继承了 A 类,B 类的前 4 个字节也当然是存储虚表地址,只不过是不同的虚表地址,所以排除 A 选项;
虚基表是用来解决菱形继承问题的,与虚函数表是两个概念。注意区分解决菱形继承的虚继承的虚基表,所以排除 B 选项;
不管是否重写,父子类的对象都是有独立的虚表,所以排除 C 选项;
8、下面程序输出结果是什么? ( A )
#include<iostream>
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;
}
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
注意这里的初始化顺序和初始化列表中的顺序无关,这里是与继承的顺序,也就是声明的顺序有关。
这里 D 继承了 B、C,要去调用父类的构造函数,谁先继承谁就先调,按理说先由 D 调用 B 的构造函数,再由 B 调用 A 的构造函数,再由 D 调用 C 的构造函数,再由 C 调用 A 的构造函数 (A ➡ B ➡ A ➡ C ➡ D)。
但是因为 virtual 后,编译器做了处理,不可能让 B 对 A 初始化一次,C 对 A 再初始化一次,所以应该是 (A ➡ B ➡ C ➡ D)。
9、多继承中指针偏移问题?下面说法正确的是( C )
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
如下图,所以选择 C 选项。注意 p1 和 p3 虽然都指向同一地址,但是它们的类型不一样,p1 是 Base1 的大小,p3 是 Derive 的大小。
10、以下程序输出结果是什么( 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: 以上都不正确
首先,这里不涉及多态,因为 p 的类型是子类的指针,p 再去调用父类继承下来的 test,但是这里父类中 test 函数的参数中有一个 A* this 的指针,所以调用时就是一个父类的指针指向子类对象,满足多态的条件之一,其次子类重写可以不写 virtual,我们需要重写虚函数,并满足三同或三个例外,但是没有说缺省参数也要相同,标准也基本不会提,我们就认为它构成重写。所以这里 this 调用 func 时符合多态,调用的是子类的 func,所以这里就从 B 选项 和 C 选项中选择。
我们又说了普通函数的继承是实现继承,而虚函数的继承是接口继承,接口继承指的是函数的声明,包括函数名、参数、返回值,所以这里把函数的缺省参数也继承下来,而这里重写的是它的实现,跟参数这些无关,所以选择 B 选项。
11、以下两段程序输出结果是什么 ( B C )
class A
{
public:
virtual void func(int val = 1)
{}
void test()
{}
};
int main()
{
//1、
A* p1 = nullptr;
p1->func();
//2、
A* p2 = nullptr;
p2->test();
return 0;
}
A. 编译报错 B. 运行崩溃 C. 正常运行
成员函数的地址不在对象中存储,而存在于公共代码段。这里调用成员函数,不会去访问 p1 和 p2 指向的空间,也就不存在空指针解引用了,这里把 p1 和 p2 传递给隐含的 this 指针,但是 p1 是一个父类的指针,而 func 是 virtual,这里转换必然要去虚表中找,因为从语法识别的角度,编译器看到 p1->func() 时也不知道指向的是哪个对象,所以这里依然对 p1 进行解引用了,所以选择 B 和 C 选项。
二、问答题
1、什么是多态?
多态是指不同继承关系的类和对象去调用同一函数,产生了不同的行为。
多态又分为静态多态和动态多态。
2、什么是重载、重写(覆盖)、重定义(隐藏)?
- 重载是指在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同,也就是说用同一个函数完成不同的功能。
- 重写(覆盖)是指两个函数分别在基类和派生类的作用域,这两个函数的函数名、参数、返回值都必须相同(协变例外),且这两个函数都是虚函数。
- 重定义(隐藏)是指两个函数分别在基类和派生类的作用域,这两个函数的函数名相同即可。
3、多态的实现原理?
构成多态的父类对象和子类对象的成员当中都包含一个虚表指针,这个虚表指针指向一个虚表,虚表当中存储的是该类对应的虚函数地址。
因此,当父类指针指向父类对象时,通过父类指针找到虚表指针,然后在虚表当中找到的就是父类当中对应的虚函数;当父类指针指向子类对象时,通过父类指针找到虚表指针,然后在虚表当中找到的就是子类当中对应的虚函数。
4、inline函数可以是虚函数吗?
可以,内联函数是会在调用的地方展开的,是没有地址的,但是 inline 只是一个建议,可以定义成虚函数的,当我们把内联函数定义成虚函数后,在多态调用中,编译器就忽略了该函数的内联属性,这个函数就不再是 inline 了,因为虚函数的地址被放到虚表中去。
5、静态成员(static 函数)可以是虚函数吗?
不能,因为静态成员函数是存在整个类域中,没有 this 指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
虚函数是为了实现多态,多态都是运行时去虚表找决议,而静态成员函数都是在编译时决议,它是virtual 没有价值。
6、构造函数可以是虚函数吗?
不可以,因为对象中的虚函数表指针是在构造函数初始化列表阶段(运行时)才初始化的,如果构造函数是虚函数,那么调用构造函数时对象中的虚表指针都没有初始化。构造函数时虚函数没有意义。
7、析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
可以,并且最好把基类的析构函数定义成虚函数。当我们 new 一个父类对象和一个子类对象,并均用父类指针指向它们,在我们使用 delete 调用析构函数并释放对象空间时,只有当父类的析构函数是虚函数的情况下,才能正确调用父类和子类的析构函数,否则当我们使用父类指针 delete 对象时,只能调用到父类的析构函数。
8、拷贝构造和 operator= 可以是虚函数吗?
不可以,拷贝构造也是构造函数,答案参考上面的构造函数。
operator= 可以,但是没有实际价值。
9、对象访问普通函数快还是虚函数更快?
如果虚函数不构成多态,是普通对象,二者是一样快的。
如果虚函数构成多态的调用,是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,在运行时调用虚函数需要到虚函数表中去查找虚函数的地址。
10、虚函数表是在什么阶段生成的,存在哪的?
构造函数初始化列表阶段初始化的是虚函数表指针,对象中存的也是虚函数表指针。虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
11、C++ 菱形继承的问题?虚继承的原理?
注意这里不要把虚函数表和虚基表搞混了。
菱形继承子类对象当中有两份父类的成员,会导致数据冗余和二义性的问题。
虚继承对于相同的虚基类在对象当中只会存储一份,若要访问虚基类的成员需要通过虚基表获取到偏移量,进而找到对应的虚基类成员,从而解决了数据冗余和二义性的问题。
12、什么是抽象类?抽象类的作用?
抽象类体现了虚函数的继承是一种接口继承,强制子类去抽象纯虚函数,如果子类不抽象从父类继承下来的纯虚函数,那么子类也是抽象类也不能实例化出对象。
其次,抽象类可以很好的去表示现实世界中没有示例对象对应的抽象类型,比如:植物、人、动物等。