Bootstrap

CPPer面试高频问题(一)

最近一直在面试,所以想记录一下面试后的一些问题,有些问题在当时我都没有回答上来,做个复盘回忆一下如何在下次的回答中表现得更好!!!

占个坑!明天补充完整!!!

一、cpp及简单八股

1.1如果让你实现一个shared_ptr,那么你会考虑如何实现呢?你有没有进去看它的源码呢(看了很加分哦!hhhh)?使用sharedptr与原始指针,他俩在的使用的性能大概在什么区间呢?

这个问题在面试中一共问了我两次,回答的都不太理想,因为我的关注点更多的在如何保证计数器的原子操作和如何实现复制和拷贝构造函数,但仅仅回答这两点是不够的。因此我打算好好考虑一下这个方面的问题如何进行实现。

1.1.1回答方向?

为什么需要用这个智能指针呢?

当然是为了更好地管理地址啦,如果不使用智能指针的话,我们可能就需要自己手动去delete地址,如果忘了delete地址的话,就会造成内存泄露,导致内存一直不断的上涨,最终导致内存崩溃。(那么这里又可以延伸出一个问题,在linux中如何检查内存泄漏呢?你会用那些命令呢?还有那些软件?)

使用了智能指针,一定就不会内存泄漏吗?

那当然不是啦,有了智能指针还是会有内存泄露的风险。如果两个类当中都对对方进行了重复引用话,那么在析构函数执行的时候,会因为计数器导致计数器无法到0导致内存泄漏。这是一个很严重的问题,在项目中很难排查。因此这就延伸出了weakptr这个智能指针(不对内存资源占有,只对观察内存资源,在使用的时候可以通过expired判断资源是否被释放),这又有的说了hhh。

如果我在一个项目多次使用智能指针的话,会对我的性能有损耗嘛?

先说结论吧。如果频繁使用拷贝的话,性能损耗相比于使用原指针的话,大概是7倍。如果是创建的话,大概是2.2倍。

实现sharedptr实际上是由很大的性能损耗,特别是在进行sharedpte的拷贝操作,如果多次进行拷贝操作的话,可能会造成一部分的性能损耗。那如果你在考虑使用shared的时候需要考虑它的性能损耗,因此你需要实际评估你是不是项目中真的需要sharedptr去管理、或者减少拷贝操作,尽量使用makeshared工厂函数进行优化,或者一个地址不需要共享所有权的话,优先使用uniqueptr(权衡项目复杂度、与sharedptr的性能损耗)。

来做个小实验,对比一下使用原始指针和sharedptr管理的指针:

#include <iostream>
#include <memory>
#include <chrono>
#include <vector>

constexpr int N = 10'000'000; // 操作次数

// 测试对象
struct Foo {
    int value;
    Foo(int v) : value(v) {}
};

// 测试 1: shared_ptr 的创建和销毁
void test_shared_ptr_create_destroy() {
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < N; ++i) {
        auto p = std::make_shared<Foo>(42);
    }
    auto end = std::chrono::high_resolution_clock::now();
    std::cout << "shared_ptr create/destroy: " 
              << std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count() / N 
              << " ns/op\n";
}

// 测试 2: 原始指针的创建和销毁(手动管理内存)
void test_raw_ptr_create_destroy() {
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < N; ++i) {
        Foo* p = new Foo(42);
        delete p;
    }
    auto end = std::chrono::high_resolution_clock::now();
    std::cout << "raw_ptr create/destroy: " 
              << std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count() / N 
              << " ns/op\n";
}

// 测试 3: shared_ptr 的拷贝构造
void test_shared_ptr_copy() {
    auto p = std::make_shared<Foo>(42);
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < N; ++i) {
        auto p2 = p;
    }
    auto end = std::chrono::high_resolution_clock::now();
    std::cout << "shared_ptr copy: " 
              << std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count() / N 
              << " ns/op\n";
}

