A. 继承 B. 封装 C. 多态 D. 抽象
继承机制是面向对象程序设计使代码可以复用的最重要手段,继承是类设计层次的复用。
A. 继承 B. 模板 C. 对象的自身引用 D. 动态绑定
动态绑定又称后期绑定或晚绑定,就是多态。
A. 继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用。
B. 组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用。
C. 优先使用继承,而不是组合,是面向对象设计的第二原则。
D. 继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现。
优先使用组合,而不是继承。
A. 声明纯虚函数的类不能实例化对象 B. 声明纯虚函数的类是虚基类
C. 子类必须实现基类的纯虚函数 D. 纯虚函数必须是空函数
虽然声明纯虚函数的类不能实例化对象,但声明纯虚函数的类可以定义指针。
A. 派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B. 内联函数不能是虚函数
C. 派生类必须重新定义基类的虚函数 D. 虚函数可以是一个 static 型的函数
首先排除 A 和 C 选项,其次虚函数的地址是放在对象的虚表中,如果要形成多态,就必需要用对象的指针或引用来调用,而 static 就意味着是静态的,你连 this 指针都没有,那就不合理了。
内联函数不能是虚函数其实是一个存疑的选项,己验证。在 VS2019 下,内联函数加上虚函数后依然能编译通过,但是我们得知道内联函数对编译器而言只是一个建议,实际上一个函数真的成为内联函数,它就不可能是虚函数,因为内联函数是没有地址的,它直接在调用的地方展开,而虚函数是要把地址放到虚函数表中,所以这里一定会把 inline 给忽略掉。
A. 一个类只能有一张虚表。
B. 基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表。
C. 虚表是在运行期间动态生成的。
D. 一个类的不同对象共享该类的虚表。
上面的多继承中就有两张虚表,且严格来说虚表不是在类,而是在对象,所以 A 选项错误;
不管是否完成重写,父子类的对象都是有独立的虚表,所以排除 B 选项;
虚表如果是运行时动态生成,虚表是需要空间的,且运行起来只能在堆上申请,而虚表是在常量区或代码段,所以虚表是在在编译阶段生成的, C 选项也错。
正确答案为 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 选项;
#include
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)。
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 的大小。
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 选项。
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 选项。
多态是指不同继承关系的类和对象去调用同一函数,产生了不同的行为。
多态又分为静态多态和动态多态。
- 重载是指在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同,也就是说用同一个函数完成不同的功能。
- 重写(覆盖)是指两个函数分别在基类和派生类的作用域,这两个函数的函数名、参数、返回值都必须相同(协变例外),且这两个函数都是虚函数。
- 重定义(隐藏)是指两个函数分别在基类和派生类的作用域,这两个函数的函数名相同即可。
构成多态的父类对象和子类对象的成员当中都包含一个虚表指针,这个虚表指针指向一个虚表,虚表当中存储的是该类对应的虚函数地址。
因此,当父类指针指向父类对象时,通过父类指针找到虚表指针,然后在虚表当中找到的就是父类当中对应的虚函数;当父类指针指向子类对象时,通过父类指针找到虚表指针,然后在虚表当中找到的就是子类当中对应的虚函数。
可以,内联函数是会在调用的地方展开的,是没有地址的,但是 inline 只是一个建议,可以定义成虚函数的,当我们把内联函数定义成虚函数后,在多态调用中,编译器就忽略了该函数的内联属性,这个函数就不再是 inline 了,因为虚函数的地址被放到虚表中去。
不能,因为静态成员函数是存在整个类域中,没有 this 指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
虚函数是为了实现多态,多态都是运行时去虚表找决议,而静态成员函数都是在编译时决议,它是virtual 没有价值。
不可以,因为对象中的虚函数表指针是在构造函数初始化列表阶段(运行时)才初始化的,如果构造函数是虚函数,那么调用构造函数时对象中的虚表指针都没有初始化。构造函数时虚函数没有意义。
可以,并且最好把基类的析构函数定义成虚函数。当我们 new 一个父类对象和一个子类对象,并均用父类指针指向它们,在我们使用 delete 调用析构函数并释放对象空间时,只有当父类的析构函数是虚函数的情况下,才能正确调用父类和子类的析构函数,否则当我们使用父类指针 delete 对象时,只能调用到父类的析构函数。
不可以,拷贝构造也是构造函数,答案参考上面的构造函数。
operator= 可以,但是没有实际价值。
如果虚函数不构成多态,是普通对象,二者是一样快的。
如果虚函数构成多态的调用,是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,在运行时调用虚函数需要到虚函数表中去查找虚函数的地址。
构造函数初始化列表阶段初始化的是虚函数表指针,对象中存的也是虚函数表指针。虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
注意这里不要把虚函数表和虚基表搞混了。
菱形继承子类对象当中有两份父类的成员,会导致数据冗余和二义性的问题。
虚继承对于相同的虚基类在对象当中只会存储一份,若要访问虚基类的成员需要通过虚基表获取到偏移量,进而找到对应的虚基类成员,从而解决了数据冗余和二义性的问题。
抽象类体现了虚函数的继承是一种接口继承,强制子类去抽象纯虚函数,如果子类不抽象从父类继承下来的纯虚函数,那么子类也是抽象类也不能实例化出对象。
其次,抽象类可以很好的去表示现实世界中没有示例对象对应的抽象类型,比如:植物、人、动物等。