继承和多态常见面试问题解析
目录
一、概念考察题
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
对象,调用的仍然是Animal
的makeSound
方法。这不是我们想要的行为。
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;
}
动态绑定的核心机制解析
-
编译时:编译器看到
myPet->makeSound()
时,只知道myPet
是Animal*
类型 -
运行时:程序会:
-
查看
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;
}
关键理解
-
纯虚函数使用
= 0
语法声明 -
抽象类:包含至少一个纯虚函数的类
-
继承规则:
-
如果子类没有实现所有纯虚函数 → 子类仍然是抽象类
-
如果子类实现了所有纯虚函数 → 子类可以实例化对象
-
现实比喻
-
抽象类就像一份工作任务清单(纯虚函数)
-
如果你只完成了部分任务 → 还是"未完成状态"(抽象类)
-
只有完成所有任务 → 才能"交付使用"(可实例化)
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。
虚继承的初始化规则:
-
虚基类由最底层派生类直接初始化
-
中间类对虚基类的初始化调用会被忽略
-
确保虚基类只被初始化一次
初始化顺序:虚基类(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
抽象类,要求所有派生类(如 Circle
、Rectangle
)必须实现 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;}
};