360 C++ 面试真题

1、虚函数表的机制

  1. 虚函数的声明和定义:在基类中声明一个函数为虚函数,然后在派生类中进行重写(override)。

class Base {
public:
    virtual void virtualFunction() {
        // 虚函数的定义
    }
};
​
class Derived : public Base {
public:
    void virtualFunction() override {
        // 派生类中对虚函数的重写
    }
};
  1. 虚函数表的创建:对于每个含有虚函数的类,编译器在该类的对象中添加一个指向虚函数表的指针(vptr)。这个指针指向一个虚函数表,其中包含该类的虚函数的地址。

  2. 对象的布局:每个对象都包含一个指向虚函数表的指针。当对象被创建时,这个指针被初始化为指向该类的虚函数表。

Base* obj = new Derived();

obj 是基类指针,指向了一个派生类的对象。实际上,obj 中存储了一个指向虚函数表的指针,而这个虚函数表是派生类的虚函数表。

  1. 动态联编(Dynamic Binding):当调用一个虚函数时,编译器会使用虚函数表来查找正确的函数地址。这样,即使通过基类指针调用虚函数,实际执行的是派生类中的重写函数。

obj->virtualFunction();  // 实际调用的是 Derived 类中的 virtualFunction
  1. 多态性的实现:虚函数表的机制使得程序能够根据对象的实际类型来调用相应的虚函数,实现了多态性。这是C++中实现运行时多态的关键。

2、构造函数可以是虚函数嘛说出原因

  1. 对象的创建时机:虚函数的调用是通过对象的虚函数表(vtable)来实现的,而虚函数表的构建是在对象构造阶段之后完成的。在对象构造期间,对象的虚函数表还没有构建完成,因此在构造函数中无法通过虚函数表调用虚函数。

  2. 虚函数表的指针初始化:对象的虚函数表指针(vptr)是在对象构造期间初始化的,而构造函数是在对象的构造过程中调用的。由于在构造函数调用之前无法确定对象的动态类型,因此在构造函数中调用虚函数会导致无法正确地找到虚函数的实现。

尝试在构造函数中使用虚函数可能会导致不可预测的行为,因为此时对象的状态尚未完全初始化,虚函数表可能尚未准备好。因此,C++语言规定构造函数不能声明为虚函数。

3、C++11新特性

  1. 自动类型推导(Auto):允许编译器推导变量的类型,使代码更加简洁。

auto x = 5; // x的类型将被推导为int
  1. 范围-based for 循环:简化了对容器元素的遍历。

std::vector numbers = {1, 2, 3, 4, 5};
for (const auto& num : numbers) {
    // 使用num
}
  1. 智能指针:引入了std::shared_ptr和std::unique_ptr等智能指针,用于管理动态分配的内存,帮助防止内存泄漏。

std::shared_ptr sharedPtr = std::make_shared(42);
  1. Lambda 表达式:允许在函数内部定义匿名函数,提高代码可读性和灵活性。

auto add = [](int a, int b) { return a + b; };
  1. nullptr:引入了空指针常量nullptr,用于替代传统的空指针NULL。

int* ptr = nullptr;
  1. 强制类型转换(Type Casting):引入了static_cast、dynamic_cast、const_cast、reinterpret_cast等更安全和灵活的类型转换操作符。

double x = 3.14;
int y = static_cast(x);
  1. 右值引用和移动语义:支持通过右值引用实现移动语义,提高了对临时对象的处理效率。

std::vector getVector() {
    // 返回一个临时vector
    return std::vector{1, 2, 3};
}

std::vector numbers = getVector(); // 使用移动语义
  1. 新的容器和算法:引入了新的容器,如std::unordered_map、std::unordered_set,以及一些新的算法。

std::unordered_map myMap = {{1, "one"}, {2, "two"}};
  1. 线程支持(std::thread):提供了原生的多线程支持,使得并发编程更加方便。

#include 

void myFunction() {
    // 线程执行的代码
}

int main() {
    std::thread t(myFunction);
    t.join(); // 等待线程结束
    return 0;
}