// 测试 4: 原始指针的赋值
void test_raw_ptr_copy() {
    Foo* p = new Foo(42);
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < N; ++i) {
        Foo* p2 = p;
    }
    auto end = std::chrono::high_resolution_clock::now();
    delete p;
    std::cout << "raw_ptr copy: " 
              << std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count() / N 
              << " ns/op\n";
}

int main() {
    test_shared_ptr_create_destroy();
    test_raw_ptr_create_destroy();
    test_shared_ptr_copy();
    test_raw_ptr_copy();
    return 0;
}

实验结果如下:

在以下环境中运行(Clang 15, O2优化, x86-64 CPU):

操作shared_ptr (ns/op)原始指针 (ns/op)差异倍数
创建和销毁15.26.82.2x
拷贝操作2.10.37x
解引用访问(未列代码)~0.5~0.51x

那么可以得到以下结论:

  1. 创建和销毁

    • shared_ptr 比原始指针慢 2~3 倍,主要因为控制块的内存分配和原子操作。

    • 如果使用 std::make_shared(推荐使用),可将对象和控制块合并为单次内存分配,优化后差异缩小。

  2. 拷贝操作

    • shared_ptr 拷贝需要原子操作,比原始指针赋值慢 5~10 倍。

    • 高频拷贝场景(如容器操作)需谨慎使用 shared_ptr

  3. 解引用访问

    • 无显著差异,因为解引用只是通过指针访问内存。

1.1.2 如何自己实现呢?

那么首先你需要考虑引用计数(这个引用计数必须是原子性的操作)、线程安全、控制块管理、weakptr的交互这几个大的方面。

引用计数(保证操作的原子性)

  • weakptr的支持
  • 拷贝赋值构造
  • 控制块管理
  • 其他函数(例如get等获取原始指针的方法)

代码(大概脑子有个大概的模型就可以了):

//控制块的设计
class ControlBlockBase {
public:
    virtual ~ControlBlockBase() = default;
    virtual void dispose() noexcept = 0;  // 销毁对象
    virtual void destroy() noexcept = 0;  // 销毁控制块自身
    std::atomic<size_t> shared_count{1};
    std::atomic<size_t> weak_count{0};
};

template<typename T, typename Deleter>
class ControlBlockImpl : public ControlBlockBase {
public:
    ControlBlockImpl(T* ptr, Deleter deleter) : ptr(ptr), deleter(deleter) {}

    void dispose() noexcept override {
        deleter(ptr);
        ptr = nullptr;
    }

    void destroy() noexcept override {
        delete this;
    }

private:
    T* ptr;
    Deleter deleter;
};

//sharedptr
template<typename T>
class shared_ptr {
public:
    // 默认构造函数
    shared_ptr() : ptr(nullptr), ctrl(nullptr) {}

    // 从原始指针构造
    template<typename Deleter = std::default_delete<T>>
    explicit shared_ptr(T* p, Deleter d = Deleter()) {
        if (p) {
            ctrl = new ControlBlockImpl<T, Deleter>(p, d);
            ptr = p;
        }
    }

    // 拷贝构造函数
    shared_ptr(const shared_ptr& other) : ptr(other.ptr), ctrl(other.ctrl) {
        if (ctrl) ctrl->shared_count.fetch_add(1, std::memory_order_relaxed);
    }

    // 移动构造函数
    shared_ptr(shared_ptr&& other) noexcept : ptr(other.ptr), ctrl(other.ctrl) {
        other.ptr = nullptr;
        other.ctrl = nullptr;
    }

    // 析构函数
    ~shared_ptr() {
        if (!ctrl) return;
        // 减少shared计数,若归零则销毁对象
        if (ctrl->shared_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
            ctrl->dispose();
            // 若weak计数也为零,销毁控制块
            if (ctrl->weak_count.load(std::memory_order_acquire) == 0) {
                ctrl->destroy();
            }
        }
    }

private:
    T* ptr;
    ControlBlockBase* ctrl;
};

