电商网站建设实训要求,福州网站建设招聘信息,深圳网页制作推广公司排名,企业名称禁限用规则文章目录 继承笔试面试题1. 什么是菱形继承#xff1f;菱形继承的问题是什么#xff1f;2. 什么是菱形虚拟继承#xff1f;如何解决数据冗余和二义性#xff1f;3. 继承和组合的区别#xff1f;什么时候用继承#xff1f;什么时候用组合#xff1f; 选择题 多态概念考察… 文章目录 继承笔试面试题1. 什么是菱形继承菱形继承的问题是什么2. 什么是菱形虚拟继承如何解决数据冗余和二义性3. 继承和组合的区别什么时候用继承什么时候用组合 选择题 多态概念考察问答题1. 什么是多态2. 什么是重载、重写(覆盖)、重定义(隐藏)3. 多态的实现原理4. inline 函数可以是虚函数吗5. 静态成员可以是虚函数吗6. 构造函数可以是虚函数吗7. 析构函数可以是虚函数吗什么场景下析构函数是虚函数8. 对象访问普通函数快还是虚函数更快9. 虚函数表是在什么阶段生成的存在哪10. C菱形继承的问题虚继承的原理11. 什么是抽象类抽象类的作用 选择题选项分析 继承
笔试面试题
什么是菱形继承菱形继承的问题是什么什么是菱形虚拟继承如何解决数据冗余和二义性的继承和组合的区别什么时候用继承什么时候用组合
1. 什么是菱形继承菱形继承的问题是什么
菱形继承指的是一个类通过两个子类继承了同一个基类这两个子类再被另一个派生类继承形成菱形结构。例如
class A {};
class B : public A {};
class C : public A {};
class D : public B, public C {};在这个结构中D 通过 B 和 C 间接继承了两次 A这就形成了菱形继承。
菱形继承的问题主要有两个
数据冗余由于 D 通过 B 和 C 继承了两份 A导致存在两份相同的 A 成员。这在内存中会引起冗余浪费空间。二义性问题当在 D 中试图访问 A 的成员时编译器无法确定该访问来自 B 继承的 A 还是 C 继承的 A从而导致二义性。例如
D d;
d._a; // 编译器不确定是访问 B::_a 还是 C::_a2. 什么是菱形虚拟继承如何解决数据冗余和二义性
菱形虚拟继承是为了解决菱形继承中的数据冗余和二义性问题的一种机制。通过在继承时使用 virtual 关键字编译器确保只会有一个 A 的实例被共享而不会有两个冗余的 A 实例。例如
class A {};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};解决方式
数据冗余问题通过虚拟继承D 类中的 A 只会有一份实例不管是通过 B 还是 C 继承D 都只包含一份 A。这样消除了冗余。二义性问题由于只有一个 A 实例编译器在 D 中访问 A 的成员时不会再出现二义性。例如
D d;
d._a; // 正常访问唯一的 A 实例虚拟继承通过虚基表指针实现B 和 C 各自保存一个指针指向一个共享的 A 实例这样就避免了冗余的 A。
3. 继承和组合的区别什么时候用继承什么时候用组合
继承和组合都是用来复用代码和实现类之间关系的两种手段但它们的适用场景和概念有明显的区别
继承Inheritance 描述的是“is-a”关系如果类 B 继承类 A意味着 B 是 A 的一种特殊类型比如 Cat 是一种 Animal。特点 子类自动继承父类的所有属性和行为。继承是一种强耦合子类的行为和父类行为紧密关联。一旦父类发生变化子类也会受到影响。 适用场景 当子类是父类的一种类型时使用继承是合适的。例如Car 继承 Vehicle。适合复用父类的方法和属性同时允许子类对父类的行为进行扩展或重写。 组合Composition 描述的是“has-a”关系如果类 B 包含了类 A 的对象意味着 B 拥有 A 的功能或行为但 B 并不是 A 的一种类型。例如Car 包含了一个 Engine但 Car 不是一种 Engine。特点 组合是弱耦合每个类保持独立性。可以动态地替换或改变组合类的行为不需要修改组合类本身的代码。 适用场景 当一个类的功能可以通过另一类来实现但它们之间不是类型的关系时使用组合。当需要在运行时组合对象行为或功能而不希望因为继承导致复杂的耦合关系。
总结
继承用于表达“B 是 A”的关系is-a。组合用于表达“B 拥有 A”的关系has-a。当两个类之间有明显的层次关系时继承是适合的当一个类需要复用另一个类的功能时但没有“类型”关系时使用组合。
选择题 A.可以存在如函数重载 B.基类与子类函数名字相同参数不同形成的是隐藏 C.可以共存 D.成员函数在同一个类里面同名此时构成了重载但变量一定不能同名故正确 A. 错误 解释选项 A 认为会打印 A::f()这是不正确的。原因是 B 类中的 f(int) 函数隐藏了基类 A 中的 f()因此在调用 b.f() 时编译器无法找到无参数的 f() 函数导致编译错误。所以不会打印 A::f()。 B. 错误 解释选项 B 认为会打印 B::f()但这是不可能的因为 b.f() 调用的是无参数版本的函数而 B 类中的 f(int) 是带参数的。因此编译器也无法匹配到 B::f(int)。最终仍然会导致编译错误。 C. 错误 解释选项 C 部分正确即“不能通过编译是对的”但给出的原因是错的。问题不在于成员变量 a而是 子类 B 中的 f(int) 函数隐藏了父类 A 中的 f() 函数并且由于没有无参的 f() 函数可用编译时会报错。因此C 的理由是不准确的。 D. 正确 解释D 是正确的选项因为前面所有的解释都有误。真正的编译问题来源于函数隐藏而不是成员变量 a 的名字冲突或者其他原因。 因此答案选 D 是最准确的选择。 A. 错误 解释如果基类有默认构造函数派生类的构造函数并不需要显式调用基类的构造函数。只有在基类没有默认构造函数或需要传递参数给基类构造函数时派生类才需要在初始化列表中显式调用基类的构造函数。因此A 是不正确的。 B. 错误 解释派生类的构造函数首先会调用 基类 的构造函数来初始化基类部分成员之后才会初始化派生类的成员。因此初始化的顺序是 先基类再子类B 的表述是反的。 C. 错误 解释派生类的析构函数会自动调用基类的析构函数并按照 构造顺序的逆序 进行析构即先析构派生类再析构基类。因此C 是不正确的。 D. 正确 解释在定义派生类的构造函数时确实有时候需要参考基类的构造函数特别是当基类没有默认构造函数或者需要特定参数来构造基类时。在这种情况下派生类的构造函数必须通过初始化列表显式调用基类的构造函数。因此D 是正确的。 综上所述答案 D 是正确的。 子类实例化对象由于继承的有父类。所以会先构造父类然后在构造子类析构顺序完全按照构造的相反顺序进行析构故答案为 C A.先构造父类在构造子类 故正确 B.不一定如果父类有默认构造函数就不需要 C.刚好相反先调用子类在调用父类 D.派生类的析构函数往往还需要连同父类析构函数一起调用同时清除父类的资源 A.静态成员函数也可以被继承 B.成员变量所有的都会被继承无论公有私有 C.友元函数不能被继承相当于你爹的朋友不一定是你的朋友 D.静态成员属于整个类不属于任何对象所以在整体体系中只有一份 A.静态变量就不被包含 B.同理静态变量就不被包含 C.父类所有成员都要被继承因此包含了 D.静态成员一定是不被包含在对象中的 E.很显然以上说法都不正确 区分静态成员和静态变量 答案是 B原因如下 A. 正确 解释基类指针可以直接指向子类对象这叫做 向上转型upcasting。由于子类是基类的扩展它包含了基类的所有部分所以基类指针可以指向子类对象。这种操作是合法的且常见。 B. 错误 解释基类对象不能直接**赋值**给子类对象。因为基类对象没有子类特有的成员和方法直接赋值会丢失子类中的额外信息或导致类型不匹配因此不允许这种操作。这叫做 向下转型downcasting需要显式的类型转换而且在某些情况下需要类型检查如在C中使用 dynamic_cast。 C. 正确 解释子类对象的引用不能引用基类的对象。这是因为基类对象不包含子类的特有成员或行为因此无法用子类的引用来指向基类对象。这种操作是非法的。 D. 正确 解释子类对象可以直接赋值给基类对象这也是 向上转型。这种情况下赋值操作会切割掉子类对象中特有的部分只保留基类部分。这在赋值时会发生“对象切割”问题。 分析:p1和p2虽然都是其父类但在子类内存模型中其位置不同所以p1和p2所指子类的位置也不相同因此p1!p2, 由于p1对象是第一个被继承的父类类型所有其地址与子类对象的地址p3所指位置都为子类对象的起始位置因此p1p3,所以C正确 答案是 C详细解析如下 A. 正确 解释在这段代码中class D 继承了两个 B分别从 C1 和 C2 继承。因此D 中实际上包含了两个 B 对象、一个 C1 对象、一个 C2 对象和一个 D 对象。 B 类的大小为 4 字节假设 int 为 4 字节。C1 类的大小为 4B 4c1 8 字节。C2 类的大小为 4B 4c2 8 字节。D 类的大小为 8C1 8C2 4d 20 字节。 因此D 的总大小为 20 字节故 A 是正确的。 B. 正确 解释由于 C1 和 C2 都继承了 B并且没有使用虚拟继承所以 D 中实际上有两个 B 的实例分别存在于 C1 和 C2 中。因此B 是正确的。 C. 错误 解释由于 D 类中存在两份 B分别继承自 C1 和 C2因此 D 对象不能直接访问 b 成员。直接访问 b 会产生二义性b 是来自 C1 继承的 B 还是 C2 继承的 B为了消除二义性必须通过指定路径来访问例如 d.C1::b 或 d.C2::b。 因此C 是不正确的。 D. 正确 解释菱形继承的确会带来二义性问题因为子类会从两个路径继承同一个基类。在这种情况下可以使用 虚拟继承 来避免多个基类实例的问题。因此D 是正确的。 假如 C1,C2都虚拟继承了B 那么本题的每个选项解释如下 如果 C1 和 C2 都虚拟继承了 B那么菱形继承的结构会发生变化。通过虚拟继承**B**** 类只会在 **D** 类中存在一份实例**解决了二义性问题。基于这个前提我们可以重新分析每个选项。 先来看类的定义假设 C1 和 C2 现在虚拟继承了 B A. D 总共占了 20 个字节 解释虚拟继承改变了继承的布局。虚拟继承确保 B 类只存在一份实例这样可以避免多次继承 B 的问题。 由于虚拟继承可能会带来额外的指针用来指向虚拟基类实例具体的内存布局依赖于编译器实现因此在某些编译器中虚拟继承可能会导致额外的字节开销最终结果未必是 20 字节。不过假设编译器管理良好且没有其他额外开销该选项可能是正确的但在实际编译器中往往会比 20 字节大。A 可能正确但不一定是完全准确的。 B 类的大小为 4 字节假设 int 为 4 字节。C1 和 C2 类通过虚拟继承继承了 B所以 B 的实例在 D 中只存在一份。C1 类的大小为 4虚拟 B 4c1。C2 类的大小为 4虚拟 B 4c2。D 类的大小为 4虚拟 B 4c1 4c2 4d。 B. B 中的内容总共在 D 对象中存储了两份 解释由于 C1 和 C2 都采用了虚拟继承B 类在 D 类中只存在一份实例。因此B 是错误的。 C. D 对象可以直接访问从基类继承的 b 成员 解释在使用虚拟继承的情况下D 类中 B 的实例只存在一份因此不再存在二义性问题。D 对象可以直接访问从 B 继承的 b 成员无需显式地指定路径。因此 C 是正确的。 D. 菱形继承存在二义性问题尽量避免设计菱形继承 解释通过虚拟继承菱形继承的二义性问题得到了很好的解决。虽然菱形继承仍然可能引发复杂的设计问题但在这种情况下二义性不再存在。因此 D 是错误的因为虚拟继承已经解决了二义性。 多态
概念考察
下面哪种面向对象的方法可以让你变得富有( A ) A: 继承 B: 封装 C: 多态 D: 抽象
解释这是一道幽默的选择题实际上并没有哪种面向对象方法可以直接让你变得“富有”。不过这里的“富有”是双关语隐喻了“继承”在实际编程中可以让你复用父类的属性和方法就像生活中的“继承财富”一样。通过继承你可以得到父类的功能而不需要重新实现因此继承“让你变得富有”是一个双关的比喻。
(D)是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关 而对方法的调用则可以关联于具体的对象。 A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
解释动态绑定Dynamic Binding是一种多态机制它允许在程序运行时根据实际对象的类型来调用相应的方法。在编译时程序并不确定将调用哪个方法而是在运行时决定。因此这使得代码更加灵活和可扩展。动态绑定是实现多态的核心机制。
其他选项的解释
A: 继承继承允许子类从父类继承属性和方法不能直接实现动态绑定。B: 模板模板是C中的一种泛型编程机制允许在编译时生成代码并不涉及动态绑定。C: 对象的自身引用这是对象通过this指针访问自身成员的机制与动态绑定无关。
面向对象设计中的继承和组合下面说法错误的是C A继承允许我们覆盖重写父类的实现细节父类的实现对于子类是可见的是一种静态复 用也称为白盒复用 B组合的对象不需要关心各自的实现细节之间的关系是在运行时候才确定的是一种动 态复用也称为黑盒复用 C优先使用继承而不是组合是面向对象设计的第二原则 D继承可以使子类能自动继承父类的接口但在设计模式中认为这是一种破坏了父类的封 装性的表现
解释这道题考察的是继承与组合的区别错误的选项是C。实际上组合优先于继承 是面向对象设计中的一条重要原则特别是在设计模式中被称为“组合优于继承原则”Favor composition over inheritance。过度使用继承会导致代码耦合性增强、灵活性降低。
其他选项的解释
A继承允许我们覆盖重写父类的实现细节父类的实现对于子类是可见的是一种静态复用也称为白盒复用正确。继承中的复用被称为“白盒复用”因为子类能够直接看到父类的实现细节。B组合的对象不需要关心各自的实现细节之间的关系是在运行时候才确定的是一种动态复用也称为黑盒复用正确。组合是一种“黑盒复用”对象间的关系可以在运行时建立而不需要了解彼此的内部实现。D继承可以使子类能自动继承父类的接口但在设计模式中认为这是一种破坏了父类的封装性的表现正确。继承虽然提供了接口复用但有时会导致父类的封装性被破坏因为子类可以依赖父类的内部实现。
以下关于纯虚函数的说法,正确的是(A ) A声明纯虚函数的类不能实例化对象 B声明纯虚函数的类是虚基类 C子类必须实现基类的纯虚函数 D纯虚函数必须是空函数
解释声明了纯虚函数的类被称为抽象类不能直接实例化对象。抽象类的目的是提供一个接口并由派生类实现该接口的纯虚函数。
其他选项的解释
B声明纯虚函数的类是虚基类错误。纯虚函数与虚基类无关虚基类用于解决多重继承问题。C子类必须实现基类的纯虚函数不完全正确。子类可以选择不实现纯虚函数但这样它本身也会变成抽象类。D纯虚函数必须是空函数错误。纯虚函数只是声明没有实现但可以有实现只不过声明部分是 0。
关于虚函数的描述正确的是(D—B ) A派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B内联函数不能是虚函数 C派生类必须重新定义基类的虚函数 D虚函数可以是一个static型的函数
解释虚函数的多态机制是在运行时实现的而内联函数是在编译时展开的。这两者是相互冲突的因此虚函数不能是内联函数。
其他选项的解释
- **A派生类的虚函数与基类的虚函数具有不同的参数个数和类型**错误。虚函数的重写要求派生类中的虚函数与基类的虚函数有相同的参数列表。
- **C派生类必须重新定义基类的虚函数**错误。派生类可以选择重写基类的虚函数u但不一定必须重新定义。/u
- **D虚函数可以是一个static型的函数**错误。u虚函数是与对象实例相关的而静态函数与类相关虚函数不能是静态函数。/u关于虚表说法正确的是 C—D A一个类只能有一张虚表 B基类中有虚函数如果子类中没有重写基类的虚函数此时子类与基类共用同一张虚表 C虚表是在运行期间动态生成的 D一个类的不同对象共享该类的虚表
解释
D一个类的不同对象共享该类的虚表正确。虚表vtable是针对类生成的而不是针对每个对象生成的因此同一个类的所有对象共享同一张虚表。对象中的虚表指针vptr指向这张虚表。
其他选项的解释
A一个类只能有一张虚表错误。如果一个类继承了多个具有虚函数的类或者自身定义了多个虚函数可能会有多张虚表存在。B基类中有虚函数如果子类中没有重写基类的虚函数此时子类与基类共用同一张虚表错误。即使子类没有重写虚函数它依然有自己的虚表虽然可能会指向相同的虚函数。C虚表是在运行期间动态生成的错误。虚表是在编译期生成的而不是在运行时生成。虚表指针会在运行时动态调整以指向正确的虚表
假设A类中有虚函数B继承自AB重写A中的虚函数也没有定义任何虚函数则B–D AA类对象的前4个字节存储虚表地址B类对象前4个字节不是虚表地址 BA类对象和B类对象前4个字节存储的都是虚基表的地址 CA类对象和B类对象前4个字节存储的虚表地址相同 DA类和B类虚表中虚函数个数相同但A类和B类使用的不是同一张虚
解释
DA类和B类虚表中虚函数个数相同但A类和B类使用的不是同一张虚表正确。A类和B类的虚表中的虚函数数量相同但由于B类重写了A类的虚函数B类的虚表会指向B类的实现而A类的虚表指向A类的实现。因此虽然虚表中函数的数量相同虚表本身是不同的。
其他选项的解释
AA类对象的前4个字节存储虚表地址B类对象前4个字节不是虚表地址错误。A类和B类对象的前4个字节都是存储虚表指针vptr用于指向各自的虚表。BA类对象和B类对象前4个字节存储的都是虚基表的地址错误。这与虚基类无关A类和B类存储的都是虚表指针。CA类对象和B类对象前4个字节存储的虚表地址相同错误。B类重写了A类的虚函数B类有自己独立的虚表因此它们的虚表地址不同。
下面程序输出结果是什么? A
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;
}Aclass A class B class C class D Bclass D class B class C class A
Cclass D class C class B class A Dclass A class C class B class D
解释
虚继承会确保在派生类中共享一个唯一的基类实例。因此当 B 和 C 虚继承了 A 时在派生类 D 中A 类的构造函数只会被调用一次。其他就是按顺序来就行了
多继承中指针偏移问题下面说法正确的是( 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;
}Ap1 p2 p3 Bp1 p2 p3 Cp1 p3 ! p2 Dp1 ! p2 ! p3
解释
d的内存布局类似
Base1 部分存储 Base1::_b1
Base2 部分存储 Base2::_b2
Derive 部分存储 Derive::_dBase1* p1 d; // 指向 d 中的 Base1 部分p1 指向对象 d 的起始地址。
Base2* p2 d; // 指向 d 中的 Base2 部分这个地址与 p1 不同
Derive* p3 d; // 指向 d 整个 Derive 对象的起始地址。与 p1 相同//因为 Derive 对象的起始部分就是 Base1。
以下程序输出结果是什么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: 以上都不正确
解释
1.virtual void test() { func(); }
test 是 A 类的虚函数并且 B 没有重写它。因此当你调用 p-test() 时它实际上执行的是 A::test()。**A::test()**** 中调用了 **func()**而 **func()** 是虚函数**。虚函数的行为是动态绑定的意味着根据对象的实际类型来调用合适的函数。在这种情况下p 是 B* 类型所以 func() 会调用 B::func()。
virtual void func(int val 1)
虽然 B::func() 覆盖了 A::func()但有一个非常重要的点默认参数是静态绑定的它在编译时绑定。具体来说A::test() 中的 func() 调用会使用 uA/u 类中定义的默认参数而不是 B 类中的默认参数。因此当 A::test() 调用 func() 时它使用的是 A 类中给定的默认参数 val 1即使实际调用的是 B::func()。
调用流程
p-test() 实际调用的是 A::test()。A::test() 中调用了虚函数 func()。由于 p 指向 B 类对象动态绑定会让 B::func() 被调用。虽然调用的是 B::func()但 A::test() 使用的是 A 的默认参数 val 1因为默认参数是在编译时绑定的。
输出结果
虽然 B::func(int val 0) 具有默认参数 0但由于 A::test() 调用了 func() 并使用了 A 类的默认参数 1因此程序输出的是B-1
问答题
作为面试者我将依次回答上列面试题
1. 什么是多态
多态是面向对象编程中的一种特性它允许同一个函数或方法在不同对象上具有不同的行为。在 C 中多态主要有两种形式
编译时多态静态多态通过函数重载和运算符重载实现。运行时多态动态多态通过继承和虚函数机制实现。当基类的指针或引用指向派生类对象时调用虚函数会根据实际对象的类型执行不同的实现。
多态的核心目的是提高代码的可扩展性和复用性。
2. 什么是重载、重写(覆盖)、重定义(隐藏)
重载Overloading是同一个作用域内允许函数同名但参数类型或参数个数不同的现象。编译器通过参数列表的不同进行区分这是静态多态的一种形式。 例子
void func(int a);
void func(double a);重写Overriding也称为覆盖是派生类重新实现基类中的虚函数必须保持函数签名完全相同。通过这种方式基类的指针或引用在运行时调用派生类的实现这是动态多态的实现方式。 例子
class Base {
public:virtual void func() { cout Base; }
};
class Derived : public Base {
public:void func() override { cout Derived; }
};重定义Hiding是派生类中的同名函数隐藏了基类中的非虚函数或静态成员函数。尽管函数签名不同基类函数不再可见。可以通过作用域分辨符 Base::func() 访问基类函数。 例子
class Base {
public:void func(int a) { cout Base; }
};
class Derived : public Base {
public:void func(double a) { cout Derived; }
};3. 多态的实现原理
多态的实现基于 虚函数表vtable 和 虚函数指针vptr。
虚函数表对于包含虚函数的类编译器会为该类生成一个虚函数表表中存储该类的虚函数地址。虚函数指针每个对象都会有一个虚函数指针vptr指向其所属类的虚函数表。当通过基类指针或引用调用虚函数时编译器通过该指针找到对象对应的虚函数表从而在运行时调用正确的派生类函数。
这一机制支持运行时根据对象的实际类型执行相应的虚函数。
4. inline 函数可以是虚函数吗
可以但是编译器通常会忽略虚函数的 inline 特性。当函数被声明为虚函数时它必须通过虚函数表来调用而不是内联替换。虚函数调用涉及动态绑定无法直接替换为内联代码因此虚函数即使被声明为 inline在大多数情况下也不会被内联。
5. 静态成员可以是虚函数吗
不能。原因如下
静态成员函数 不属于任何对象它们不依赖于具体的实例也没有 this 指针。虚函数 依赖于对象的 this 指针通过 vtable虚函数表来实现动态绑定而静态成员函数无法访问虚函数表。因此静态成员函数不能是虚函数。
class Base {
public:virtual void func() { // 普通虚函数cout Base::func() called endl;}static void staticFunc() { // 静态成员函数cout Base::staticFunc() called endl;}
};class Derived : public Base {
public:void func() override { // 重写虚函数cout Derived::func() called endl;}// 重定义静态成员函数static void staticFunc() {cout Derived::staticFunc() called endl;}
};int main() {Base* ptr new Derived();//通过虚函数表实现了动态绑定ptr 实际指向 Derived 对象// 调用虚函数运行时绑定输出 Derived::func()ptr-func();// 静态成员函数是基于类名调用的不能通过指针动态绑定// ptr-staticFunc(); // 错误静态成员函数不能通过对象指针调用// 必须用类名调用静态成员函数静态成员函数没有虚表不能实现多态Base::staticFunc(); // 输出 Base::staticFunc() calledDerived::staticFunc(); // 输出 Derived::staticFunc() calleddelete ptr;return 0;
}
6. 构造函数可以是虚函数吗
答不能原因是对象中的虚函数表指针vptr是在构造函数初始化列表阶段才初始化的。在对象构造过程中虚函数表还未完成设置此时如果调用虚函数会无法正确绑定到具体的函数实现。因此构造函数无法是虚函数。
详细解释
虚函数的作用是在运行时实现动态绑定多态但在构造函数执行时类的虚函数表指针尚未被正确设置。因为在对象创建时基类部分的构造函数先执行这时候派生类部分还没被初始化如果基类构造函数是虚函数就会产生不一致的行为。
7. 析构函数可以是虚函数吗什么场景下析构函数是虚函数
答可以并且在多态场景下基类的析构函数最好定义为虚函数。
如果一个类可能作为基类被继承并且会通过基类指针或引用指向派生类对象在这种情况下基类的析构函数需要是虚函数。否则当通过基类指针删除派生类对象时只有基类的析构函数会被调用而派生类的析构函数不会被执行从而导致资源泄漏。
场景
class Base {
public:virtual ~Base() { cout Base destructor called endl; }
};class Derived : public Base {
public:~Derived() { cout Derived destructor called endl; }
};int main() {Base* ptr new Derived();delete ptr; // 如果Base的析构函数不是虚函数这里只会调用Base的析构函数导致派生类的析构函数不执行。return 0;
}输出
Derived destructor called
Base destructor called如果基类的析构函数不是虚函数Derived 类的析构函数将不会被调用造成内存泄漏或其他资源释放问题。
8. 对象访问普通函数快还是虚函数更快
答普通函数更快。
如果是通过普通对象调用函数普通函数和虚函数的访问速度是一样的。但在通过指针或引用调用时普通函数更快。调用虚函数需要动态绑定这意味着编译时不能确定具体要调用哪个函数必须通过查找虚函数表vtable来找到对应的函数指针因此虚函数调用会略慢一些。普通函数则在编译时就能确定不需要查表直接调用。
例子
class Base {
public:virtual void virtualFunc() { cout Base::virtualFunc endl; }void normalFunc() { cout Base::normalFunc endl; }
};int main() {Base b;Base* ptr b;ptr-normalFunc(); // 普通函数直接调用ptr-virtualFunc(); // 虚函数查表调用return 0;
}对于普通函数调用编译器直接生成调用指令而虚函数则需要通过虚函数表查找稍微增加了开销。
9. 虚函数表是在什么阶段生成的存在哪
答虚函数表vtable是在编译阶段生成的。
虚函数表一般存储在代码段常量区。每个带有虚函数的类在编译时会生成一个虚函数表虚函数表中存储了该类的所有虚函数的地址。每个对象有一个虚表指针vptr指向虚函数表的位置。编译器在构造对象时会初始化虚表指针vptr指向类对应的虚函数表。
在程序运行时如果通过指针或引用调用虚函数编译器会通过对象的 vptr 查找虚函数表中的函数地址从而实现动态绑定。
总结
编译阶段生成虚函数表。虚函数表一般存储在代码段属于程序的常量区域。
10. C菱形继承的问题虚继承的原理
菱形继承问题
C 中的菱形继承是一种特殊的多继承结构它是指一个类从两个基类继承而这两个基类又继承自同一个父类。菱形继承引发了两个主要问题
数据冗余问题 当派生类通过多条路径继承同一个基类时基类的成员变量会在派生类中出现多份副本。这意味着派生类对象中会有多份相同的基类成员导致内存浪费和逻辑混乱。二义性问题 由于派生类通过多个路径继承基类编译器可能无法确定调用哪个基类的成员函数尤其是当基类有同名成员时。例如编译器会遇到二义性调用 A::func() 时是从 B 继承的 A 版本还是从 C 继承的 A 版本。
例子
class A {
public:int x;void func() { cout As func endl; }
};class B : public A {};
class C : public A {};
class D : public B, public C {};int main() {D obj;// obj.x 会导致二义性编译器不确定是B::A::x还是C::A::x// obj.func() 也会导致二义性
}虚继承的原理
虚继承是为了解决上述菱形继承中的问题。通过虚继承基类的成员在继承路径上只保留一份拷贝从而解决了数据冗余和二义性问题。
虚继承声明在继承时基类前加上 virtual 关键字表示对该基类进行虚继承。
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};虚基表虚继承的实现依赖于虚基表Virtual Base Table简称 VBT。虚基表存储了虚基类成员在派生类中的偏移量。当派生类访问虚基类的成员时会通过虚基表找到正确的偏移从而解决了菱形继承中的二义性问题。虚基表的工作原理是 当类进行虚继承时派生类会为虚基类保留一份特殊的偏移量表即虚基表。每个虚继承的类都会有指向虚基表的指针。在访问虚基类的成员时编译器会通过该指针找到虚基表进而计算虚基类成员的实际地址避免产生冗余的副本。
虚继承的好处
只保留一份基类成员节省内存避免数据冗余。消除了访问基类成员的二义性解决了编译时的歧义。
11. 什么是抽象类抽象类的作用
抽象类定义
抽象类是包含纯虚函数的类。纯虚函数是一个没有实现的虚函数其定义如下
virtual void func() 0;任何包含至少一个纯虚函数的类都称为抽象类。抽象类不能被实例化必须通过派生类来实现其中的纯虚函数。
抽象类的作用
强制子类实现特定功能 抽象类通过定义纯虚函数强制派生类必须提供这些函数的具体实现。这种设计确保了某些行为在派生类中一定会被实现。提供接口继承 抽象类体现了接口继承的概念即抽象类定义了一组功能的接口而派生类实现具体功能。通过这种方式可以实现面向接口编程减少对具体实现的依赖从而提高代码的扩展性和可维护性。实现多态 抽象类是实现多态的重要手段。使用抽象类的指针或引用可以在运行时通过动态绑定调用派生类的具体实现。
例子
class Shape {
public:virtual void draw() 0; // 纯虚函数
};class Circle : public Shape {
public:void draw() override { cout Drawing a circle endl; }
};class Rectangle : public Shape {
public:void draw() override { cout Drawing a rectangle endl; }
};int main() {Shape* shape1 new Circle();Shape* shape2 new Rectangle();shape1-draw(); // 动态绑定调用Circle的drawshape2-draw(); // 动态绑定调用Rectangle的drawdelete shape1;delete shape2;
}抽象类通过纯虚函数为派生类提供了统一的接口同时也使得派生类必须实现这些接口从而保证了多态的实现。
选择题 选项 A. 被 virtual 修饰的函数称为虚函数 解释此说法基本正确但需要更精确地表述。被 virtual 关键字修饰的**成员函数才称为虚函数用于支持动态绑定和多态。详细来说virtual 关键字用于标记基类中的成员函数使得在派生类中可以通过基类指针或引用调用派生类的重写版本动态绑定。所以准确的表述应为“被 virtual 修饰的成员函数**称为虚函数”。因此A 是不完全准确的。 选项 B. 虚函数的作用是用来实现多态 解释这句话是正确的。虚函数的主要作用就是支持 运行时多态。通过虚函数C 实现了动态绑定使得程序在运行时能够根据实际对象类型来调用正确的函数版本而不是仅仅根据编译时的类型。这样可以实现“一个接口多种实现”的设计模式。 因此虚函数是实现运行时多态的关键机制。B 是正确的。 - **静态绑定**编译时决定调用哪个函数适用于非虚函数。
- **动态绑定**运行时根据对象的实际类型决定调用哪个函数适用于虚函数。选项 C. 虚函数在类中声明和类外定义时都必须加 virtual 关键字 解释这是 错误的。virtual 关键字只需要在函数声明时添加在类外定义时不需要重复添加 virtual 关键字。编译器通过函数声明中的 virtual 关键字来知道该函数是虚函数后续的定义部分不需要再次声明。例如 class Base {
public:virtual void show(); // 声明时加 virtual
};// 定义时无需加 virtual
void Base::show() {cout Base show endl;
}因此C 是错误的。 选项 D. 静态虚成员函数没有 this 指针 解释此说法包含两部分 因此D 是错误的。 1. **静态成员函数没有 **this** 指针**这是正确的因为静态成员函数属于类本身而不属于某个对象因此它们不依赖于具体的对象也没有 this 指针。
2. **静态虚成员函数**这是不可能的。C 不允许成员函数同时是 static 和 virtual。虚函数必须通过对象来调用因为虚函数的行为依赖于对象的动态类型而静态成员函数与类绑定不依赖于对象所以这两个关键字是互相矛盾的。A.友元函数不属于成员函数不能成为虚函数 B.静态成员函数就不能设置为虚函数 C.静态成员函数与具体对象无关属于整个类核心关键是没有隐藏的this指针可以通过类名::成员函数名 直接调用此时没有this无法拿到虚表就无法实现多态因此不能设置为虚函数 D.尤其是父类的析构函数强力建议设置为虚函数这样动态释放父类指针所指的子类对象时能够达到析构的多态 A.多态分为编译时多态和运行时多态也叫早期绑定和晚期绑定 B.编译时多态是早期绑定主要通过重载实现 C.模板属于编译时多态故错误 D.运行时多态是动态绑定也叫晚期绑定 A.必须是父类的函数设置为虚函数 B.必须通过父类的指针或者引用才可以子类的不行 C.不是在编译期而应该在运行期间编译期间编译器主要检测代码是否违反语法规则此时无法知道基类的指针或者引用到底引用那个类的对象也就无法知道调用那个类的虚函数。在程序运行时才知道具体指向那个类的对象然后通过虚表调用对应的虚函数从而实现多态。 D.正确实现多态是要付出代价的如虚表虚表指针等所以不实现多态就不要有虚函数了 A.重写即覆盖针对多态 重定义即隐藏 两者都发生在继承体系中 B.重载只能在一个范围内不能在不同的类里 C.只有重写要求原型相同 D.重写和重定义是两码事,重写即覆盖针对多态 重定义即隐藏 E.重写和重定义是两码事,重写即覆盖针对多态 重定义即隐藏 F.重写要求函数完全相同重定义只需函数名相同即可 G.很明显有说法正确的答案 选项 B 是正确的因为其他选项的描述都有误 A. 如果父类和子类都有相同的方法参数个数不同将子类对象赋给父类对象后采用父类对象调用该同名方法时实际调用的是子类的方法。 错误这个描述混淆了重载和覆盖。此处说的“相同的方法”实际上应该是“重载的方法”因为它们参数个数不同。在这种情况下父类的同名方法如果不被重写不会调用子类的方法而是调用父类的方法。 B. 选项全部都不正确。 正确由于 A、C、D 选项都有错误因此 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);
}错误虽然 a 最后指向了 B 的对象但 **ua/u** 是 **uA/u** 类型的指针调用 a-test(1.1) 会查找 A 中的 test(float a) 方法。而因为参数类型不匹配1.1 是 double 类型编译器会发生类型转换以匹配 float。最终输出是 1.1而不是 1。 A. 基类和子类的f1函数构成重写 错误虽然 B 中的 f1 函数是重定义因为 f1 在基类 A 中没有被声明为虚函数它并没有构成重写。重写发生在子类重写父类的虚函数而 f1 并不是虚函数。 B. 基类和子类的f3函数没有构成重写因为子类f3前没有增加virtual关键字 错误f3 函数确实构成重写因为在 C 中当子类中的函数与基类的虚函数具有相同的签名时即使子类不显式地使用 virtual 关键字也视为重写。f3 在基类中是虚函数因此 B 中的 f3 是对 A::f3 的重写。 C. 基类引用引用子类对象后通过基类对象调用f2时调用的是子类的f2 错误这一说法的后半部分是错误的。虽然基类引用可以引用子类对象并且在通过基类引用调用虚函数时会执行子类的实现多态但题目中提到的是通过“基类对象”调用 f2。这意味着如果我们通过 A 的对象调用 f2那么无论 f2 是否是虚函数它都会调用 A 的实现而不会是 B 的实现。因此基类对象调用时总是调用基类的版本。 D. f2和f3都是重写f1是重定义 正确这一说法是正确的。f2 是虚函数在 B 中被重写f3 也是重写尽管没有加 virtual 关键字。f1 是基类中的非虚函数因此在 B 中的实现是重定义而不是重写。 在这道题中我们需要分析关于抽象类和纯虚函数的描述确定哪个选项是错误的。下面是每个选项的详细解析 选项分析 A. 纯虚函数的声明以“0;”结束 正确这是纯虚函数的标准语法。在 C 中纯虚函数通过在声明后加上 0 来定义表示该函数没有实现并且在派生类中必须被重写。 B. 有纯虚函数的类叫抽象类它不能用来定义对象 正确这个描述是正确的。只要一个类中有一个或多个纯虚函数这个类就被称为抽象类抽象类不能直接实例化对象。 C. 抽象类的派生类如果不实现纯虚函数它也是抽象类 正确这个说法也是正确的。如果派生类未实现基类中的纯虚函数则派生类也成为抽象类不能被实例化。 D. 纯虚函数不能有函数体 错误这个说法是错误的。虽然纯虚函数通常在基类中不需要实现但是它可以有函数体。这种情况下纯虚函数可以在基类中提供默认的实现派生类可以选择重写这个实现。如下所示 class Base {
public:virtual void pureVirtualFunction() 0; // 纯虚函数virtual void implementedFunction() {// 有函数体的虚函数}
};A.抽象类不能实例化对象所以以对象返回是错误 B.抽象类可以定义指针而且经常这样做其目的就是用父类指针指向子类从而实现多态 C.参数为对象所以错误 D.直接实例化对象这是不允许的 A. 一个类只能有一张虚表 错误当使用多重继承时可能会为每个基类生成不同的虚表。因此多个基类可能会导致同一派生类对象有多张虚表。比如菱形继承:B,C继承A。D继承了B,C。 这就导致了 D 类拥有多张虚表。 B. 基类中有虚函数如果子类中没有重写基类的虚函数此时子类与基类共用同一张虚表 错误虽然子类可以使用基类的虚函数但每个类都有自己的虚表。在这种情况下子类会有自己的虚表即使其中的虚函数没有重写。基类和子类的虚表是不同的。 C. 虚表是在运行期间动态生成的 错误虚表通常是在编译期间生成的而不是在运行期间。虚表的结构是静态的它在编译时确定。 D. 一个类的不同对象共享该类的虚表 正确所有该类的对象共享同一张虚表。这意味着同一类的不同对象在调用虚函数时都会使用同一张虚表来进行查找。这可以通过代码验证创建一个类的多个对象并调用虚函数确保它们都共享同一虚表。 class Base {
public:virtual void func() { std::cout Base::func() std::endl; }
};int main() {Base obj1, obj2;// 检查虚表地址std::cout obj1 virtual table address: *(void**)obj1 std::endl;std::cout obj2 virtual table address: *(void**)obj2 std::endl;// 确认共享同一虚表if (*(void**)obj1 *(void**)obj2) {std::cout Both objects share the same virtual table. std::endl;}else {std::cout Objects do not share the same virtual table. std::endl;}return 0;
}7.下面函数输出结果是
class A {
public: virtual void f() {cout A::f() endl;}
};class B : public A {
private://注意是 私有的virtual void f() {cout B::f() endl;}
};int main() {A* pa (A*)new B; // 强制类型转换将 B 的对象指针转为 A 的指针pa-f(); // 调用虚函数 freturn 0;
}A.B::f()
B.A::f()因为子类的f()函数是私有的
C.A::f()因为强制类型转化后生成一个基类的临时对象pa实际指向的是一个基类的临时对象
D.编译错误私有的成员函数不能在类外调用函数的输出结果是 B::f()原因如下
多态的工作原理
虚函数类 A 中的 f() 是一个虚函数意味着可以通过基类指针调用派生类的实现。即使派生类 B 的 f() 函数是私有的编译器在处理虚函数调用时仍然会查找对象的虚表找到实际指向的 B 类的 f()。强制类型转换A* pa (A*)new B; 这行代码将 B 类的对象强制转换为 A 类的指针。虽然 B 的 f() 是私有的但由于 pa 实际上指向的是一个 B 类型的对象因此在调用 pa-f() 时程序会根据对象的实际类型font stylecolor:#DF2A3F;B/font来决定调用哪个版本的 f()。访问权限与多态虽然 B 的 f() 是私有的但这并不妨碍通过 A 的指针调用它。C 的多态机制依赖于虚表vtable来查找方法确保调用的是对象的真实类型的方法而不是指针类型的方法。因此尽管 font stylecolor:#DF2A3F;f()/font 是私有的font stylecolor:#DF2A3F;pa-f()/font 调用的依然是 font stylecolor:#DF2A3F;B/font 的 font stylecolor:#DF2A3F;f()/font。
选项分析
A. B::f()正确因为实际调用的是 B 类的 f() 方法。B. A::f()因为子类的f()函数是私有的错误因为私有性只影响访问权限而不影响多态调用。C. A::f()因为强制类型转化后生成一个基类的临时对象pa实际指向的是一个基类的临时对象错误pa 指向的是 B 类型的对象。D. 编译错误私有的成员函数不能在类外调用错误因为访问权限不影响多态的运行时行为。 正确答案是 B. B::x()。在调用 b.x(); 时由于 B 类重写了 A 类的虚函数 x()最终调用的是 B 中的 x() 方法。 以下程序输出结果是( )
class A
{
public:A(): m_iVal(0) { test(); } // 1virtual void func() { std::cout m_iVal ; } // 2void test() { func(); } // 3public:int m_iVal;
};class B : public A
{
public:B() { test(); } // 4virtual void func() { m_iVal; std::cout m_iVal ; } // 5
};int main(int argc, char* argv[])
{A* p new B; // 6p-test(); // 7return 0;
}
A.1 0
B.0 1
C.0 1 2
D.2 1 0
E.不可预期
F. 以上都不对详细执行步骤 对象创建 A* p new B; 创建 B 对象时会先调用 A 的构造函数。 执行 A 的构造函数 A(): m_iVal(0) { test(); } 将 m_iVal 初始化为 0。调用 test()此时仍在 A 的构造阶段。注意在此阶段B 的 func() 尚未被调用因为虚表尚未完成构造。调用的是 A 的 func()。 **执行 **A::test() 在 A::test() 中调用 func()。由于此时在 A 的构造过程中虚函数机制并未生效因此调用的是 A 中的 func()。输出 0当前 m_iVal 值。 返回到 B 的构造函数 A 的构造函数完成后接下来执行 B 的构造函数。在 B 的构造函数中执行 test()。 执行 B 的构造函数 B() { test(); } 再次调用 test()此时 B 的虚表已经构建完成可以正常调用 B 的虚函数。 当在 B 的构造函数中调用 test() 时实际上是调用了 A 的 test() 方法。因为 继承B 继承了 A 的所有公共和保护成员包括方法 test()。因此B 类的对象可以直接调用 A 的成员函数。 可见性由于 test() 是 A 中的公共成员函数它在 B 中是可见的。 **执行 **B::test() 在 B::test() 中调用 func() 由于此时 font stylecolor:#DF2A3F;this/font 指针指向的是 font stylecolor:#DF2A3F;B/font 的对象所以此时会调用 font stylecolor:#DF2A3F;B/font 中的 font stylecolor:#DF2A3F;func()/font因为已经完成了 font stylecolor:#DF2A3F;B/font 的构造。m_iVal; 使 m_iVal 从 0 变为 1并输出 1。 **返回到 main() 中的 **p-test(); 再次调用 p-test()由于 p 是指向 B 的 A 指针这里也会调用 B 的 test()。调用 func()此时会调用 B 中的 func()使 m_iVal 从 1 变为 2并输出 2。 最终输出 因此输出结果依次为 第一次输出 0来自 A::func()第二次输出 1来自 B::func()当 B 构造时第三次输出 2来自 B::func()在 main() 中调用时 最终的输出是 0 1 2因此选择 C 作为答案。 A.父类对象和子类对象的前4字节都是虚表地址 B.A类对象和B类对象前4个字节存储的都是虚表的地址只是各自指向各自的虚表。选B. C.不相同各自有各自的虚表 D.A类和B类不是同一类内容不同 问题背景 假设有以下类的继承结构 B1 和 B2 是基类都包含虚函数。D 类继承 B1 和 B2并且对 B1 和 B2 的虚函数进行了重写同时还增加了新的虚函数。 虚表的概念 虚表是由类的虚函数组成的数据结构每个包含虚函数的类都有自己的虚表。虚表指针是指向虚表的指针通常保存在每个对象的内部。每个对象的虚表指针指向它的虚表。 选项解析 A. D类对象模型中包含了3个虚表指针 错误在多重继承中子类这里是 D只有自己的虚表和继承自每个父类的虚表指针。由于 D 只继承了两个基类B1 和 B2因此只会有两个虚表指针。即使 D 重写了 B1 和 B2 的虚函数也不会增加虚表的数量。因此D 不会包含三个虚表指针。 B. D类对象有两个虚表D类新增加的虚函数放在第一张****虚表最后 正确D 类对象确实只包含两个虚表一个是来自 B1一个是来自 B2。D 自己的新虚函数会被添加到第一个父类B1的虚表中因为 D 在调用这些函数时只会通过其第一个父类的虚表进行解析。其新添加的虚函数被放置在 B1 的虚表的最后一项。 C. D类对象有两个虚表D类新增加的虚函数放在第二张虚表最后 错误新增加的虚函数只会放在 B1 的虚表中而不是放在 B2 的虚表中。虽然 D 对两个基类的虚函数进行了重写但新的虚函数不需要被放入第二个虚表中因此这一选项是错误的。 D. 以上全部错误 错误如上所述选项 B 是正确的因此此选项也不成立。 [ 声明 ] 由于作者水平有限本文有错误和不准确之处在所难免 本人也很想知道这些错误恳望读者批评指正我是勇敢滴勇~感谢大家的支持