3、介绍4种智能指针

  1. std::unique_ptr

    • std::unique_ptr 是一种独占所有权的智能指针,即同一时刻只能有一个 std::unique_ptr 指向一个对象。当 std::unique_ptr 被销毁时,它拥有的对象会被自动释放。

    • 使用 std::move 可以将所有权从一个 std::unique_ptr 转移到另一个,但源 std::unique_ptr 将变为 null。

    • 适用于在动态分配内存时,需要确保只有一个指针拥有所有权的情况。

#include 

int main() {
    std::unique_ptr ptr = std::make_unique(42);
    // 使用 std::move 转移所有权
    std::unique_ptr ptr2 = std::move(ptr);
    // 此时 ptr 为 null
    return 0;
}
  1. std::shared_ptr

    • std::shared_ptr 允许多个智能指针共享同一个对象,通过引用计数来追踪对象的引用次数。当最后一个 std::shared_ptr 指向对象的引用计数变为零时,对象会被销毁。

    • 可以通过 std::make_shared 来创建 std::shared_ptr。

#include 

int main() {
    std::shared_ptr ptr1 = std::make_shared(42);
    // 共享同一个对象
    std::shared_ptr ptr2 = ptr1;
    // 引用计数为 2
    return 0;
}
  1. std::weak_ptr

    • std::weak_ptr 也是用于共享对象所有权的智能指针,但不增加引用计数。它通常用于打破 std::shared_ptr 的循环引用,避免内存泄漏。

    • 可以通过 std::shared_ptr 创建 std::weak_ptr。

#include 

int main() {
    std::shared_ptr sharedPtr = std::make_shared(42);
    std::weak_ptr weakPtr = sharedPtr;
    // ...
    // 当需要使用时,通过 lock 转换为 shared_ptr
    if (std::shared_ptr lockedPtr = weakPtr.lock()) {
        // 使用 lockedPtr
    }
    return 0;
}
  1. std::auto_ptr(已被废弃)

    • std::auto_ptr 是C++98时代的智能指针,用于实现独占所有权。然而,由于其潜在的不安全性和不明确的所有权传递语义,C++11 引入的 std::unique_ptr 已经取代了 std::auto_ptr。

    • 不建议在现代C++中使用 std::auto_ptr,而应该使用 std::unique_ptr。

4、weak_ptr如何访问指向的数据

std::weak_ptr 通过 lock 方法来访问其指向的数据。lock 方法返回一个 std::shared_ptr 对象,该对象与 std::weak_ptr 共享同一对象,如果 std::weak_ptr 不再有效(底层对象已经被销毁),则返回一个空的 std::shared_ptr。

给个例子:

#include 
#include 

int main() {
    // 创建 shared_ptr 和 weak_ptr 共享同一个对象
    std::shared_ptr sharedPtr = std::make_shared(42);
    std::weak_ptr weakPtr = sharedPtr;

    // 使用 lock 方法获取 shared_ptr
    if (std::shared_ptr lockedPtr = weakPtr.lock()) {
        // lockedPtr 指向的对象仍然存在
        std::cout << "Value: " << *lockedPtr << std::endl;
    } else {
        // weak_ptr 不再有效,对象已经被销毁
        std::cout << "The object is no longer available." << std::endl;
    }

    // sharedPtr 离开作用域,底层对象引用计数变为零,对象被销毁

    // 再次使用 lock 方法获取 shared_ptr
    if (std::shared_ptr lockedPtr = weakPtr.lock()) {
        // 这部分代码不会执行,因为对象已经被销毁
        std::cout << "Value: " << *lockedPtr << std::endl;
    } else {
        std::cout << "The object is no longer available." << std::endl;
    }

    return 0;
}

5、右值引用

用于支持移动语义和完美转发。右值引用的语法使用 && 符号。

  1. 移动语义:右值引用允许将资源(如堆上分配的内存)的所有权从一个对象转移到另一个对象,而不是进行深拷贝。这可以提高性能,特别是对于大型数据结构。

  2. 完美转发:右值引用可以在函数模板中实现完美转发,将参数按原样传递给其他函数,包括它们的引用类型。这样,被调用函数能够保留原始参数的左值或右值性质。

