当前位置: 首页 > news >正文

继承和多态常见面试问题解析

目录

一、概念考察题

1、下面哪种面向对象的方法可以让你变得富有(A)

2、(D)是面向对象程序设计语言中的一种机制,这种机制实现了方法的定义与具体的对象无关,而方法的调用则可以关联于具体的对象

场景:动物园表演

1. 没有多态(静态绑定)的代码

2. 使用多态(动态绑定)的代码

动态绑定的核心机制解析

3、关于面向对象设计中的继承和组合,下面说法错误的是(C)

4、以下关于纯虚函数的说法,正确的是(A)

5、关于虚函数的描述正确的是(B)

6、关于虚表的说法正确的是(D)

7、假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则(D)

8、下面程序输出结果是什么?(A)

9、下面说法正确的是?(C)

10、以下程序输出结果是什么?(B)(超重点!!!易错!!!)

逐步执行过程分析

1. 对象创建和内存布局

2. 函数调用

3. 在A::test()中

4. 关键点:默认参数的绑定时机

详细解析(编译期发生的事情)

运行期发生的事情

为什么输出"B->1"而不是"B->0"?(重点!!!)

二、问答题解析

1、什么是多态?

2、什么是重载、重写(覆盖)、重定义(隐藏)?

3、多态的实现原理?

4、inline函数可以是虚函数吗?

5、静态成员函数可以是虚函数吗?

6、构造函数可以是虚函数吗?(注意!!!)

7、析构函数可以是虚函数吗?什么场景下析构函数是虚函数?(注意!!!)

8、对象访问普通函数快还是虚函数更快?

9、虚函数表是在什么阶段生成的?存在哪的?

10、C++菱形继承的问题?虚继承的原理?

菱形继承问题:

虚继承原理:

11、什么是抽象类?抽象类的作用?

(2) 派生类 Circle 和 Rectangle


一、概念考察题

1、下面哪种面向对象的方法可以让你变得富有(A)

A. 继承 B. 封装 C. 多态 D. 抽象

解析:这是一道趣味题。继承在面向对象编程中允许子类获得父类的属性和方法,体现了"继承财富"的概念。其他选项虽然是重要的面向对象特性,但不直接体现"变得富有"的含义。

2、(D)是面向对象程序设计语言中的一种机制,这种机制实现了方法的定义与具体的对象无关,而方法的调用则可以关联于具体的对象

A. 继承 B. 模板 C. 对象的自身引用 D. 动态绑定

解析:动态绑定(后期绑定)是多态的核心机制,它允许在运行时根据对象的实际类型来确定调用哪个方法,实现了方法定义与具体对象的分离。

场景:动物园表演

假设有一个动物园,不同动物会发出不同的叫声。

1. 没有多态(静态绑定)的代码
#include <iostream>
using namespace std;class Animal {
public:void makeSound() {cout << "Some generic animal sound" << endl;}
};class Dog : public Animal {
public:void makeSound() {cout << "Woof! Woof!" << endl;}
};class Cat : public Animal {
public:void makeSound() {cout << "Meow! Meow!" << endl;}
};int main() {Animal* myPet = new Dog();  // 父类指针指向子类对象myPet->makeSound();  // 输出:Some generic animal sounddelete myPet;return 0;
}

问题:即使myPet实际指向的是Dog对象,调用的仍然是AnimalmakeSound方法。这不是我们想要的行为。