//补充一个weakptr的实现
template<typename T>
class weak_ptr {
public:
    weak_ptr(const shared_ptr<T>& sp) : ctrl(sp.ctrl) {
        if (ctrl) ctrl->weak_count.fetch_add(1, std::memory_order_relaxed);
    }

    ~weak_ptr() {
        if (!ctrl) return;
        // 减少weak计数,若归零且shared计数为零,则销毁控制块
        if (ctrl->weak_count.fetch_sub(1, std::memory_order_acq_rel) == 1 &&
            ctrl->shared_count.load(std::memory_order_acquire) == 0) {
            ctrl->destroy();
        }
    }

private:
    ControlBlockBase* ctrl;
};

1.1.3 看看源码

这一篇写的挺好的

从零开始写一个shared_ptr-源代码解析和一些常见面试问答 - 知乎

1.2stl中的unorder_map与map的结构是什么?map结构的查找的时间复杂度呢?unordered_map呢(你需要考虑他的结构)?

这个问题是在面试腾讯cpp实习的时候问的我,当时有点懵,没有考虑到hash冲突的情况,脑子有点蒙蒙的,答得也不太行。在这一部分我打算稍微详细的解释一下这俩这的区别。

unorderedmap的实现方式是hash表、map的话是红黑树。详细介绍如下:

1.2.1 std::map 的结构与时间复杂度

结构

std::map 基于 红黑树(Red-Black Tree) 实现,核心特性如下:

  • 红黑树规则

    1. 每个节点是红色或黑色。

    2. 根节点是黑色。

    3. 所有叶子节点(NIL 节点)是黑色。

    4. 红色节点的子节点必须是黑色(即不能有连续的红色节点)。

    5. 从任一节点到其所有叶子节点的路径包含相同数量的黑色节点。
      这些规则保证树的高度近似平衡,避免退化成链表。

查找时间复杂度
  • 平均和最坏情况均为 O(log n),因为红黑树是平衡二叉搜索树,树高始终为 O(log n)


1.2.2. std::unordered_map 的结构与时间复杂度

结构

std::unordered_map 基于 哈希表(Hash Table) 实现,核心组件如下:

  1. 哈希函数:将键映射到桶(Bucket)的索引,例如:

    size_t hash = std::hash<Key>{}(key) % bucket_count();
  2. 桶(Bucket):每个桶存储一个链表(或动态数组),用于解决哈希冲突(链地址法)。

  3. 负载因子(Load Factor)元素数量 / 桶数量,当负载因子超过阈值(默认 1.0)时,触发扩容(rehash),重新分配桶并重新哈希所有元素。

查找时间复杂度
  • 平均情况O(1),假设哈希函数分布均匀,冲突较少。

  • 最坏情况O(n),当所有键哈希到同一个桶时,退化为链表遍历。


1.2.3. 关键对比

特性std::map (红黑树)std::unordered_map (哈希表)
有序性键按升序排列无序
查找时间O(log n)(稳定)O(1)(平均),O(n)(最坏)
内存开销较高(树节点存储父子指针)较低(桶+链表指针)
适用场景需要有序遍历或范围查询高频单点查询,无需顺序

1.2.4. 实验验证查找性能

测试代码
#include <iostream>
#include <map>
#include <unordered_map>
#include <chrono>
#include <random>

constexpr int N = 1'000'000;

int main() {
    std::map<int, int> m;
    std::unordered_map<int, int> um;

    // 插入数据
    for (int i = 0; i < N; ++i) {
        m[i] = i;
        um[i] = i;
    }

    // 测试 std::map 查找
    auto start_map = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < N; ++i) {
        auto it = m.find(i);
    }
    auto end_map = std::chrono::high_resolution_clock::now();

    // 测试 std::unordered_map 查找
    auto start_um = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < N; ++i) {
        auto it = um.find(i);
    }
    auto end_um = std::chrono::high_resolution_clock::now();

    // 输出结果
    std::cout << "std::map 平均查找时间: "
              << std::chrono::duration_cast<std::chrono::nanoseconds>(end_map - start_map).count() / N
              << " ns\n";

    std::cout << "std::unordered_map 平均查找时间: "
              << std::chrono::duration_cast<std::chrono::nanoseconds>(end_um - start_um).count() / N
              << " ns\n";
}
示例结果(优化编译 O2)
容器平均查找时间 (ns/次)
std::map~100 ns
std::unordered_map~30 ns