给个例子:

#include 

// 移动构造函数演示移动语义
class MyString {
public:
    MyString() : data(nullptr), size(0) {}

    // 移动构造函数
    MyString(MyString&& other) noexcept : data(other.data), size(other.size) {
        // 将原对象置为空,防止原对象析构时释放内存
        other.data = nullptr;
        other.size = 0;
    }

    // 输出字符串内容
    void print() const {
        std::cout << "Data: " << (data ? data : "null") << ", Size: " << size << std::endl;
    }

private:
    char* data;
    size_t size;
};

// 函数模板演示完美转发
template 
void forwardFunction(T&& arg) {
    std::cout << "Received: ";
    arg.print();
}

int main() {
    // 使用移动构造函数演示移动语义
    MyString source;
    source.print();

    MyString destination = std::move(source);
    std::cout << "After move:\n";
    source.print();  // source 现在为空
    destination.print();

    // 使用函数模板演示完美转发
    MyString myString;
    myString.print();
    forwardFunction(myString);                // 传递左值
    forwardFunction(MyString());              // 传递右值
    forwardFunction(std::move(myString));     // 传递右值引用

    return 0;
}

7、将亡值有哪些

  1. 纯右值:纯右值是指不能被命名的、临时的右值表达式。它们通常是在表达式求值过程中创建的,例如函数返回值、类型转换的结果等。纯右值在使用后通常会被立即销毁。

int getValue() {
    return 42;  // 函数返回的是纯右值
}

int main() {
    int result = getValue();  // getValue() 的结果是纯右值
    return 0;
}
  1. 将亡引用:将亡引用是C++11引入的一项特性,使用 && 来声明。将亡引用主要用于实现移动语义和完美转发。将亡引用可以绑定到纯右值,同时它也允许我们修改右值。

int main() {
    int x = 42;
    int&& rvalueRef = std::move(x);  // 将亡引用绑定到纯右值
    rvalueRef++;  // 修改右值
    return 0;
}

8、new出来的数据存放在哪里

在C++中,使用 new 运算符动态分配内存来创建对象时,这些对象的存储位置通常位于堆(Heap)内存中。堆是程序运行时分配的内存区域,它的大小动态增长和缩小,用于存储程序运行时动态分配的对象和数据。

具体来说,使用 new 运算符创建的对象在堆内存中分配一块连续的内存空间,而返回的是该内存块的地址,即指向对象的指针。这个指针可以存储在栈上、其他堆上,或者全局数据区等地方,取决于它的生命周期和作用域。

示例:

#include 

int main() {
    // 使用 new 运算符在堆上动态分配一个整数
    int* dynamicInt = new int(42);

    // 使用动态分配的整数
    std::cout << "Dynamic Integer: " << *dynamicInt << std::endl;

    // 记得在不再需要时释放堆上的内存
    delete dynamicInt;

    return 0;
}

9、多线程的模式

  1. Fork-Join模式

Fork-Join模式是一种并行计算的模式,主要用于将一个大任务拆分为多个小任务,每个任务独立执行,最后将结果合并。这通常包括一个"分阶段"的过程,其中任务分为更小的子任务,然后在合并阶段将结果组合起来。在C++中,可以使用std::async和std::future等来实现Fork-Join模式。

  1. Producer-Consumer模式

Producer-Consumer模式涉及两类线程:生产者(Producer)线程和消费者(Consumer)线程。生产者线程生成数据,并将其放入共享的缓冲区,而消费者线程从缓冲区中取出数据进行处理。这种模式需要考虑线程之间的同步,以避免竞态条件。

  1. Readers-Writers模式

Readers-Writers模式用于管理对共享资源的读写访问。多个读取者可以同时访问共享资源,但写入者必须独占访问。这有助于提高读取性能,同时确保写入的一致性。在C++中,可以使用互斥锁和条件变量来实现Readers-Writers模式。

  1. Worker Thread模式

Worker Thread模式将任务提交到工作队列,然后由多个工作线程并发处理这些任务。这种模式常用于处理异步事件或需要并行处理的任务。在C++中,可以使用线程池来实现Worker Thread模式。

  1. Pipeline模式

