C++面试锦囊-面向对象篇

😉本文主要总结了一些常见的C++面试题-面向对象相关知识点,欢迎大家前来学习指正,我也会不定期去更新相关内容。

面向对象

面向对象(OOP)的核心特性:封装继承多态

封装:将数据(成员变量)和操作数据的方法(成员函数)封装打包成一个类

  • 数据隐藏:将类的数据(成员变量)私有化(private),禁止外部直接访问,只有通过公有(public)的方法间接访问操作。
  • 接口暴露:提供公有方法(如get()、set()等)控制对私有成员的访问和修改
  • 实现隔离:类内部的实现可以随意修改,只要接口保持不变,就不会对外影响代码

继承:允许一个类子类(派生类),基于父类(基类)构建,从而复用父类的方法和属性;子类可以扩展or修改父类的方法,同时保留父类的功能。

  • 单继承:子类仅继承一个父类
  • 多继承:子类可以继承多个父类
  • 多层继承:子类继承的父类本身也是另一个类的子类,形成继承链

多态

  • 含义:同一个接口,多种实现
  • 编译时多态(静态多态):函数重载、模板实现
  • 运行时多态(动态多态):通过基类指针或引用,调用子类的虚函数实现
  • 调用关系:this-> vptr->vtable->virtual function

重载、重写、隐藏

重载(overload):同一个作用域内,多个函数的 函数名相关,但是参数列表不同 <编译阶段:静态绑定>
重写(override):也成为覆盖,子类重写基类的虚函数 <运行阶段:动态绑定>
隐藏(Hiding):子类中定义了与父类同名参数列表不同的函数,导致基类函数不可见 <编译阶段:静态绑定>

重载(overload)重写(override)隐藏(Hiding)
作用域同一个作用域继承关系的 父子类之间继承关系的 父子类之间
函数名必须相同必须相同必须相同
参数列表不同必须相同无要求
虚函数无要求父类必须为虚函数无要求
返回值无要求必须相同无要求
绑定时机编译器,静态绑定运行期,动态绑定编译器,静态绑定
调用规则根据实参类型匹配相应的函数根据对象类型调用子类函数隐藏父类同名函数

多态

静态多态(编译期间):函数重载,模板

  • 效率高但是不灵活

动态多态(运行期间):虚函数

  • 灵活性高,但是有性能开销

虚函数:

virtual修饰的成员函数

虚函数的调用流程:

  • 编译期间:确认函数在虚函数表中的偏移
  • 运行期间:
    • 通过指针找到对象的虚函数表指针vptr
    • 通过虚函数表指针vptr找到对象的虚函数表
    • 根据偏移量找到实际的虚函数
    • 正确的执行函数

虚函数的实现原理:

虚函数是C++实现运行时多态和核心机制,其底层通过虚函数表虚函数表指针实现:
虚函数表vtable:编译器生成的静态的函数指针数组,存放类的所有的虚函数地址;

  • 同一个类的所有对象共用一个虚函数表`(静态数组)
  • 子类会继承父类的虚函数表,并根据自身是否重写虚函数进行更新(如果子类的虚函数重写了父类的虚函数,则对应在虚函数表中会把对应的虚函数替换成子类的函数的地址)

虚函数表指针vptr:每一个包含虚函数的类的对象,都拥有一个虚函数表指针,指向该对象所属类的虚函数表

  • 通常vptr,在每个类对象的前4/8字节(32位/84位系统)
  • 每个vptr在构造时初始化,指向所属类的虚函数表
  • 当继承和多态发生时,vptr会动态更新指向派生类的虚函数表
  • 在多继承环境下,会存在多个虚函数表指针,分别指向对应的 不同基类的虚函数表

总结:
①每个包含虚函数的类需要维护一张虚函数表
②每个包含虚函数的类的对象要额外存储一个虚函数指针(vptr),通常为4/8字节
③虚函数的调用比普通函数多2次内存访问:通过虚函数指针vptr虚函数表vtable,通过虚函数表vtable虚函数地址

构造/析构函数中的虚函数

<1>在构造函数中调用虚函数,不会触发多态,因此此时子类部分尚未构造。
构造的顺序是:父类构造->子类构造
类的对象构造时,先调用基类构造,此时对象的vptr指向基类的虚函数表,因此构造中的虚函数调用不会触发子类的重写版本。
<2>在析构函数中调用虚函数,也不会触发多态,因为子类已经析构。

  • 析构时vptr逐级降级:析构中调用虚函数时,对象的虚函数指针vptr,指向当前正在执行析构函数的类的虚函数表,而非其子类的虚函数表。
  • 子类析构阶段:执行子类的析构函数,子类的资源被释放,vptr仍然指向子类的虚函数表,此时调虚函数会调子类的实现(若有重写)
  • 父类析构阶段:执行父类的析构函数,vptr会立即更新为指向父类的虚函数表,调用虚函数会触发父类的实现。

纯虚函数和抽象类

纯虚函数:纯虚函数在虚函数表中用特殊标记表示,通常为nullptr
抽象类:抽象类的虚函数表,包含纯虚函数的标记,其实例化会导致编译错误,因此纯虚函数无法实例化,必须继承后才能实例化。

菱形继承 & 虚继承

菱形继承:

class Person {
public:
    int age;
    void eat() { cout << "Person eats" << endl; }
};

class Student : public Person {
public:
    int studentId;
};

class Employee : public Person {
public:
    int employeeId;
};

class WorkingStudent : public Student, public Employee {
public:
    int projectId;
};


// 内存布局
WorkingStudent对象
┌─────────────────────┐
│ Student::Person::age  │  // 第一份age
├─────────────────────┤
│ studentId           │
├─────────────────────┤
│ Employee::Person::age │  // 第二份age
├─────────────────────┤
│ employeeId          │
├─────────────────────┤
│ projectId           │
└─────────────────────┘

// 二义性问题
WorkingStudent ws;
ws.age = 25;  // 错误:二义性,不知道是Student::Person::age还是Employee::Person::age
ws.eat();     // 错误:二义性,不知道是Student::Person::eat()还是Employee::Person::eat()

虚继承

虚继承,通过虚基类表(VBTable)和虚基类指针(VBptr)实现,更复杂,但是可以解决菱形继承的问题
虚基类表:VBTable

  • 每个虚基类的子类维护一个VBTable,存储在该子类到公共基类的偏移量
  • 运行时通过VBptr和VBTable,计算公共基类的实际地址
  • 空间开销
    • 每个虚继承的子类增加一个 VBPtr(通常 4/8 字节)
    • 公共基类的成员被集中存储,可能导致内存布局不紧凑。
  • 时间开销:
    • 访问公共基类成员时,需要通过 VBPtr 和 VBTable 计算偏移量,增加一次内存访问
class Person {
public:
    int age;
    void eat() { cout << "Person eats" << endl; }
};

class Student : virtual public Person {  // 虚继承
public:
    int studentId;
};

class Employee : virtual public Person {  // 虚继承
public:
    int employeeId;
};

class WorkingStudent : public Student, public Employee {
public:
    int projectId;
};


// 内存布局
WorkingStudent对象
┌─────────────────────┐
│ Student::VBPtr       │  // 指向Student的虚基类表
├─────────────────────┤
│ studentId           │
├─────────────────────┤
│ Employee::VBPtr      │  // 指向Employee的虚基类表
├─────────────────────┤
│ employeeId          │
├─────────────────────┤
│ projectId           │
├─────────────────────┤
│ Person::age          │  // 唯一一份age
└─────────────────────┘

// 解决二义性
WorkingStudent ws;
ws.age = 25;  // 正确:唯一一份age
ws.eat();     // 正确:唯一一份eat()

虚指针初始化:

由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要的。换句话说,在虚表指针没有正确初始化之前,我们不能够去调用虚函数。
那么虚表指针在什么时候,或者说在什么地方初始化呢?
答案是在构造函数中进行虚表的创建和虚表指针的初始化。
构造函数的调用顺序,在构造子类对象时,要先调用父类的构造函数,之后再完成子类的构造。在调用父类的构造函数时,编译器只“看到了”父类,并不知道后面是否后还有继承者,它初始化虚表指针,将该虚表指针指向父类的虚表。当执行子类的构造函数时,虚表指针被重新赋值,指向自身的虚表。

C++多态性实现的原理
对于虚函数调用来说,每一个对象内部都有一个虚表指针,该虚表指针被初始化为指向本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以才能实现动态的对象函数调用。

虚函数、构造函数、析构函数、的一些问题

构造函数析构函数虚函数
作用初始化对象成员释放对象资源实现运行时多态
是否可继承是(基类析构需为虚函数)
是否可重载是(参数列表不同)否(唯一,无参数)是(派生类重写)
是否可 virtual否(构造时 VPTR 未初始化)是(基类必须为虚)
是否可 inline是(隐式或显式)是(虚析构时动态绑定忽略)是(但动态绑定时忽略)
是否可 static

构造可以是virtual吗?析构函数呢?

  • 构造函数:构造函数不能使用virtual修饰,因为构造函数执行时,vptr尚未初始化,无法实现虚函数的动态绑定。且无法通过基类指针调用派生类的构造(构造函数不能继承),virtual修饰构造存在语义矛盾。
  • 析构函数:基类的析构函数,应该声明为虚函数
  • 构造函数中调用虚函数:因为构造函数调用期间,此虚函数调用不会触发派生类的重写,因此是调用的当前类的构造

哪些函数不能是虚函数:

(1)构造函数

  • 有虚函数的类都有一个虚函数表,每个类对象都有一个虚函数表指针,虚函数表指针在构造函数中初始化。
  • 在构造函数执行时,vptr尚未初始化,无法实现虚函数的动态绑定。
  • 无法通过基类指针或引用指向子类对象的形式调用子类的构造(构造函数不能继承),virtual修饰构造存在语义矛盾

(2)内联函数

  • inline修饰的内联函数表示在编译期间展开;而虚函数意味着在运行期间进行类型确定。

(3)静态函数

  • 静态函数不属于任何对象,属于类;静态成员函数没this指针,因为设置为虚函数没意义

(4)友元函数

  • 友元函数不属于类的成员函数,不能被继承;对于没有继承性的函数没有虚函数的意义

(5)普通函数

  • 普通成员函数不属于类的成员函数,不能被继承;对于没有继承性的函数没有虚函数的意义

构造函数、析构函数可以抛异常吗?

  • C++只会析构已经完成的对象,如果在构造函数中抛出异常,控制权转出构造函数之外,因为构造并未完成,因此对象析构函数也不会被调用,因此会产生内存泄漏
  • 析构函数不应该抛出异常:当对象被异常销毁(如栈展开)时,若析构函数抛出新异常,会导致两个异常同时存在,从而导致程序直接调用std::terminate终止
函数类型是否允许抛出异常异常处理要点
构造函数允许- 确保部分构造的对象正确释放资源(利用 RAII)
- 异常应被上层调用者捕获处理
析构函数不推荐- 必须保证无异常抛出(通过 noexcept 或内部处理)
- 资源释放逻辑应简单可靠
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值