C++面试题汇总(下)

1、当一个子类中有对象成员时,他们的构造和析构顺序是怎样的?

首先是构造函数调用顺序:

  1. 父类的构造函数:先调用直接父类的构造函数(如果有多层继承,则从最顶层基类开始依次向下)。
  2. 对象成员的构造函数:按照对象成员在子类中声明的顺序依次调用(与初始化列表中的顺序无关)。
  3. 子类自己的构造函数:最后调用子类的构造函数体。

再是析构函数:

  1. 子类自己的析构函数:先调用子类的析构函数体。
  2. 对象成员的析构函数:按照对象成员构造顺序的逆序依次调用。
  3. 父类的析构函数:最后调用父类的析构函数(如果有多层继承,则从最底层子类的直接父类开始依次向上)。

观察可以发现,析构和构造的顺序是完全相反的。

#include <iostream>
using namespace std;

// 父类
class Parent {
public:
    Parent() { cout << "Parent 构造函数" << endl; }
    ~Parent() { cout << "Parent 析构函数" << endl; }
};

// 对象成员类A
class A {
public:
    A() { cout << "A 构造函数" << endl; }
    ~A() { cout << "A 析构函数" << endl; }
};

// 对象成员类B
class B {
public:
    B() { cout << "B 构造函数" << endl; }
    ~B() { cout << "B 析构函数" << endl; }
};

// 子类,继承Parent,包含A和B的对象成员
class Child : public Parent {
private:
    A a;  // 声明顺序:A在前
    B b;  // 声明顺序:B在后
public:
    Child() { cout << "Child 构造函数" << endl; }
    ~Child() { cout << "Child 析构函数" << endl; }
};

int main() {
    Child c;  // 创建子类对象
    return 0;
}

输出结果:

Parent 构造函数  // 1. 父类构造
A 构造函数      // 2. 对象成员A构造(按声明顺序)
B 构造函数      // 3. 对象成员B构造(按声明顺序)
Child 构造函数   // 4. 子类自身构造

// 析构阶段(与构造顺序相反)
Child 析构函数   // 1. 子类自身析构
B 析构函数      // 2. 对象成员B析构(逆序)
A 析构函数      // 3. 对象成员A析构(逆序)
Parent 析构函数  // 4. 父类析构

2、继承中的同名成员是如何处理的?

同名成员当然包括成员变量和成员函数,首先是成员变量:

  • 隐藏规则:子类的同名成员变量会隐藏父类的同名成员变量(而非覆盖)。
  • 访问方式:
    • 直接访问时,默认访问的是子类自己的成员变量
    • 若要访问父类的同名成员,需使用作用域解析符 ::父类名::成员变量)。

接下来就是成员函数:

  • 区分重载与隐藏:

    • 若子类与父类的同名函数参数列表不同:父类的函数会被隐藏(即使符合重载条件,也不会形成重载)。
    • 若子类与父类的同名函数参数列表相同且父类函数非虚函数:父类的函数会被隐藏
    • 若子类与父类的同名函数参数列表相同且父类函数是虚函数:形成多态(覆盖 / 重写),此时父类函数被覆盖。
  • 访问方式:

    • 直接调用时,默认调用子类的函数
    • 若要调用父类的同名函数,需使用作用域解析符 ::父类名::函数名)。

3、什么是菱形继承?菱形继承存在哪些问题?如何解决?

如图所示可知菱形继承就是一个派生类同时继承自两个基类,而这两个基类又共同继承自同一个基类。

菱形继承会导致数据冗余二义性问题:

  • 数据冗余

  • 派生类 D 会间接包含两份基类 A 的成员(一份来自 B,一份来自 C),造成内存浪费。

  • 访问二义性

  • 当访问基类 A 的成员时,编译器无法确定应该访问从 B 继承的版本还是从 C 继承的版本,导致编译错误。

而解决方法就是采用虚继承,C++ 引入虚继承机制,使得派生类在继承间接基类时,只保留一份间接基类的成员,从而解决菱形继承的问题。

4、虚继承的原理?

虚继承通过引入 虚基类指针(virtual base pointer)虚基类表(virtual base table) 实现,具体机制如下:

  • 单一实例保证

  • 当使用 virtual 关键字声明继承(如 class B : virtual public A)时,派生类(B、C)不会直接包含基类(A)的成员,而是在最终派生类(D)中只保留一份基类(A)的实例

  • 虚基类指针(vbptr)

  • 中间派生类(B、C)会生成一个隐藏的指针成员(vbptr),指向虚基类表

  • 虚基类表(vbtable)

  • 虚基类表是一个存储偏移量的数组,记录了从当前类(B 或 C)的内存地址到共享基类(A)实例的偏移量。通过这个偏移量,中间类(B、C)可以找到唯一的基类(A)实例。

可以使用VS开发命令工具查看虚基类指针(vbptr)和虚基类表(vbtable)
示例:cl /dl reportSingleClassLayoutMule MainTest. cpp
reportSingleClassLayout是展示单个类 Mule就是要查看的类,MainTest. cpp就是类所在的cpp文件。

5、c++如何实现动态绑定?

要实现动态绑定,必须同时满足以下条件:

  1. 继承关系:存在基类和派生类的继承结构;
  2. 虚函数重写:派生类重写(覆盖)基类中的虚函数(函数名、参数列表、返回值必须完全一致,协变返回值除外);
  3. 基类指针 / 引用:通过基类指针或引用调用虚函数。

当通过基类指针 / 引用调用虚函数时:

  1. 编译器通过基类指针 / 引用找到对象的 vptr;
  2. 通过 vptr 访问该对象所属类的 vtable;
  3. 在 vtable 中找到对应虚函数的地址并调用。