Pipeline模式将一个大任务划分为一系列的阶段,每个阶段由一个线程负责。每个阶段的输出作为下一个阶段的输入。这种模式适用于需要顺序处理的任务,每个阶段可以并行执行。在C++中,可以使用多个线程和队列来实现Pipeline模式。

  1. Actor模型

Actor模型是一种并发计算模型,其中系统由独立的Actor组成,每个Actor都有自己的状态、行为和邮箱。Actors之间通过消息进行通信,而不是共享内存。这种模型可以减少共享状态的复杂性,从而降低并发编程的难度。

10、介绍项目中用到的生产者消费者模式

这里就讲讲生产者消费者模式吧。

生产者-消费者模式是一种并发设计模式,用于解决在多线程环境中生产者和消费者之间的数据共享和同步问题。在这个模式中,有一个生产者线程负责生成数据并将其放入共享的缓冲区,同时有一个或多个消费者线程负责从缓冲区中取出数据并进行处理。

以下是生产者-消费者模式的基本结构和关键概念:

  1. 共享缓冲区:用于存放生产者生成的数据,以便消费者线程可以从中取出数据。这个缓冲区可以是一个队列或者是一个固定大小的缓冲区。

  2. 同步机制:用于确保生产者和消费者之间的正确同步,避免竞态条件和数据不一致的问题。通常使用互斥锁和条件变量来实现。

  3. 信号量或计数器:用于跟踪缓冲区中的可用空间或待处理元素的数量,以便控制生产者和消费者的行为。

给个例子:

#include 
#include 
#include 
#include 
#include 

const int BUFFER_SIZE = 5;

std::queue buffer;
std::mutex bufferMutex;
std::condition_variable bufferNotEmpty;
std::condition_variable bufferNotFull;

void producer() {
    for (int i = 1; i <= 10; ++i) {
        std::unique_lock lock(bufferMutex);

        // 等待缓冲区非满
        bufferNotFull.wait(lock, [] { return buffer.size() < BUFFER_SIZE; });

        buffer.push(i);
        std::cout << "Produced: " << i << std::endl;

        // 通知消费者缓冲区非空
        bufferNotEmpty.notify_all();
    }
}

void consumer() {
    for (int i = 1; i <= 10; ++i) {
        std::unique_lock lock(bufferMutex);

        // 等待缓冲区非空
        bufferNotEmpty.wait(lock, [] { return !buffer.empty(); });

        int data = buffer.front();
        buffer.pop();
        std::cout << "Consumed: " << data << std::endl;

        // 通知生产者缓冲区非满
        bufferNotFull.notify_all();
    }
}

int main() {
    std::thread producerThread(producer);
    std::thread consumerThread(consumer);

    producerThread.join();
    consumerThread.join();

    return 0;
}

11、生产者生产太快,消费者消费太慢怎么办(条件变量)

  1. 增大缓冲区大小:增大缓冲区的大小可以容纳更多的数据,减缓缓冲区满的速度。这可以是一个简单的解决方案,但要注意,如果缓冲区过大,可能会占用过多的内存。

  2. 调整生产者和消费者线程的工作速度:这可以通过控制生产者和消费者的休眠时间来实现。例如,如果生产者速度太快,可以在生产者线程中添加短暂的休眠,以减缓其生成数据的速度。

  3. 使用有界队列:可以使用有界队列,即设置一个最大容量,当队列满时,生产者将被阻塞,直到有足够的空间。这样可以防止生产者无限制地生成数据。

12、负载均衡算法有哪些(只答了概念,没具体了解有哪些算法)

  1. 轮询

轮询算法按顺序将请求分配给服务器,每个请求依次分配到下一个服务器。它是最简单的负载均衡算法,适用于各服务器性能相近的场景。

  1. 最小连接数

最小连接数算法将请求分配给当前连接数最少的服务器,以确保连接数相对均衡。这对于处理长连接的应用场景比较有效。

  1. 最短响应时间