1.2.5. 总结

  • std::map:适合需要有序性的场景,查找稳定但较慢(O(log n))。

  • std::unordered_map:适合高频单点查询,平均 O(1),但需注意哈希函数质量和负载因子。

  • 选择依据:权衡有序性、性能要求和哈希冲突风险。

1.3.线程与进程通信方式有哪几种?详细介绍一下?介绍一下信号与条件变量(最好介绍一下他俩在性能损耗的数量级)?

这个问题的话,无论是小米、腾讯、字节等等都问过我,而我答得只能说是一般般。这个问题我觉得是很重要的一个知识点,因为在实际的生产工作中,多线程的方式是必不可少的,要实现多任务的工作,这一点是必不可少的。在后面我可能考虑分析分析libuv、WebRTC(浅浅挖个坑)!!

进程间通信方式

进程间通信的方式一般有管道、消息队列、信号量、共享内存、网络套接字以及其他一些方式。

  1. 管道(Pipe)

    • 匿名管道:单向通信,仅用于父子进程。

    • 命名管道(FIFO):通过文件系统路径访问,支持无亲缘关系的进程。

    • 性能:基于内核缓冲区的数据流,每次读写涉及用户态与内核态切换,吞吐量较低(约 100MB/s)。

  2. 消息队列(Message Queue)

    • 内核维护的消息链表,支持优先级和异步通信。

    • 性能:频繁的系统调用导致延迟较高(单次操作约 1~10μs)。

  3. 共享内存(Shared Memory)

    • 多个进程映射同一块物理内存,无需内核介入,直接读写。

    • 性能:最快(访问速度接近普通内存,纳秒级),但需同步机制(如信号量)。

  4. 信号量(Semaphore)

    • 用于协调共享资源的访问,基于原子计数器实现互斥或同步。

    • 性能:轻量级系统调用(如 Linux 的 futex,无竞争时用户态完成,约 10~100ns)。

  5. 信号(Signal)通过自定义信号达到通知的目的

    • 异步通知机制,如 SIGINT(Ctrl+C)或自定义信号。

    • 性能:上下文切换开销大(单次信号处理约 1~10μs)

  6. 套接字(Socket)

    • 支持跨网络通信,如 TCP/UDP。

    • 性能:受协议栈和网络延迟影响,本地回环(loopback)约 1~10μs/次。

线程间通信方式

线程间通信方式一般有共享内存、互斥锁、条件变量、信号量等。

  1. 共享内存

    • 天然共享进程内存,需通过锁或原子操作同步。

    • 性能:直接内存访问(纳秒级),同步机制是关键瓶颈。

  2. 互斥锁(Mutex)

    • 保护共享资源,避免竞态条件。

    • 性能:无竞争时约 10~50ns(用户态自旋),竞争时可能触发系统调用(约 1μs)。

  3. 条件变量(Condition Variable)

    • 允许线程等待特定条件成立,需搭配互斥锁使用。

    • 性能:唤醒等待线程的开销约 100ns~1μs,详见下文分析。

  4. 信号量(Semaphore)

    • 线程间同步,与进程信号量类似。

    • 性能:与互斥锁相近,但更通用。

信号(Signal)与条件变量(Condition Variable)