2. 使用多态(动态绑定)的代码
#include <iostream>
using namespace std;class Animal {
public:virtual void makeSound() {  // 关键:加上 virtualcout << "Some generic animal sound" << endl;}
};class Dog : public Animal {
public:void makeSound() override {  // 重写虚函数cout << "Woof! Woof!" << endl;}
};class Cat : public Animal {
public:void makeSound() override {  // 重写虚函数cout << "Meow! Meow!" << endl;}
};int main() {Animal* myPet = new Dog();  // 父类指针指向子类对象myPet->makeSound();  // 输出:Woof! Woof!// 动态绑定的威力:同一段代码,不同行为myPet = new Cat();myPet->makeSound();  // 输出:Meow! Meow!delete myPet;return 0;
}

动态绑定的核心机制解析

  1. 编译时:编译器看到myPet->makeSound()时,只知道myPetAnimal*类型

  2. 运行时:程序会:

    • 查看myPet实际指向的对象(是Dog还是Cat?)

    • 通过对象的虚函数表找到正确的makeSound函数地址

    • 调用该地址对应的函数

这就是"动态绑定":函数调用与具体实现的绑定不是在编译时确定的,而是在程序运行时根据对象的实际类型动态决定的。

3、关于面向对象设计中的继承和组合,下面说法错误的是(C)

A. 继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用。
B. 组合的对象不需要关系各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用。
C. 优先使用继承,而不是组合,是面向对象设计的第二原则。
D. 继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现。

解析:面向对象设计原则实际上是优先使用组合而非继承,因为组合提供更好的封装性和灵活性。继承是一种强耦合关系,而组合是弱耦合关系。

4、以下关于纯虚函数的说法,正确的是(A)

A. 声明纯虚函数的类不能实例化对象

B. 声明纯虚函数的类是虚基类

C. 子类必须实现基类的纯虚函数

D. 纯虚函数必须是空函数

解析:含有纯虚函数的类称为抽象类,不能直接实例化对象。B错误:虚基类是针对菱形继承问题的概念;C错误:子类如果不实现所有纯虚函数,它仍然是抽象类;D错误:纯虚函数可以有实现,但必须被重写。

B选项:

#include <iostream>
using namespace std;// 基类A
class A {
public:int data;A() : data(100) {cout << "A constructed" << endl;}
};// B虚继承A - 关键:加上virtual
class B : virtual public A { // 虚继承
public:B() { cout << "B constructed" << endl; }
};// C虚继承A - 关键:加上virtual  
class C : virtual public A { // 虚继承
public:C() { cout << "C constructed" << endl; }
};// D同时继承B和C
class D : public B, public C {
public:D() { cout << "D constructed" << endl; }
};int main() {D d;cout << "Size of D: " << sizeof(d) << endl; // 比之前大,因为包含虚基表指针// 问题解决:d.data = 200;           // 没有二义性了!cout << d.data << endl; // 输出200// 通过不同路径访问的是同一个datad.B::data = 500;cout << "Through C: " << d.C::data << endl; // 输出500,是同一个数据return 0;
}

A是虚基类(被虚继承的类)、B和C是虚继承A的类、D是最终派生类(继承B和C)

C选项:

#include <iostream>
using namespace std;// 抽象类 - 包含纯虚函数
class Animal {
public:virtual void makeSound() = 0;  // 纯虚函数virtual void eat() = 0;        // 另一个纯虚函数
};// Dog类只实现了其中一个纯虚函数
class Dog : public Animal {
public:void makeSound() override {    // 只实现了一个cout << "Woof! Woof!" << endl;}// 没有实现eat()函数
};// Cat类实现了所有纯虚函数
class Cat : public Animal {
public:void makeSound() override {cout << "Meow! Meow!" << endl;}void eat() override {          // 实现了第二个cout << "Eating fish..." << endl;}
};int main() {// Animal animal;  // 错误:不能实例化抽象类// Dog dog;       // 错误:Dog仍然是抽象类,因为没有实现eat()Cat cat;          // 正确:Cat实现了所有纯虚函数,不是抽象类cat.makeSound();cat.eat();return 0;
}

关键理解

  1. 纯虚函数使用= 0语法声明

  2. 抽象类:包含至少一个纯虚函数的类

  3. 继承规则

    • 如果子类没有实现所有纯虚函数 → 子类仍然是抽象类

    • 如果子类实现了所有纯虚函数 → 子类可以实例化对象

现实比喻

  • 抽象类就像一份工作任务清单(纯虚函数)

  • 如果你只完成了部分任务 → 还是"未完成状态"(抽象类)

  • 只有完成所有任务 → 才能"交付使用"(可实例化)

5、关于虚函数的描述正确的是(B)

A. 派生类的虚函数与基类的虚函数具有不同的参数个数和类型
B. 内联函数不能是虚函数
C. 派生类必须重新定义基类的虚函数
D. 虚函数可以是一个static型的函数

解析:A错误:虚函数要求相同的函数签名(协变返回值除外);C错误:派生类可以不重写基类的虚函数;D错误:虚函数不能是static的,因为static函数没有this指针。

为什么虚函数不能是static的?this指针的缺失

  • 虚函数调用需要this指针来访问对象的虚表指针(vptr)

  • static函数没有this指针,无法访问对象的虚表

// 虚函数调用背后的机制(简化理解)
animal->makeSound();
// 实际上相当于:
// 1. 通过this指针找到vptr → *(this + 0)
// 2. 通过vptr找到虚表 → vtable
// 3. 在虚表中找到函数地址 → vtable[0]
// 4. 调用函数 → (this->*(vtable[0]))()// static函数调用:
Animal::staticSound();
// 直接调用函数地址,不需要this指针

对象关联性

  • 虚函数:与对象实例关联

  • static函数:与关联

为什么内联函数不能是虚函数?

编译时 vs 运行时

  • 内联:编译时决定,在调用处展开代码

  • 虚函数:运行时决定,通过虚表动态绑定

地址要求冲突

  • 内联函数:不应该有地址(因为代码被展开)

  • 虚函数:必须有地址(要放入虚表)

编译器行为

  • 当虚函数被声明为inline时,编译器会忽略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类使用的不是同一张虚表

解析:B类继承A类并重写虚函数,B类会有自己的虚表,其中包含重写后的函数地址。虚函数个数相同但内容不同。A类对象和B类对象前4个字节存储的是各自的虚表地址。关于虚表和虚基表,忘记对应的知识点的话就翻回去看看:

特性虚表(vtable)虚基表(vbtable)
目的实现多态(动态绑定)解决菱形继承(共享基类)
内容虚函数地址到虚基类的偏移量
触发条件类中有虚函数类虚继承其他类
指针名称vptr(虚表指针)vbptr(虚基表指针)
使用时机虚函数调用时访问虚基类成员时

8、下面程序输出结果是什么?(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

解析:这是虚继承的初始化顺序问题。虚继承确保A只被初始化一次,初始化顺序为:虚基类A → 直接基类B → 直接基类C → 派生类D。

虚继承的初始化规则

  1. 虚基类由最底层派生类直接初始化

  2. 中间类对虚基类的初始化调用会被忽略

  3. 确保虚基类只被初始化一次

初始化顺序:虚基类(A)→ 2. 直接基类(B, C)→ 3. 派生类自身(D)

如果去掉A的初始化会怎样?如果从D的初始化列表中移除A(s1),会出现编译错误:

error: no matching function for call to 'A::A()'

因为编译器要求最底层派生类必须初始化虚基类。

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;
}

C. p1 == p3 != p2

解析:在多继承中,派生类对象包含多个基类子对象。Base1是第一个基类,与派生类地址相同;Base2是第二个基类,地址会有偏移。发生了切片机制!!!

Derive对象内存布局:

10、以下程序输出结果是什么?(B)(超重点!!!易错!!!)

#include <iostream>
using namespace std;class A {
public:virtual void func(int val = 1) {cout << "A->" << val << endl;}virtual void test() {func();}
};class B : public A {
public:void func(int val = 0) {cout << "B->" << val << endl;}
};int main() {B* p = new B;p->test();return 0;
}

B. B->1

解析虽然B重写了func函数,但test函数在A中定义,使用的是A的默认参数值(编译期确定),而函数调用通过虚表找到B的func实现(运行期确定)。

逐步执行过程分析

1. 对象创建和内存布局
B* p = new B;
  • 创建B类对象

  • 对象中包含虚表指针(vptr),指向B的虚表

  • B的虚表内容:

    • func(): 指向B::func(int)

    • test(): 指向A::test()(B没有重写test)

2. 函数调用
p->test();
  • test()不是虚函数,但通过虚表找到A::test()

  • 调用A::test()函数

3. 在A::test()中
void test() {func();  // 这里发生多态调用!
}
  • func()是虚函数调用

  • 通过虚机制查找实际对象的虚表

  • 发现p实际指向B对象,所以调用B::func(int)

4. 关键点:默认参数的绑定时机
// A类中定义
virtual void func(int val = 1) { ... }// B类中重写
void func(int val = 0) { ... }  // 默认参数改为0

重要规则:默认参数是编译期静态绑定的,而虚函数是运行期动态绑定的。

详细解析(编译期发生的事情)

编译A::test()时

void test() {func();  // 编译器看到这里调用func()
}
  • 编译器知道test()是A的成员函数

  • 在A的作用域中查找func()的声明

  • 找到A::func(int val = 1)

  • 编译期将调用展开为:func(1)(插入默认参数1)

生成的实际代码

void A::test() {this->func(1);  // 默认参数在编译期确定!
}

运行期发生的事情

虚函数机制启动

this->func(1);  // this实际指向B对象
  • 通过虚表找到实际要调用的函数:B::func(int)

  • 调用B::func(1) ← 参数1已经在编译期确定

B::func执行

void B::func(int val = 0) {  // 这里的默认参数0根本没用上!cout << "B->" << val << endl;  // val的值是1
}
  • 虚函数的重写关系在编译阶段确定,但具体调用哪个重写函数是在运行阶段根据对象实际类型决定的这里的0用不上,因为此时已经有了1这个默认参数。

为什么输出"B->1"而不是"B->0"?(重点!!!)

阶段动作结果
编译期编译A::test()func()展开为func(1)
编译期查找func声明找到A::func(int val = 1)
运行期多态调用实际调用B::func(int)
运行期参数传递传递编译期确定的参数值1

二、问答题解析

1、什么是多态?

多态是指同一操作作用于不同的对象,可以有不同的解释和执行结果。在C++中分为:

  • 静态多态(编译期):函数重载、模板

  • 动态多态(运行期):通过虚函数机制实现,允许基类指针或引用根据实际对象类型调用相应方法

#include <iostream>
using namespace std;class Vehicle {
public:virtual void start() {cout << "Vehicle starting" << endl;}
};class Car : public Vehicle {
public:void start() override {cout << "Car engine starting" << endl;}
};class Bike : public Vehicle {
public:void start() override {cout << "Bike pedaling" << endl;}
};int main() {Car car;Bike bike;// 使用基类指针Vehicle* vehiclePtr = &car;vehiclePtr->start();  // 输出: Car engine startingvehiclePtr = &bike;vehiclePtr->start();  // 输出: Bike pedaling// 使用基类引用Vehicle& vehicleRef = car;vehicleRef.start();   // 输出: Car engine startingreturn 0;
}

2、什么是重载、重写(覆盖)、重定义(隐藏)?

  • 重载:同一作用域中,函数名相同但参数列表不同(参数类型、数量或顺序)

  • 重写/覆盖:派生类中重新定义基类的虚函数,要求函数名、参数列表和返回类型都相同(协变返回类型除外)

  • 重定义/隐藏:派生类中定义与基类同名的函数(无论参数是否相同),隐藏基类中的同名函数

3、多态的实现原理?

通过虚函数表(vtable)机制实现:

  • 每个含有虚函数的类都有一个虚函数表

  • 每个对象包含一个指向虚函数表的指针(vptr)

  • 派生类继承基类的虚函数表并重写虚函数地址

  • 运行时通过vptr找到正确的函数实现

继承基类的vtable:派生类的对象中会包含一个指向虚函数表的指针(通常位于对象内存布局的开头)。派生类复用基类的vtable结构,但会修改其中被重写的虚函数的地址。

重写虚函数地址:当派生类重写基类的虚函数时,编译器会将派生类中对应的虚函数地址写入基类vtable中相应的位置

如果派生类没有重写基类的虚函数,那么派生类的虚函数表(vtable)中对应的虚函数地址会直接继承基类的实现,即派生类和基类的虚函数地址相同。

4、inline函数可以是虚函数吗?

        可以,但编译器会忽略inline属性。因为虚函数需要通过虚表进行动态绑定,必须有实际地址,而内联函数在调用处展开,不需要地址。当函数被声明为虚函数时,inline关键字只是一个建议,编译器通常不会内联虚函数。

5、静态成员函数可以是虚函数吗?

不能。原因:

  • 静态成员函数没有this指针,无法访问对象的虚函数表

  • 虚函数机制依赖于对象实例,而静态函数属于类而非对象

  • 静态函数调用不通过对象实例,无法实现动态绑定

6、构造函数可以是虚函数吗?(注意!!!)

不能。原因:

  • 虚函数调用需要通过虚表,但对象在构造前还没有初始化vptr

  • 构造函数的作用是创建对象,在运行时对象类型已知,不需要动态绑定

  • 语法上不允许声明虚构造函数

7、析构函数可以是虚函数吗?什么场景下析构函数是虚函数?(注意!!!)

可以且应该为虚函数的情况:

  • 当类可能被继承,且会通过基类指针删除派生类对象时

  • 如果基类析构函数不是虚函数,通过基类指针删除派生类对象会导致派生类的析构函数不被调用,造成资源泄漏

  • 一般来说,只要有虚函数的类,析构函数就应该声明为虚函数

8、对象访问普通函数快还是虚函数更快?

访问普通函数更快,因为:

  • 普通函数调用是直接调用,地址在编译期确定

  • 虚函数需要间接调用:先通过vptr找到虚表,再找到函数地址,多一次寻址操作

  • 虚函数调用无法内联优化,而普通函数可能被内联

9、虚函数表是在什么阶段生成的?存在哪的?

  • 生成阶段:编译期间生成,构造函数运行期间初始化vptr

  • 存储位置:通常存储在只读数据段(常量区),编译器相关

  • 虚表在程序加载时已存在,对象构造时vptr被赋值为虚表地址

  • 虚表中的虚函数是在运行阶段被编译器决定调用哪个,即当前对象的虚函数重写还是它的基类的虚函数。

10、C++菱形继承的问题?虚继承的原理?

菱形继承问题

class A { int data; };
class B : public A {};
class C : public A {};
class D : public B, public C {}; // 有两份A的成员,导致二义性

虚继承原理

  • 使用virtual关键字继承(一般在中间部分加virtual关键字):class B : virtual public A;

  • 虚基类在派生类中只保留一份实例,这个实例要在底层子类显式实例化(即在D中显式实例化A,如没有则报错)

  • 通过虚基表指针找到虚基类子对象的位置

  • 解决数据冗余和二义性问题,但增加访问开销

11、什么是抽象类?抽象类的作用?

抽象类:含有纯虚函数的类,不能实例化对象

class AbstractClass {
public:virtual void pureVirtualFunction() = 0; // 纯虚函数virtual void normalVirtualFunction() {} // 普通虚函数(可选)
};
  • 纯虚函数:没有默认实现,派生类必须重写它,否则派生类也会变成抽象类。
  • 抽象类:包含纯虚函数的类就是抽象类,不能直接 new AbstractClass()

作用

  • 定义接口规范,强制派生类实现特定方法

  • 实现接口与实现分离,提高代码可扩展性

  • 表示抽象概念,反映现实世界中的抽象实体

  • 作为多态的基础,允许通过基类接口操作派生类对象

设计意义:抽象类体现了"依赖倒置"原则,高层模块不应该依赖低层模块,二者都应该依赖抽象接口,从而提高系统的灵活性和可维护性。

场景:定义一个 Shape 抽象类,要求所有派生类(如 CircleRectangle)必须实现 draw() 方法。(基于这个性质:没有默认实现,派生类必须重写它,否则派生类也会变成抽象类。)

(1) 定义抽象类 Shape

#include <iostream>
using namespace std;class Shape { // 抽象类
public:virtual void draw() = 0; // 纯虚函数,强制派生类实现virtual void move(int x, int y) { // 普通虚函数,可以有默认实现cout << "Shape moved to (" << x << ", " << y << ")" << endl;}
};
(2) 派生类 Circle 和 Rectangle
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;}
};
http://www.dtcms.com/a/390181.html

相关文章:

  • 博士生如何进行文献阅读和文献整理?
  • 矩阵分析线性表示例题
  • OpenEuler---jumpserver堡垒机部署
  • STM32 驱动 MAX31865 读取 PT100 温度方案
  • 第四次编程记录
  • 2024年7月 自旋散射效应
  • 理解神经网络中的批量数据处理:维度、矩阵乘法与广播机制
  • UDP传输大数据?真的能兼顾速度和可靠性吗?
  • 某税网登录逆向-sm2-HMacSHA256-sm4-滑块
  • HashMap 添加元素put()的源码和扩容方法resize()的源码解析
  • Windows系统如何查看SSH公钥?
  • 苹果软件代码混淆与多框架应用加固 iOS混淆、ipa文件安全、跨端应用安全防护全流程指南
  • 第一章 神经网络的复习:神经网络的推理
  • MinIO 4 节点集群部署实战:RPM 安装 + mc 工具攻略(网站托管、自动备份)
  • 支持向量机 SVM 预测人脸数据集时数据是否标准化的对比差异
  • 学习笔记:Vue 透传
  • 【记录59】携带token加载图片、图片过大自行压缩、转base64、
  • CentOS 7下FTP配置全攻略
  • 利用Debezium和PostgreSQL逻辑复制实现实时数据同步架构设计与优化实践
  • Part05 数学与其他
  • 链接脚本总结
  • 模电基础:基本放大电路及其优化
  • Curl、Wget 等命令 Uses proxy env variable https_proxy 如何解决
  • 自注意力机制Self-Attention (一)
  • (论文速读)DeNVeR(可变形神经血管表示)-X射线血管造影视频的无监督血管分割
  • css实现3D变化之两面翻转的盒子效果
  • 多项式回归原理与实战:从线性扩展到非线性建模
  • 【层面二】.NET 运行时与内存管理-01(CLR/内存管理)
  • 【51单片机】【protues仿真】基于51单片机温度检测数码管系统
  • Sketch安装图文教程:从下载到账号注册完整流程