这个过程在运行时完成,因此能根据对象的实际类型(而非指针 / 引用的静态类型)调用正确的函数版本。

6、虚函数的原理?

  • 动态绑定的条件:必须通过基类指针 / 引用调用虚函数,且派生类需重写该虚函数。
  • 内存开销:每个对象增加一个 vptr 的内存开销(通常为 4/8 字节),每个类增加一个 vtable 的全局内存开销(与虚函数数量成正比)。
  • 构造函数不能是虚函数:因为对象创建时 vptr 尚未初始化完成,无法指向正确的 vtable。
  • 析构函数建议设为虚函数:确保通过基类指针删除派生类对象时,能正确调用派生类的析构函数(避免内存泄漏)。

虚函数的核心原理是通过 vtable 存储虚函数地址vptr 定位 vtable,使得程序在运行时能根据对象的实际类型动态选择函数版本,从而实现多态。这一机制由编译器自动实现,开发者只需通过 virtual 关键字声明虚函数即可使用。

7、什么是抽象基类?抽象基类的特点?

抽象基类(Abstract Base Class,简称 ABC) 是一种特殊的类,它不能被实例化,主要用于定义派生类的接口规范。抽象基类的核心作用是为继承体系提供统一的接口标准,强制派生类实现特定的功能。

1.不能实例化对象

class Shape {  // 抽象基类
public:
    virtual void draw() = 0;  // 纯虚函数
};

// Shape s;  // 编译错误:不能实例化抽象基类

2.包含纯虚函数

3.可包含非纯虚函数和成员变量

4.派生类必须实现所有纯虚函数

抽象基类是 C++ 中实现接口抽象的核心机制,其核心特点是:

  • 包含至少一个纯虚函数;
  • 不能实例化对象;
  • 强制派生类实现接口;
  • 支持多态和统一接口操作。

8、虚析构函数和纯虚析构函数的区别?

虚析构函数:在析构函数前加 virtual 关键字,有具体实现。

  • 格式:virtual ~类名() { ... }

  • 纯虚析构函数:在虚析构函数后加 = 0,但必须提供实现(与普通纯虚函数不同)。

  • 格式:virtual ~类名() = 0;(声明),且需在类外定义实现。

结论:

  • 虚析构函数的核心作用是:当通过基类指针删除派生类对象时,确保派生类的析构函数被调用(避免内存泄漏),同时基类本身可实例化。

  • 纯虚析构函数的核心作用是:在实现虚析构函数功能的同时,强制基类成为抽象类(无法实例化),适用于需要基类作为接口规范且不能被实例化的场景。

  • 两者都必须有实现(纯虚析构函数的实现需在类外),否则会导致链接错误。

  • 实际开发中,若基类可能被多态使用,建议将析构函数声明为虚函数;若基类需作为抽象接口(不能实例化),则使用纯虚析构函数(通常结合其他纯虚函数)。

9、说一下模板是如何编译的?

C++ 模板的编译核心是 “先检查语法,再按需实例化”,通过两次编译阶段实现对泛型代码的支持。其特殊之处在于:

  • 模板定义需可见才能实例化(通常放在头文件);
  • 仅在实例化时才检查与具体类型相关的代码合法性;
  • 实例化过程由编译器自动完成(或显式声明)。

10、类模板的源文件和头文件如果分离?有没有什么问题?如何解决?

  • 问题本质:模板实例化需要完整定义,分离存放会导致定义不可见,引发链接错误。
  • 推荐方案
    1. 简单场景:将声明和定义放在同一头文件(.h 或 .hpp);
    2. 逻辑分离:使用 .tpp 实现文件,在头文件中包含;
    3. 固定类型:显式实例化已知类型(如标准库中的部分容器)。

实际开发中,大多数开源库(如 STL)采用方案 1 或 2,因为模板的灵活性依赖于支持任意类型,显式实例化会限制其通用性。

11、C++的4种类型转换?

12、为什么要使用c++异常?

C++ 异常的核心价值在于:提供了一种清晰、安全、可扩展的错误处理模式,尤其适合复杂系统开发。它解决了传统错误码方式的代码混乱、易忽略、传递繁琐等问题,同时与 RAII 结合能有效保证资源安全。

当然,异常也有轻微的性能开销(主要在抛出时),但现代编译器优化已使其在正常路径(无异常)下的开销可忽略不计。在注重可靠性和代码可维护性的场景中,异常是更优的选择。
也可以通过继承的方式,自己来写异常,但也需要注意:

  • 继承体系

    • 通常从 std::exception 派生(便于与标准库异常处理兼容)
    • 设计基础异常类(如 BaseException),再派生出具体类型(如 FileException
    • 形成异常层级,方便按类型捕获
  • 核心成员

    • 必须重写 what() 方法(返回错误描述字符串)
    • 可添加错误码(code_)用于更精细的错误分类
    • 根据业务需求添加额外信息(如文件名、IP 地址等)
  • 使用原则

    • 异常类应轻量(避免复杂构造 / 析构)
    • 确保异常对象可安全复制(因为异常传递可能涉及复制)
    • noexcept 修饰不抛出异常的方法(如 what()getCode()
  • 捕获方式

    • 按从具体到抽象的顺序捕获(先子类后父类)
    • 最后用 catch(...) 捕获未知异常,避免程序崩溃

13、stl的组件有哪些?分别有什么作用?

  • 容器提供数据存储结构;
  • 算法提供操作数据的方法;
  • 迭代器连接容器和算法;
  • 函数对象定制算法行为;
  • 适配器转换接口以适配新需求;
  • 分配器管理底层内存。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值