最短响应时间算法将请求分配给当前响应时间最短的服务器。通过监测服务器的响应时间,确保将请求发送到最快的服务器上。

  1. IP散列

IP散列算法使用客户端IP地址进行散列计算,然后将请求分配给特定的服务器。这确保相同IP的客户端始终被分配到同一台服务器,适用于需要保持会话一致性的场景。

  1. 加权轮询

加权轮询算法根据服务器的性能设置权重,高性能服务器分配更多的请求。这样可以根据服务器的负载能力分配请求,提高整体性能。

  1. 加权最小连接数

加权最小连接数算法结合了最小连接数和加权轮询的思想,根据服务器的权重和当前连接数分配请求。

  1. Random算法

Random算法随机选择一个服务器来处理请求。虽然简单,但可能导致负载不均衡。

  1. Fair算法

Fair算法根据服务器的响应时间动态调整权重,以确保服务器的负载相对均衡。

  1. Chash算法(一致性哈希)

一致性哈希算法将请求和服务器通过哈希函数映射到一个固定的范围,确保添加或删除服务器时最小化映射的变化,减小影响范围。

13、介绍thrift框架

Apache Thrift(简称Thrift)是一个跨语言的远程服务调用框架,由Apache开发。它被设计用于在不同的系统之间进行通信,并支持多种编程语言。

  1. 多语言支持

Thrift支持多种编程语言,包括但不限于Java、C++、Python、Ruby、C#、PHP等。这使得不同语言的应用程序能够通过Thrift进行通信。

  1. 接口定义语言(IDL)

Thrift使用接口定义语言(IDL)来定义服务接口和数据类型。IDL描述了服务的方法、输入参数、输出参数等信息,同时定义了跨语言的数据结构。

  1. 多协议支持

Thrift支持多种传输协议,包括二进制、JSON、XML等。这使得Thrift可以适应不同的应用场景和网络环境。

  1. 多传输层支持

Thrift支持多种传输层协议,例如TCP、HTTP等。这使得Thrift可以在不同的网络层上工作,从而更灵活地适应各种网络环境。

  1. 服务框架

Thrift提供了生成的服务框架,开发者只需实现IDL中定义的接口,Thrift会生成服务端和客户端的代码。这大大简化了远程服务的开发。

  1. 异步通信

Thrift支持异步通信,可以通过回调方式处理请求,提高系统的并发性能。

  1. 跨语言异常传递

Thrift允许服务端在不同的编程语言中抛出异常,而客户端能够在其自身的异常处理机制中捕获和处理这些异常。

  1. 扩展性

Thrift提供了一些可扩展的机制,例如自定义协议、传输层、数据类型等,以满足特定需求。

14、介绍rpc框架

RPC(Remote Procedure Call,远程过程调用)是一种通信协议,用于使一个程序执行另一个地址空间(通常是共享网络的另一台机器上)的过程调用。RPC框架是实现RPC协议的软件框架,提供了一种简单的方法来进行远程通信,使得在分布式系统中的不同节点上的程序能够像调用本地函数一样调用远程函数。

一些特点:

  1. 远程调用

RPC框架允许程序调用远程机器上的过程,就像调用本地过程一样。调用者无需关心底层的网络通信细节,使得分布式系统的开发更加简便。

  1. 透明性

RPC框架提供了透明性,使得远程调用对于调用者而言是透明的,就像调用本地函数一样。开发者不需要关心底层的网络传输和序列化过程。

  1. 通用性

RPC框架通常是语言无关的,可以支持多种编程语言。这使得在不同语言编写的程序之间进行通信成为可能。

  1. 序列化

RPC框架通过序列化和反序列化来在不同机器之间传递数据。序列化是将数据转换为字节流的过程,反序列化则是将字节流还原为数据的过程。

  1. 服务描述

RPC框架通常使用一种服务描述语言(IDL)来定义服务接口和数据结构。IDL描述了远程服务的方法、输入参数、输出参数等信息。

  1. 通信协议

RPC框架使用不同的通信协议,如HTTP、TCP、UDP等,来实现远程过程调用。

  1. 错误处理