1. 信号(Signal)
  • 机制
    信号是内核向进程发送的异步事件通知,例如:

    • SIGTERM:终止进程。

    • SIGSEGV:段错误。

    • 自定义信号(如 SIGUSR1)。

  • 使用场景

    • 进程间简单通知(如终止请求)。

    • 异常处理(如内存错误)。

  • 性能损耗

    • 发送信号:用户态到内核态的上下文切换(约 0.1~1μs)。

    • 信号处理

      1. 内核中断当前执行流,切换到信号处理函数。

      2. 处理函数需符合异步信号安全(如不可调用 malloc)。

      3. 返回后恢复原上下文。
        总延迟:约 1~10μs/次。

  • 示例代码

    #include <signal.h>
    #include <unistd.h>
    
    void handler(int sig) {
        write(STDOUT_FILENO, "Signal received!\n", 17);
    }
    
    int main() {
        signal(SIGUSR1, handler);
        raise(SIGUSR1);  // 发送信号
        return 0;
    }

2. 条件变量(Condition Variable)
  • 机制
    条件变量用于线程在特定条件下等待或通知其他线程,需配合互斥锁:

    1. 线程A获取锁,检查条件,若条件不满足则等待。

    2. 线程B修改条件后,通过信号唤醒等待线程。

    3. 线程A被唤醒后重新检查条件。

  • 使用场景

    • 生产者-消费者模型(缓冲区空/满时阻塞)。

    • 任务队列同步(工作线程等待任务)。

  • 性能损耗

    • 等待(wait)

      • 若条件已满足,直接返回(约 10~50ns)。

      • 否则,释放锁并进入休眠(触发系统调用,约 1μs)。

    • 通知(notify)

      • pthread_cond_signal:唤醒单个线程(约 100ns~1μs)。

      • pthread_cond_broadcast:唤醒所有线程(开销与线程数线性相关)。

  • 示例代码

    #include <mutex>
    #include <condition_variable>
    
    std::mutex mtx;
    std::condition_variable cv;
    bool ready = false;
    
    void worker() {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, []{ return ready; }); // 等待 ready 为 true
        // 执行任务
    }
    
    void controller() {
        {
            std::lock_guard<std::mutex> lock(mtx);
            ready = true;
        }
        cv.notify_all(); // 唤醒所有等待线程
    }

性能对比(信号 vs 条件变量)

操作信号条件变量
单次操作延迟1~10 μs100 ns~1 μs
上下文切换用户态↔内核态用户态为主
适用场景跨进程异步通知线程间同步
吞吐量(高频操作)低(约 1e5 ops/s)高(约 1e7 ops/s)

总结

  • 信号:适用于跨进程的简单异步通知,但性能开销大,不适合高频场景。

  • 条件变量:线程间同步的首选,低延迟且吞吐量高,但需合理设计条件检查逻辑。

  • 选择原则

    • 若需跨进程通信 → 信号、共享内存、消息队列。

    • 若需线程间高效同步 → 条件变量 + 互斥锁。

1.4线程与协程的区别?线程和协程在底层实现上有什么区别吗?他俩在性能上的数量级别是什么?如何在cpp中如何手动实现一个协程呢?

这一步是在腾讯的面试的时候问到的,我答得不是很好。我说不上线程与协程的底层原理。

1.4.1. 核心概念对比
特性线程(Thread)协程(Coroutine)
调度方式由操作系统内核调度(抢占式)由用户程序主动调度(协作式)
上下文切换需要内核介入,涉及用户态↔内核态切换纯用户态切换,无需内核参与
内存开销每个线程需独立栈(默认1~8MB)协程栈可定制(通常KB级)
并发粒度适合粗粒度任务(如CPU密集型)适合高并发、I/O密集型任务(如网络服务)
典型应用多核并行计算、阻塞操作异步I/O、事件循环、生成器(Generator)

1.4.2. 底层实现差异

线程的实现
  • 内核线程:由操作系统直接管理,通过系统调用(如 pthread_create)创建。

  • 上下文切换

    1. 保存当前线程的寄存器状态(PC、SP等)到内核栈。

    2. 加载目标线程的上下文。

    3. 切换过程涉及内核态切换,开销较大(约 1~10μs)。

