嵌入式八股-C++面试30题(20240819)

1. 单继承与多继承的虚函数表结构

单继承的虚函数表结构
在单继承中,每个类都有一个虚函数表(vtable),它是一个指向虚函数的指针数组。虚函数表中包含该类及其父类的所有虚函数指针。对象中会有一个指向该虚函数表的指针,称为虚表指针(vptr)。

class Base {
public:
    virtual void foo() { std::cout << "Base::foo" << std::endl; }
    virtual void bar() { std::cout << "Base::bar" << std::endl; }
};

class Derived : public Base {
public:
    void foo() override { std::cout << "Derived::foo" << std::endl; }
};

对于 Base 类,其虚表会包含 foobar 的指针。对于 Derived 类,其虚表中 foo 指针会指向 Derived::foo,而 bar 指针则继承自 Base

多继承的虚函数表结构
在多继承中,情况更复杂。如果一个类从多个基类继承,通常每个基类部分都会有自己的虚函数表,这些虚表指针通常放在类的实例对象中。

class Base1 {
public:
    virtual void foo1() { std::cout << "Base1::foo1" << std::endl; }
};

class Base2 {
public:
    virtual void foo2() { std::cout << "Base2::foo2" << std::endl; }
};

class Derived : public Base1, public Base2 {
public:
    void foo1() override { std::cout << "Derived::foo1" << std::endl; }
    void foo2() override { std::cout << "Derived::foo2" << std::endl; }
};

Derived 类会有两个虚表指针,一个对应 Base1 的虚表,另一个对应 Base2 的虚表。

2. C++ 程序编译过程

C++ 程序的编译过程包括以下几个步骤:

  1. 预处理(Preprocessing)
    预处理器处理 #include#define 等指令,生成预处理后的文件。此步骤替换宏、处理条件编译指令等。

  2. 编译(Compilation)
    编译器将预处理后的代码翻译为汇编代码。这个过程会将高级语言转换为目标处理器可以理解的低级指令。

  3. 汇编(Assembly)
    汇编器将汇编代码转换为机器码,生成目标文件(.obj.o 文件)。

  4. 链接(Linking)
    链接器将多个目标文件及其依赖的库文件链接为最终的可执行文件。这一步解决符号引用,合并代码段和数据段,并最终生成一个完整的可执行程序。

3. C++ 内存管理

C++ 提供了两种主要的内存管理方式:

  1. 静态内存分配
    编译时分配,大小和生命周期固定,如全局变量、局部静态变量。

  2. 动态内存分配
    通过 newdelete 操作符在堆上动态分配和释放内存。动态内存分配允许在运行时决定内存的大小,但需要手动管理内存的释放,避免内存泄漏。

int* ptr = new int(5);  // 动态分配内存
delete ptr;             // 释放内存

4. 栈和堆的区别

  • 栈(Stack)
    栈是自动管理的内存区域,函数调用时自动分配和释放。栈内存管理快,但大小有限。

  • 堆(Heap)
    堆是用于动态内存分配的区域,内存由程序员手动管理。堆内存管理灵活但较慢,容易导致内存泄漏或碎片化。

使用场景:
栈用于局部变量、函数调用等生命周期明确的数据。堆用于需要动态分配内存的数据,如大型数组或对象。

5. 变量的区别

  • 局部变量
    在函数或代码块中定义,生命周期在函数或块结束时终止。

  • 全局变量
    在所有函数外部定义,生命周期随程序运行期始终存在,作用域为整个文件或通过 extern 扩展到其他文件。

  • 静态变量
    局部静态变量在函数内部定义,但其生命周期贯穿整个程序运行期。全局静态变量则只能在定义它的文件中可见。

  • 动态变量
    通过 new 分配的变量,其生命周期由程序员管理,需手动释放。

6. 全局变量定义在头文件中的问题

将全局变量定义在头文件中会导致在多个文件中重复定义同一变量,从而导致链接错误(重复定义)。解决方法是将全局变量的定义放在一个源文件中,而在其他需要使用的文件中通过 extern 声明。

// 头文件.h
extern int globalVar;

// 源文件.cpp
int globalVar = 0;

7. 内存对齐

内存对齐是为了提高内存访问效率。处理器通常要求数据在内存中的地址是某个特定倍数(如 4 或 8)的对齐地址。如果数据没有对齐,处理器可能需要多次内存访问才能读取完整数据,影响性能。

struct MyStruct {
    char a;     // 1字节
    int b;      // 4字节
    char c;     // 1字节
};

在默认对齐下,b 可能会被放在 4 字节边界处,导致 MyStruct 的总大小为 8 字节而非 6 字节。

8. 什么是内存泄漏

内存泄漏是指程序运行时分配的内存未被正确释放,导致这部分内存无法再被使用,最终可能导致系统内存耗尽。常见原因包括忘记释放 new 分配的内存或资源未被正确清理。

9. 如何防止内存泄漏?内存泄漏检测工具的原理

防止内存泄漏的方法:

  • 使用智能指针,如 std::unique_ptrstd::shared_ptr
  • 确保所有 new 的对象都有相应的 delete
  • 在析构函数中释放动态分配的资源。

内存泄漏检测工具:

  • Valgrind:通过监控内存分配和释放检测泄漏。
  • AddressSanitizer:编译时插入额外检查代码,运行时检测非法内存访问和泄漏。

10. 智能指针的种类及实现原理

  • unique_ptr:独占所有权,不能复制,转移所有权使用 std::move
  • shared_ptr:共享所有权,内部使用引用计数管理内存,引用计数为零时释放资源。
  • weak_ptr:不控制资源,不增加引用计数,用于解决 shared_ptr 的循环引用问题。