RPC框架提供了错误处理机制,使得远程调用中的错误能够被捕获和处理。

15、介绍项目中的raft协议

16、发生网络故障时raft协议如何工作的

17、tcp和udp的区别

  1. 连接性

TCP是面向连接的协议,它在通信之前需要先建立连接,然后进行数据传输,最后释放连接。UDP是无连接的协议,通信双方之间不需要建立持久的连接。

  1. 可靠性

TCP提供可靠的数据传输,它使用确认、重传、超时和流量控制等机制来确保数据的完整性和顺序性。如果某个数据包丢失或损坏,TCP会尝试重新传输。UDP不提供可靠性,它将尽最大努力交付数据,但不保证顺序或完整性。

  1. 数据流

TCP是面向字节流的,它保证数据以正确的顺序到达接收端。UDP是面向数据包的,每个数据包是独立的,可能以不同的顺序到达接收端。

  1. 开销

TCP的连接建立和维护会引入一定的开销,包括三次握手、数据传输和四次挥手等。UDP没有连接的开销,因此更轻量级。

  1. 适用场景

TCP适用于对可靠性要求较高的应用,如文件传输、Web浏览、电子邮件等。UDP适用于对实时性要求较高、可以容忍一定数据丢失的应用,如音视频传输、实时游戏等。

  1. 流量控制

TCP使用流量控制机制来防止发送方发送速度过快导致接收方无法处理的情况。UDP不提供流量控制,发送方可以按照自己的速率发送数据。

  1. 拥塞控制:

TCP有拥塞控制机制,通过慢启动、拥塞避免等算法来适应网络的拥塞情况。UDP没有拥塞控制,发送方会不断发送数据,无法感知网络的拥塞情况。

18、介绍websocket

WebSocket是一种在单个TCP连接上提供全双工通信的协议,允许在客户端和服务器之间进行实时双向数据传输。它与传统的HTTP通信不同,HTTP是基于请求-响应模型的,每次请求都需要服务器响应,而WebSocket允许在一次握手之后,双方之间建立持久连接,可以随时发送数据。

原理:

  1. 握手过程

WebSocket通信始于一个特殊的握手过程,通过HTTP协议完成。客户端通过发送一个HTTP请求,其中包含了WebSocket的协议信息,服务器在接收到请求后协商是否升级到WebSocket。如果协商成功,连接将升级到WebSocket。

  1. 全双工通信

一旦握手成功,WebSocket连接就变成了全双工通信,允许客户端和服务器双向实时通信。这与传统的HTTP通信模型不同,无需等待请求-响应的周期。

  1. 持久连接

WebSocket连接是持久的,不像HTTP那样每次请求都需要重新建立连接。这减少了每次通信的开销,提高了实时性。

  1. 帧格式

WebSocket使用帧(Frame)来传输数据,帧包括了一些控制帧和数据帧,以及一些用于处理长度、掩码等信息的标志位。这种帧格式可以更有效地传输小块数据。

  1. 协议标识符

WebSocket的协议标识符是"ws"(WebSocket)或"wss"(WebSocket Secure)。"ws"表示普通的WebSocket连接,"wss"表示WebSocket连接上加了TLS/SSL加密层的安全连接。

  1. 安全性

为了提高安全性,可以在WebSocket上加入TLS/SSL层,形成WebSocket Secure(WSS)。这样可以加密通信内容,防止中间人攻击等安全威胁。

  1. 适用场景

WebSocket适用于需要实时性、低延迟的应用场景,如在线聊天、实时通知、在线协作编辑等。它在传输时的开销相对较小,适合频繁的小数据传输。

19、介绍redis,为什么redis快?

  1. 数据存储在内存中

Redis将所有数据存储在内存中,通过在内存中读写数据,避免了传统磁盘数据库频繁的I/O操作。内存的读写速度远远快于磁盘,因此Redis能够提供非常高的读写性能。

  1. 单线程模型

Redis采用单线程的事件循环模型,这使得Redis能够充分利用CPU,避免了多线程的切换开销。虽然Redis是单线程的,但它通过非阻塞的方式处理多个连接,实现了并发性。

  1. 非阻塞I/O