协程的实现
  • 用户态调度:协程的切换完全由用户代码控制,常见实现方式:

    1. 栈切换:为每个协程分配独立栈空间(如 ucontext 或手动汇编切换)。

    2. 状态机:通过 switch-case 或 goto 模拟协程状态流转(无栈协程)。

  • 轻量级切换:仅需保存少量寄存器(如PC、SP),无需内核介入(约 10~100ns)。


1.4.3. 性能数量级对比

操作线程协程
创建开销10~100μs10~100ns
切换开销1~10μs10~100ns
内存占用1~8MB/线程1~100KB/协程
并发能力千级(受内核限制)百万级(用户态可控)

1.4.4 实现协程的几种办法

由于协程是在用户态的一种轻量级操作——切换开销小、适用于io密集任务等优点。因此大部分的协程都是通过线程来完成的。手动实现协程也有几种办法,一是使用第三方库(如 Boost.Coroutine2)、二是利用状态机完成协程、三则是利用汇编的上下文切换操作。

在这里我使用状态机完成协程。代码如下:

在以下代码我们可以发现,我们可以通过不断检查状态机的状态,得知我们的线程执行到哪一步,他的状态信息都保存在用户态(大量减少了线程切换的消耗)。如果有大量的io任务,我们可以通过协程来完成,通过不断检查线程的状态得知我们的协程完成到哪一步,从而判断可不可以继续执行我们的任务。

#include <iostream>
#include <functional>

// 协程状态枚举
enum class CoroutineState {
    Start,      // 初始状态
    State1,     // 执行第一步
    State2,     // 执行第二步
    State3,     // 执行第三步
    Done        // 协程结束
};

// 协程对象
class Coroutine {
public:
    Coroutine() : state(CoroutineState::Start) {}

    // 恢复协程执行
    bool resume() {
        switch (state) {
            case CoroutineState::Start:
                return runStart();
            case CoroutineState::State1:
                return runState1();
            case CoroutineState::State2:
                return runState2();
            case CoroutineState::State3:
                return runState3();
            case CoroutineState::Done:
                return false;
            default:
                return false;
        }
    }

    // 是否已完成
    bool isDone() const {
        return state == CoroutineState::Done;
    }

private:
    CoroutineState state;  // 当前状态
    int localVar = 0;      // 模拟需要保存的局部变量

    // 各状态执行逻辑
    bool runStart() {
        std::cout << "Coroutine started.\n";
        localVar = 0;      // 初始化局部变量
        state = CoroutineState::State1;
        return true;
    }

    bool runState1() {
        localVar++;
        std::cout << "State1: localVar = " << localVar << "\n";
        state = CoroutineState::State2;
        return true;
    }

    bool runState2() {
        localVar *= 2;
        std::cout << "State2: localVar = " << localVar << "\n";
        state = CoroutineState::State3;
        return true;
    }

    bool runState3() {
        localVar += 100;
        std::cout << "State3: localVar = " << localVar << "\n";
        state = CoroutineState::Done;
        return false;      // 返回false表示协程结束
    }
};

int main() {
    Coroutine coro;

    // 手动逐步恢复协程
    while (coro.resume()) {
        std::cout << "Main thread: waiting for coroutine...\n";
    }

    std::cout << "Coroutine finished.\n";
    return 0;
}

总结

  • 线程:适合利用多核的CPU密集型任务,但并发数受限于内核资源。

  • 协程:适合高并发I/O密集型任务(如Web服务器),通过用户态调度实现极低开销。

二、网络基础

2.1、网络协议的模型你知道吗?分别有哪些呢?每一层各有什么协议?

这个问题是腾讯问题的,我扪心自问,我由于之前的学习过程中,我只有个基本的概念,比如七层osi模型与tcp/ip四层模型。osi中的表示层与会话层具体的作用是什么,作用场景是什么,我并没有一个很好的掌握,回答的不是很好。

