字节面试准备

字节面试准备

1. C++ 基础

(1) 面向对象的三个特性

C++ 是一种支持面向对象编程的语言,其面向对象编程的三个核心特性是:封装(Encapsulation)、继承(Inheritance)、多态(Polymorphism)。

  • 封装
    封装是将数据(成员变量)和操作这些数据的代码(成员函数)绑定在一起,形成一个整体(类)。
    优点:

    • 限制对内部成员的访问,保护数据隐私(通过 publicprotectedprivate 访问权限控制关键字)。
    • 提高代码的可维护性和复用性。
  • 继承
    继承是一种用于实现代码复用和建立层次化关系的机制。子类可以继承父类的属性和行为。
    优点:

    • 重用父类代码。
    • 可以通过父类指针或引用操作多态对象。
  • 多态
    多态是允许通过基类引用或指针访问派生类对象,并调用派生类的重写方法。基于动态绑定(Run-time Polymorphism)的多态主要通过虚函数实现。
    优点:

    • 提高代码的扩展性和灵活性,可以使用统一的接口处理不同类型的对象。

(2) 多态和虚函数的底层实现

  • 多态的实现

    • 多态的核心是通过 虚函数表(Virtual Table)和虚函数表指针实现的。
    • 编译器会在类中生成一个指向虚函数表的指针,称为 vptr
    • 虚函数表是一个数组,存储了该类的所有虚函数的函数指针。
    • 在运行时,通过 vptr 指向的虚函数表,根据实际的动态类型调用对应的方法。
  • 虚函数的底层实现

    • 类中有虚函数时,类会生成一个虚函数表(存储虚函数指针)。
    • 每个对象的内存布局中会加入一个指向虚函数表的指针(vptr)。
    • 通过 vptr 和虚函数表在运行时找到并调用具体的函数。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base {
public:
virtual void display() { std::cout << "Base" << std::endl; }
};

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

int main() {
Base* obj = new Derived();
obj->display(); // 执行 Derived::display()
}

区别场景:带虚函数与不带虚函数

  • 如果类中没有虚函数,那么对象内存只存储成员变量。
  • 带虚函数的类,内存中会存储额外的虚函数表指针。
  • 因此,带虚函数的对象的内存占用会比没有虚函数的对象多。

(3) 多继承的特殊情况(菱形继承问题)

场景:如何区分多继承中调用的同名成员和方法?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
class A {
public:
int x;
void func() { std::cout << "A::func()" << std::endl; }
};

class B : public A {
public:
void func() { std::cout << "B::func()" << std::endl; }
};

class C : public A {
public:
void func() { std::cout << "C::func()" << std::endl; }
};

class D : public B, public C {};

int main() {
D obj;
// 调用 B::func 或 C::func?
obj.B::func(); // 通过类名加作用域区分
obj.C::func();
return 0;
}

菱形继承问题的解决方式:

  • 普通继承会导致 A 的成员在 BC 中各有一份(两份副本)。
  • 使用 虚继承 可以解决该问题,确保 A 的成员在多继承的子类中只有一份。
1
2
3
4
class A { public: int x; };
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};

D 类中只有一份 A::x


3. 内存

(1) malloc 和 new 的区别

  1. malloc

    • 只分配内存,不调用构造函数。
    • 返回 void* 需要强制类型转换。
    • 需要 free 手动释放。
  2. new

    • 分配内存并自动调用构造函数初始化对象。
    • 返回目标类型的指针,无需强转。
    • 使用 delete 释放内存并调用析构函数。

(2) 只分配内存或只调用构造函数

  1. 只分配内存(不调用构造函数):

    • 可使用 operator new
      1
      void* ptr = ::operator new(sizeof(MyClass)); // 只分配内存
  2. 只调用构造函数(对象已分配内存):

    • 可通过 placement new
      1
      MyClass* obj = new(ptr) MyClass(args); // 在已有内存 `ptr` 上构造对象

operator new:用来分配原始内存,不涉及对象的构造。
operator delete:用来释放原始内存,不涉及对象的析构。
普通的 new 调用等价于:

1
2
MyClass* obj = static_cast<MyClass*>(::operator new(sizeof(MyClass))); // 分配内存
new (obj) MyClass(); // 构造对象

普通的 delete 调用等价于:

1
2
obj->~MyClass();             // 调用析构函数
::operator delete(obj); // 释放内存

注意:new不能重载,只有operator new才能重载

(3) sizeof 结构体的对齐规则

C++ 中结构体内存对齐主要受到以下因素影响:

  1. 每个成员变量的对齐方式由编译器决定(通常与成员类型的大小相关)。
  2. 结构体的总大小必须是最大对齐成员的倍数。

示例:

1
2
3
4
5
6
7
8
9
10
struct S1 {
char A; // 1 字节
char B; // 1 字节
int C; // 4 字节
};
struct S2 {
char A; // 1 字节
int C; // 4 字节
char B; // 1 字节
};
  • 对于 S1,内存布局为:A + B + padding + C(总大小 8)
  • 对于 S2,内存布局为:A + padding + C + B + padding(总大小 12)

4. STL 和智能指针

(1) 智能指针概览

C++ 标准库提供了三种智能指针:

  1. std::unique_ptr: 独占所有权,不可复制。
  2. std::shared_ptr: 共享所有权,使用引用计数。
  3. std::weak_ptr: 使用弱引用,依赖 shared_ptr,避免循环引用。

(2) std::shared_ptr 的实际应用

  • 场景:共享资源的生命周期管理(如线程池中的任务对象)。
  • 示例:
    1
    2
    3
    4
    std::shared_ptr<int> p = std::make_shared<int>(10);
    {
    std::shared_ptr<int> q = p; // 引用计数+1
    } // q 离开作用域,引用计数-1

使用智能指针(如 std::shared_ptr)的优点

自动化的内存管理,避免内存泄漏和悬垂指针。简化代码逻辑,减少人工管理内存的复杂性。更容易与标准容器(如 std::vector)配合使用。不使用智能指针是可以的,

但代价是:

更容易引入 bug,例如内存泄漏、悬垂指针问题。
增加代码维护难度,并且可能需要大量的单元测试来覆盖所有的边界情况。
难以保证代码的健壮性,特别是在复杂的资源关系中。

RAII是什么

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是一种资源管理的编程惯用法,是 C++ 中处理资源管理的重要设计理念。它通过将资源的生命周期与对象的生命周期绑定来确保资源得到正确的分配和释放,从而避免资源泄漏。

核心思想

  1. 资源绑定到对象的生命周期

    • 当一个对象在栈上或通过堆分配时,与该对象相关联的资源(例如内存、文件句柄、锁、网络连接等)也随之初始化。
    • 当该对象离开作用域(或者析构)时,资源会被自动释放。
  2. 依靠析构函数来释放资源

    • C++ 中,当一个对象生命周期结束时,其析构函数会被自动调用。因此,可以利用析构函数自动释放资源,而无需手动释放。
    • 通过 RAII,资源分配后不用担心异常、早退或其他复杂逻辑干扰正确释放。

RAII 的优点

  1. 自动化资源管理:用户不需要手动释放资源,不用担心遗漏或错误。
  2. 异常安全:即使代码中间发生异常,析构函数仍然会自动释放资源。
  3. 代码简洁:减少了显式的资源释放代码,避免了冗余或错误。

应用场景

RAII 在内存分配、文件操作、线程锁、数据库连接等场景广泛应用。例如:

  1. 智能指针
    • std::unique_ptr:独占资源,适合动态内存管理。
    • std::shared_ptr:共享资源,适合多个对象共享动态内存。
  2. 互斥锁
    • std::lock_guard<std::mutex>std::unique_lock
  3. 文件与流对象
    • C++ 的 IO 流类如 std::ifstreamstd::ofstream
  4. 其他自定义的资源管理类
    • 如管理数据库连接、网络套接字、句柄等。

总结

RAII 的核心就是将资源的生命周期绑定到对象的生命周期,通过构造函数和析构函数确保资源的正确分配和释放。这种机制有效避免了手动管理资源时的各种问题(如资源泄漏、重复释放等),是 C++ 中非常重要的编程思想,尤其在异常安全和代码简洁性方面效果显著。

面试中,如果被问到 RAII,你可以直接引用 C++ 智能指针、文件流或锁管理类作为例子展开解释。进一步可以说明 RAII 提升了程序的安全性及简洁性,使 C++ 的资源管理更加工程化和易用化。

5. 操作系统

(1) 并发与并行的区别

  • 并发(Concurrency):多个任务在逻辑上同时进行,但实际上可能是按时间片交替执行。
  • 并行(Parallelism):多个任务在物理上真正同时运行(需要多核 CPU 支持)。

(2) 最大并发线程池设计

  • 关键点:
    1. 使用任务队列管理任务。
    2. 通过信号量或条件变量限制线程并发数量。
    3. 动态创建、销毁线程降低资源消耗(即线程复用)。

6. 网络

(1) HTTPS 的通信过程

  1. 客户端发送请求,包括支持的协议版本、加密套件等。
  2. 服务器发送证书,客户端验证证书的合法性。
  3. TLS 握手,双方协商对称密钥(通过非对称加密交换密钥)。
  4. 加密通信,之后的所有数据使用对称密钥加密。

7. 项目挑战

Buffer 的思想

  • 临时缓存用于解决数据处理速度的差异(如网络接收速度慢于处理速度)。
  • **自动增长:**通过动态分配内存扩容实现,常用 指数增长(2 倍增长) 策略;也可以按照具体业务需要调整。