Redis使用了非阻塞的I/O模型,通过事件轮询机制处理多个客户端请求。这种方式可以在不阻塞其他请求的情况下处理某个请求,提高了系统的并发性能。

  1. 优秀的数据结构支持

Redis支持丰富的数据结构,如字符串、哈希表、列表、集合、有序集合等。这些数据结构的实现经过优化,能够在内存中高效存储和访问数据。

  1. 持久化机制

Redis提供了多种持久化机制,包括RDB快照和AOF日志。虽然Redis将数据存储在内存中,但通过定期将内存中的数据快照到磁盘,可以保障数据的持久性。此外,AOF日志可以记录每次写操作,用于在重启后恢复数据。

  1. 基于事件的通知

Redis支持基于事件的通知机制,客户端可以订阅对某个键进行的操作。这种机制可以用于构建实时性较高的应用,如实时通知系统。

  1. 简单而强大的原子操作:

Redis提供了一系列的原子操作,这些操作可以在一个命令中完成,保证了在多个操作之间的原子性。这对于构建高效的分布式系统非常有帮助。

20、redis的两种落盘方式?

  1. RDB(Redis DataBase)快照持久化

    • RDB是一种周期性将内存中的数据保存到磁盘的方式。它通过在指定的时间间隔内生成数据的快照(快照文件以.rdb为后缀),并将该快照文件保存到磁盘上。这个过程是一个异步操作,Redis会单独fork一个子进程来执行快照的生成工作,父进程继续处理客户端请求。

    • RDB持久化适合用于数据备份、灾难恢复等场景。你可以配置Redis定期生成快照,也可以手动执行SAVE或BGSAVE命令来生成快照。然而,RDB持久化的缺点是,如果系统突然宕机,可能会丢失最后一次快照之后的数据。

  2. AOF(Append-Only File)日志文件持久化

    • AOF持久化是通过将每个写操作追加到一个文件中,以保证数据的持久性。AOF文件是一个只追加、不截断的日志文件。通过记录每个写操作,AOF文件可以在服务器重启时通过重新执行这些写操作来还原数据。

    • AOF持久化适合用于数据的实时恢复和灾难恢复等场景。相比RDB,AOF的缺点是持久化文件通常较大,且重启时恢复速度相对较慢。

21、redis如何做分布式?(不会)

  1. Redis Cluster

  • Redis Cluster是Redis官方提供的分布式解决方案,支持水平分片,将数据分散存储在多个节点上。每个节点只存储部分数据,通过哈希槽(hash slot)将数据划分到不同的节点上。Redis Cluster提供了高可用性和容错性,支持节点的动态扩缩,即可以动态添加或删除节点。

  • Redis Cluster的一些特性:

    • 自动分片:Redis Cluster通过哈希槽自动分片,将数据均匀地分布在多个节点上。

    • 节点间通信:节点之间通过Gossip协议进行通信,用于发现其他节点和维护集群的状态。

    • 主从复制:每个分片有一个主节点和多个从节点,主节点负责读写,从节点负责复制主节点的数据,提供读取和故障转移支持。

    • 故障转移:当主节点发生故障时,Redis Cluster能够通过选举新的主节点来实现故障转移。

    • 客户端分区:客户端可以直接连接到任意一个节点,不需要中间代理。客户端根据哈希槽决定数据应该存储在哪个节点上。

  1. Redis Sentinel

  • Redis Sentinel是用于高可用性(High Availability)的监控系统,可以监控多个Redis实例,发现并通知客户端有关故障转移的信息。虽然Sentinel本身不提供数据分片,但可以与多个独立的Redis实例结合使用,每个实例负责存储一部分数据,从而实现分布式的数据存储。

  • Redis Sentinel的一些特性:

    • 监控:Sentinel负责监控Redis节点的健康状态,及时发现主节点故障。

    • 故障转移:Sentinel可以在主节点故障时自动进行故障转移,选择一个从节点升级为主节点。

    • 配置管理:Sentinel可以根据配置文件中的规则,自动进行节点的添加、删除、故障检测等管理操作。

    • 通知:Sentinel通过发布订阅模式向客户端发送有关节点状态变化的通知。