关于这方面内容,可以自己去小林coding去看相关知识,这方面的知识太复杂了,我没办法一下子进行总结,我在这里给出几个在阅读中需要考虑的知识点,带着这些个问题去看相关资料会比较好(这些也基本都是在面试中面试官会问到的问题)。

网络基础相关(这一层腾讯必问)

OSI七层模型每一层是什么?分别是干什么的?

TCP/IP每一层模型是什么?分别是干什么的?

TCP/IP每一层介绍一点标志性的协议是什么?

TCP/UDP是什么?他俩的区别是什么?

TCP/UDP报文结构是什么?

TCP/UDP可以用相同的端口吗?

TCP与HTTP的KeepAlive是同一个东西吗?

TCP/IP协议三次握手?在三次握手的时候都分别携带了那些信息?详细介绍一下?

为什么是三次握手?不能是四次握手或者两次握手呢?

什么时候会出现四次挥手呢?

为什么四次挥手之后客户端会等待一段时间才完全关闭呢?

挥手为什么是四次?为什么不能是三次?

传输的时候TCP报文的最大承载量?

TCP沾包现象?如何解决呢?

如何通过udp实现tcp呢?

那么你介绍一下quic协议?

http相关

介绍一下http1.0、http1.1、http2.0、http3.0?然后对比一下他们的相同点、不同点?

介绍一下http的一些基本的命令?

介绍一下HTTP的缓存机制?

介绍一下https?介绍一下https的加密过程?

介绍一下TLS加密的握手流程?

Ping的工作原理(小米面试问到了)?

其他

介绍一下NAT协议?那么介绍一下CIDR(腾讯问到了)?

介绍一下ICMP协议?

介绍一下ARP、DHCP、DNS等协议?

参考

图解网络介绍 | 小林coding

三、数据库

其实数据库对于cpp选手可能并不是最重要的部分,但是多了解了解,以防他们真的问到了【哭】

像腾讯、字节、美团的话大部分都用到了go语言去开发,如果你有数据库等相关知识,即使你学的是cpp,那他们也会考虑你的。

3.1、redis与mysql的数据一致性如何保证?

这个问题为什么会出现。我觉得主要有两个原因,一是因为我自己在简历中体现了我学过了这两个技术,二是因为我投的就是后台开发。这俩是必不可少的,你必须了解其中的原理,你才能解决实际生产应用中碰到的问题。

这篇讲的挺好的。总结就是先更新数据库,再删除redis。

考虑到如果有很多的连接请求的话,那么可以用更新数据库再更新redis(同时再redis做一个小的过期时间,为了减少一点读取到错误的数据,又或者是加一个分布式锁的方法)

数据库和缓存如何保证一致性? | 小林coding

图解MySQL介绍 | 小林coding

图解Redis介绍 | 小林coding

四、项目类

4.1、redis如何实现呢?

那么这个就是我的项目了,我之前做个一个特别简单的redis的实现。代码很简单,因此我需要做一个整体性的复盘工作。在这一部分问我最多的就是如何实现他的数据结构!!!问了我两次。

redis的数据结构如何实现?

如何实现哨兵模式?

如何实现数据持久化?

最后加一个,一个空的class,它占多少个字节呢?如果里面的函数都是纯虚函数呢?

可以自行验证

#include <iostream>

class Empty {};

class Abstract {
public:
    virtual void func() = 0;
};

int main() {
    std::cout << "sizeof(Empty)   = " << sizeof(Empty) << " byte\n";
    std::cout << "sizeof(Abstract) = " << sizeof(Abstract) << " bytes\n";
}

总结:

类类型大小原因
空类(无成员)1字节(由C++标准保证,确保每个对象有唯一地址)保证对象地址唯一性
含虚函数的类指针大小(4/8字节)虚表指针(vptr)占用空间
;