11. 智能指针应用举例

std::unique_ptr<int> p1(new int(10));
std::shared_ptr<int> p2 = std::make_shared<int>(20);

void func() {
    std::shared_ptr<int> p3 = p2; // p2, p3 共享同一内存
}

优点: 自动管理内存,防止泄漏。
缺点: 可能导致循环引用问题。

12. 如何将 unique_ptr 赋值给另一个 unique_ptr 对象

unique_ptr 不能复制,但可以通过移动语义转移所有权。

std::unique_ptr<int> p1(new int(10));
std::unique_ptr<int> p2 = std::move(p1); // p1 失去所有权,p2 获得所有权

13. 使用智能指针可能出现的问题及解决方法

循环引用shared_ptr 可能导致循环引用,导致内存泄漏。

解决方法:使用 weak_ptr 打破循环引用。

struct A;
struct B;

struct A {
    std::shared_ptr<B> b_ptr;
};

struct B {
    std::weak_ptr<A> a_ptr;
};

14. 在 Visual Studio 中检测内存泄漏的方法

Visual Studio 提供内存泄漏检测功能,通过在代码中插入 _CrtDumpMemoryLeaks() 函数,程序结束时自动检测泄漏。

#define _CRTDBG

_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

int main() {
    _CrtDumpMemoryLeaks();
    return 0;
}

15. 深拷贝与浅拷贝

  • 浅拷贝:复制对象时,仅复制指针或引用,两个对象共享同一资源。
  • 深拷贝:复制对象时,同时复制指向的资源,两个对象独立存在。
class MyClass {
    int* data;
public:
    MyClass(int val) : data(new int(val)) {}
    MyClass(const MyClass& other) : data(new int(*other.data)) {} // 深拷贝
    ~MyClass() { delete data; }
};

16. 虚拟内存

虚拟内存是操作系统管理内存的技术,提供一个逻辑上连续的地址空间,并将物理内存和磁盘空间结合。它允许程序使用比实际物理内存更多的内存,并隔离各程序的内存空间。

17. 语言对比

C++ 与 Java:

  • 内存管理:C++ 需要手动管理内存,Java 有垃圾回收。
  • 语法:C++ 支持多重继承,Java 不支持,但有接口机制。
  • 性能:C++ 性能更高,因为它编译为本地代码,而 Java 运行在 JVM 上。

C++ 与 Python:

  • 性能:C++ 性能优于 Python。
  • 语法:Python 语法更简洁,适合快速开发。
  • 内存管理:Python 有自动垃圾回收,C++ 需要手动管理内存。

18. C++ 11 新特性

  • 自动类型推导auto 关键字。
  • 智能指针unique_ptrshared_ptr
  • lambda 表达式:简化回调和函数对象的使用。
  • 右值引用和移动语义:提高性能,避免不必要的拷贝。

19. C 和 C++ 的区别

  • 面向对象:C 是面向过程的语言,C++ 支持面向对象编程。
  • 标准库:C++ 提供更丰富的标准库,如 STL。
  • 类型检查:C++ 类型检查更严格。

20. Python 和 C++ 的区别

  • 语法:Python 更简洁,适合快速开发;C++ 语法复杂,但灵活性更高。
  • 性能:C++ 性能更优,Python 适合编写脚本和快速原型。
  • 内存管理:C++ 手动管理内存,Python 自动垃圾回收。

21. 面向对象

面向对象编程(OOP)是一种编程范式,通过类和对象封装数据和操作。其优势在于提高代码的可复用性、可扩展性和可维护性。

22. 面向对象的三大特性

  • 封装:将数据和操作封装在类中,隐藏内部实现。
  • 继承:通过继承复用代码,创建层次结构。
  • 多态:允许不同类型对象通过同一接口进行操作,分为静态多态和动态多态。

23. 重载、重写、隐藏的区别

  • 重载:同一作用域内,函数名称相同,但参数不同。
  • 重写:子类重写父类的虚函数,实现不同的功能。
  • 隐藏:子类定义了与父类同名但不同参数的函数,隐藏父类同名函数。

24. 如何理解 C++ 是面向对象编程

C++ 通过类、继承和多态等特性支持面向对象编程,允许程序员创建和操作对象,并利用封装、继承和多态实现代码复用和扩展。

25. 什么是多态?多态如何实现?

多态是指不同对象可以通过同一接口进行操作。C++ 中多态通过虚函数实现,允许子类重写父类的虚函数,从而在运行时决定调用哪个函数。

26. 静态多态与动态多态

  • 静态多态:编译时决定调用哪个函数,如函数重载、模板。
  • 动态多态:运行时决定调用哪个函数,通过虚函数实现。

27. 类相关

类是 C++ 中的基本构造,包含数据成员和成员函数。构造函数用于初始化对象,析构函数用于清理资源。

28. 什么是虚函数?什么是纯虚函数?

  • 虚函数:基类中通过 virtual 关键字声明,允许子类重写。
  • 纯虚函数:在基类中定义为纯虚函数,要求子类必须实现,表示类是抽象类。

29. 虚函数与纯虚函数的区别

虚函数可以有默认实现,而纯虚函数没有实现,必须由子类实现。纯虚函数使得类成为抽象类,不能实例化。

30. 虚函数的实现机制

虚函数通过虚函数表(vtable)实现。每个含有虚函数的类都有一个虚函数表,表中存储了该类及其继承链上所有虚函数的地址。对象中存储一个指向该表的指针(vptr),运行时通过这个指针找到并调用正确的函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sagima_sdu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值