22、决策树节点分裂的算法?

  1. ID3(Iterative Dichotomiser 3)

  • ID3是早期的决策树算法,它基于信息论中的信息增益来选择最优的特征进行分裂。信息增益的计算使用熵(Entropy)的概念,熵度量了数据的不确定性。在每个节点上,算法计算每个特征的信息增益,选择具有最大信息增益的特征进行分裂。

  • 步骤:

    1. 计算当前节点的熵。

    2. 对于每个特征,计算使用该特征分裂后的加权平均熵。

    3. 计算信息增益,选择信息增益最大的特征进行分裂。

  • ID3的主要问题之一是对于具有大量取值的特征,倾向于选择该特征进行分裂,导致生成的树容易过拟合。

  1. C4.5

  • C4.5是ID3的改进版本,它使用信息增益比(Gain Ratio)来选择最优的特征进行分裂。信息增益比对信息增益进行了归一化,解决了ID3中对具有大量取值特征的偏好问题。

  • 步骤:

    1. 计算当前节点的熵。

    2. 对于每个特征,计算使用该特征分裂后的加权平均熵。

    3. 计算信息增益比,选择信息增益比最大的特征进行分裂。

  • C4.5还引入了剪枝机制,以防止过拟合。它通过递归地将树剪枝,选择在剪枝后损失最小的子树。

23、会Python嘛,是否用过pandas之类的库,平时如何学Python 的

24、leetcode23.合并k个升序链表(用优先队列秒了)

思路:

  1. 分治合并:将链表数组划分为两部分,递归地合并左半部分和右半部分。

  2. 两两合并:设计一个函数用于合并两个升序链表,具体的合并方式可以参考合并两个有序链表的方法。

  3. 递归结束条件:当链表数组为空时,返回空链表;当链表数组中只有一个链表时,返回该链表。

参考代码:

#include 

using namespace std;

// 链表节点的定义
struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(nullptr) {}
};

class Solution {
public:
    // 合并两个升序链表的函数
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        if (!l1) return l2;
        if (!l2) return l1;
        
        if (l1->val < l2->val) {
            l1->next = mergeTwoLists(l1->next, l2);
            return l1;
        } else {
            l2->next = mergeTwoLists(l1, l2->next);
            return l2;
        }
    }

    // 分治合并链表数组的函数
    ListNode* mergeKLists(vector& lists) {
        if (lists.empty()) return nullptr;
        int n = lists.size();
        
        // 分治合并
        while (n > 1) {
            for (int i = 0; i < n / 2; ++i) {
                lists[i] = mergeTwoLists(lists[i], lists[n - 1 - i]);
            }
            n = (n + 1) / 2;
        }
        
        return lists[0];
    }
};

// 用于创建链表的辅助函数
ListNode* createList(vector& nums) {
    ListNode dummy(0);
    ListNode* current = &dummy;
    for (int num : nums) {
        current->next = new ListNode(num);
        current = current->next;
    }
    return dummy.next;
}

// 用于输出链表的辅助函数
void printList(ListNode* head) {
    while (head) {
        cout << head->val << " ";
        head = head->next;
    }
    cout << endl;
}

int main() {
    Solution solution;

    // 示例输入
    vector> input = {{1,4,5},{1,3,4},{2,6}};
    
    // 将示例输入转换为链表数组
    vector lists;
    for (auto& nums : input) {
        lists.push_back(createList(nums));
    }

    // 调用合并链表数组的函数
    ListNode* result = solution.mergeKLists(lists);

    // 输出合并后的链表
    printList(result);

    return 0;
}

25、反问,面试官说主要用c++和python的,同时给了建议可以学一学c++服务端开发方向的内容。

还有一些服务端开发相关的问题记不清了,但其它的基本都答出来了,应该是过了,总体来说360的面试强度还是比较大的。

以上便是这位小伙伴的两面面经,就如其本人来说,面试强度还是蛮大的,所以大家也要加油呀~

你可能感兴趣的:(开发语言,c++,算法,面试,职场和发展)