14 场景代码题-面试题

14.1 手撕C/C++

手撕array?

#include <iostream>
#include <stdexcept>  // for std::out_of_range
#include <algorithm>  // for std::copy

template <typename T, size_t N>
class MyArray {
private:
    T data_[N];  // 固定大小的连续内存

public:
    // 默认构造函数
    MyArray() = default;

    // 初始化列表构造(C++11,推荐)
    MyArray(std::initializer_list<T> init) {
        if (init.size() > N) {
            throw std::out_of_range("Too many elements in initializer_list");
        }
        std::copy(init.begin(), init.end(), data_);
        // 如果传入的元素少于N,剩余部分是未初始化的!
    }

    // 拷贝构造函数
    MyArray(const MyArray& other) {
        std::copy(other.data_, other.data_ + N, data_);
    }

    // 拷贝赋值运算符
    MyArray& operator=(const MyArray& other) {
        if (this != &other) {
            std::copy(other.data_, other.data_ + N, data_);
        }
        return *this;
    }

    // 移动构造函数(C++11,加分)
    MyArray(MyArray&& other) noexcept {
        std::copy(other.data_, other.data_ + N, data_);
    }

    // 移动赋值运算符(C++11,加分)
    MyArray& operator=(MyArray&& other) noexcept {
        if (this != &other) {
            std::copy(other.data_, other.data_ + N, data_);
        }
        return *this;
    }

    // 访问元素 [],不检查边界
    T& operator[](size_t index) {
        return data_[index];
    }

    const T& operator[](size_t index) const {
        return data_[index];
    }

    // 安全访问,带边界检查
    T& at(size_t index) {
        if (index >= N) {
            throw std::out_of_range("Index out of range");
        }
        return data_[index];
    }

    const T& at(size_t index) const {
        if (index >= N) {
            throw std::out_of_range("Index out of range");
        }
        return data_[index];
    }
    // 获取数组大小(编译期常量,但封装成函数)
    constexpr size_t size() const {
        return N;
    }

    // 是否为空(固定大小,一般不为空)
    constexpr bool empty() const {
        return N == 0;
    }

    // 返回第一个元素
    T& front() {
        return data_[0];
    }

    const T& front() const {
        return data_[0];
    }

    // 返回最后一个元素
    T& back() {
        return data_[N - 1];
    }

    const T& back() const {
        return data_[N - 1];
    }

    // 迭代器支持(让MyArray支持范围for循环)
    T* begin() {
        return data_;
    }

    T* end() {
        return data_ + N;
    }

    const T* begin() const {
        return data_;
    }

    const T* end() const {
        return data_ + N;
    }

    const T* cbegin() const {
        return data_;
    }

    const T* cend() const {
        return data_ + N;
    }
};

手撕vector?

#include <iostream>
#include <stdexcept> // for std::out_of_range
#include <algorithm> // for std::copy

template <typename T>
class MyVector {
private:
    T* data_;          // 指向堆上数组的指针
    size_t size_;      // 当前存储的元素个数
    size_t capacity_;  // 当前分配的容量

    // 扩容函数(私有)
    void resize_if_needed() {
        if (size_ >= capacity_) {
            size_t new_capacity = (capacity_ == 0) ? 1 : capacity_ * 2;
            reserve(new_capacity);
        }
    }

public:
    // 默认构造函数
    MyVector() : data_(nullptr), size_(0), capacity_(0) {}

    // 带初始容量的构造
    explicit MyVector(size_t capacity) 
        : data_(new T[capacity]), size_(0), capacity_(capacity) {}

    // 拷贝构造函数
    MyVector(const MyVector& other)
        : data_(new T[other.capacity_]), size_(other.size_), capacity_(other.capacity_) {
        for (size_t i = 0; i < size_; ++i) {
            data_[i] = other.data_[i]; // 调用元素的拷贝构造
        }
    }

    // 移动构造函数(C++11,加分项)
    MyVector(MyVector&& other) noexcept 
        : data_(other.data_), size_(other.size_), capacity_(other.capacity_) {
        other.data_ = nullptr;
        other.size_ = 0;
        other.capacity_ = 0;
    }

    // 析构函数
    ~MyVector() {
        delete[] data_;
    }

    // 拷贝赋值运算符
    MyVector& operator=(const MyVector& other) {
        if (this != &other) {
            // 先释放原有资源
            delete[] data_;

            // 分配新空间
            capacity_ = other.capacity_;
            size_ = other.size_;
            data_ = new T[capacity_];

            // 拷贝元素
            for (size_t i = 0; i < size_; ++i) {
                data_[i] = other.data_[i];
            }
        }
        return *this;
    }

    // 移动赋值运算符(加分项)
    MyVector& operator=(MyVector&& other) noexcept {
        if (this != &other) {
            delete[] data_;

            data_ = other.data_;
            size_ = other.size_;
            capacity_ = other.capacity_;

            other.data_ = nullptr;
            other.size_ = 0;
            other.capacity_ = 0;
        }
        return *this;
    }
    // 添加元素到末尾
    void push_back(const T& value) {
        resize_if_needed();
        data_[size_] = value;
        ++size_;
    }

    // 添加元素到末尾(移动语义,加分)
    void push_back(T&& value) {
        resize_if_needed();
        data_[size_] = std::move(value);
        ++size_;
    }

    // 删除末尾元素
    void pop_back() {
        if (size_ > 0) {
            --size_;
        }
    }

    // 访问元素 [] 不检查边界
    T& operator[](size_t index) {
        return data_[index];
    }

    const T& operator[](size_t index) const {
        return data_[index];
    }

    // 访问元素,带边界检查
    T& at(size_t index) {
        if (index >= size_) {
            throw std::out_of_range("Index out of range");
        }
        return data_[index];
    }

    const T& at(size_t index) const {
        if (index >= size_) {
            throw std::out_of_range("Index out of range");
        }
        return data_[index];
    }
    // 容量相关接口
    size_t size() const { return size_; }
    size_t capacity() const { return capacity_; }
    bool empty() const { return size_ == 0; }

    // 预留空间
    void reserve(size_t new_capacity) {
        if (new_capacity <= capacity_) return;

        T* new_data = new T[new_capacity];
        for (size_t i = 0; i < size_; ++i) {
            new_data[i] = data_[i]; // 调用拷贝构造
        }

        delete[] data_;
        data_ = new_data;
        capacity_ = new_capacity;
    }

    // 改变大小
    void resize(size_t new_size) {
        if (new_size > capacity_) {
            reserve(new_size);
        }

        if (new_size > size_) {
            // 默认初始化新增的元素(调用T的默认构造)
            for (size_t i = size_; i < new_size; ++i) {
                data_[i] = T(); // 或者使用 placement new 更精细控制
            }
        }

        size_ = new_size;
    }

    void resize(size_t new_size, const T& value) {
        if (new_size > capacity_) {
            reserve(new_size);
        }

        if (new_size > size_) {
            for (size_t i = size_; i < new_size; ++i) {
                data_[i] = value;
            }
        }

        size_ = new_size;
    }

    // 迭代器支持(简化,可选)
    T* begin() { return data_; }
    T* end() { return data_ + size_; }

    const T* begin() const { return data_; }
    const T* end() const { return data_ + size_; }
};

手撕string?

#include <cstring>  // for strlen, strcpy
#include <utility>  // for std::swap (C++11)

class MyString {
public:
    // 1. 默认构造函数
    MyString() : data_(nullptr), size_(0) {}

    // 2. 从 C 风格字符串构造
    MyString(const char* str) {
        if (str) {
            size_ = strlen(str);
            data_ = new char[size_ + 1];  // +1 for '\0'
            strcpy(data_, str);
        } else {
            data_ = nullptr;
            size_ = 0;
        }
    }

    // 3. 析构函数
    ~MyString() {
        delete[] data_;  // 安全,delete nullptr 是合法的
    }

    // 4. 拷贝构造函数(深拷贝)
    MyString(const MyString& other) : data_(nullptr), size_(0) {
        if (other.data_) {
            size_ = other.size_;
            data_ = new char[size_ + 1];
            strcpy(data_, other.data_);
        }
    }

    // 5. 拷贝赋值运算符(使用 copy-and-swap 惯用法,更安全、更简洁)
    MyString& operator=(MyString other) {  // 注意:传值,已经调用了拷贝构造
        swap(*this, other);                // 交换当前对象与传进来的副本
        return *this;                      // other 析构时会释放旧内存
    }
    
    // 移动构造
    MyString(MyString&& other) noexcept 
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }
    
    // 移动赋值
    MyString& operator=(MyString&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

    // 6. swap 函数(用于 copy-and-swap)
    friend void swap(MyString& a, MyString& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }

    // (可选加分项)获取 C 风格字符串
    const char* c_str() const { return data_ ? data_ : ""; }

    // (可选加分项)获取字符串长度
    size_t size() const { return size_; }
    
    char& operator[](size_t index) {
        // 注意:这里不做越界检查,真实项目中建议检查!
        return data_[index];
    }

private:
    char* data_;
    size_t size_;
};

手撕printf?

#include <stdio.h>
#include <stdarg.h>
#include <unistd.h>  // for write()

// 辅助函数:输出单个字符(可用 putchar 或 write)
static void my_putchar(char c) {
    putchar(c);  // 或者使用 write(1, &c, 1);
}

// 辅助函数:整数转字符串(十进制,支持正负,不反向)
// 注意:这里为了简化,直接输出每一位,不返回字符串
static void print_int(int num) {
    if (num == 0) {
        my_putchar('0');
        return;
    }

    // 处理负数
    if (num < 0) {
        my_putchar('-');
        num = -num;
    }

    char digits[12]; // 足够存放下 32-bit int
    int i = 0;

    while (num > 0) {
        digits[i++] = '0' + (num % 10);
        num /= 10;
    }

    // 倒序输出
    while (i > 0) {
        my_putchar(digits[--i]);
    }
}

// 辅助函数:输出字符串
static void print_string(const char *str) {
    if (!str) {
        my_putchar('(');
        my_putchar('n');
        my_putchar('u');
        my_putchar('l');
        my_putchar('l');
        my_putchar(')');
        return;
    }
    while (*str) {
        my_putchar(*str++);
    }
}

// 辅助函数:输出字符
static void print_char(char c) {
    my_putchar(c);
}

// 主函数:简化版 printf
int my_printf(const char *format, ...) {
    va_list args;
    va_start(args, format);

    int count = 0;

    while (*format) {
        if (*format == '%') {
            ++format; // 跳过 %

            switch (*format) {
                case 'c': {
                    char c = (char)va_arg(args, int); // char 在可变参数中提升为 int
                    print_char(c);
                    ++count;
                    break;
                }
                case 's': {
                    char *str = va_arg(args, char*);
                    print_string(str);
                    // 粗略计算长度
                    const char *s = str;
                    while (s && *s) { ++count; ++s; }
                    break;
                }
                case 'd':
                case 'i': {
                    int num = va_arg(args, int);
                    print_int(num);
                    // 粗略计算数字位数
                    if (num == 0) ++count;
                    else {
                        int tmp = num < 0 ? -num : num;
                        while (tmp > 0) { ++count; tmp /= 10; }
                        if (num < 0) ++count; // '-'
                    }
                    break;
                }
                case '%': {
                    my_putchar('%');
                    ++count;
                    break;
                }
                default: {
                    // 未知格式符,原样输出(或可报错)
                    my_putchar('%');
                    my_putchar(*format);
                    count += 2;
                    break;
                }
            }
        } else {
            // 普通字符
            my_putchar(*format);
            ++count;
        }
        ++format;
    }

    va_end(args);
    return count; // 返回打印的字符数,模仿标准 printf
}
(追问)C的printf是线程安全的吗?你设计的printf是线程安全的吗?

C的printf原理:当调用 printf 时,首先进行可变参数处理,接着由 glibc的 vfprintf 函数解析格式化字符串,再将数据转换为文本并写入用户态缓冲区。缓冲区满或遇到换行时,write系统调用将数据发送到内核。

并非线程安全。

如何设计一个线程安全的类?

  1. 使用mutex,但是需要注意死锁问题。

  2. 使用原子变量、CAS。

  3. 避免构造/析构期间暴露this指针。

一个C++程序有什么情况会导致宕机?进程崩溃?段错误?

  1. 空指针解引用

  2. 野指针 —— 访问已释放的内存

  3. 访问越界的数组

  4. 使用未初始化的指针

  5. 访问已释放的栈对象

  6. 双重释放

  7. 访问非法内存地址(比如 0x1、0xDEADBEEF)

  8. 栈溢出

  9. 使用非法类型转换或访问虚函数表错误(UB)

  10. 断言失败(assert() 触发)

  11. 访问未对齐的内存

  12. 多线程数据竞争,导致内存破坏

手撕move?

#include <iostream>
#include <type_traits> // 仅用于 std::remove_reference,可选,我们也可以不用

// 简易版 my_move,不依赖 <utility> 或 <type_traits>
template <typename T>
typename std::remove_reference<T>::type&& my_move(T&& arg) noexcept {
    return static_cast<typename std::remove_reference<T>::type&&>(arg);
}

// 如果你不想用 type_traits,也可以这样写(更基础的方式):
/*
template <typename T>
T&& my_move(T&& arg) noexcept {
    return static_cast<T&&>(arg);
}
*/

手撕forward?

#include <iostream>
#include <type_traits> // for std::remove_reference

// 简易版 my_forward
template <typename T>
T&& my_forward(typename std::remove_reference<T>::type& arg) noexcept {
    return static_cast<T&&>(arg);
}

14.2 手撕数据结构

【必问】手撕智能指针?

shared_ptr?
template<typename T>
struct ControlBlock {
    T* ptr;                                     // 指向实际对象
    std::atomic<size_t> use_count;              // 强引用计数(shared_ptr 个数)
    std::atomic<size_t> weak_count;             // 弱引用计数(weak_ptr 个数)

    // 构造函数:有对象时
    explicit ControlBlock(T* p)
        : ptr(p), use_count(1), weak_count(0) {}

    // 析构函数:不负责删除 ptr,由 shared_ptr 在 use_count==0 时处理
    ~ControlBlock() {
        // 注意:我们不在 ControlBlock 析构函数里 delete ptr
        // 因为只有当 use_count == 0 时,才应该 delete ptr
        // 所以由 shared_ptr 来负责对象的销毁
    }
};
#include <atomic>
#include <utility> // for std::swap

template <typename T>
class shared_ptr {
private:
    T* ptr;                      // 可能为 nullptr
    ControlBlock<T>* ctrl_block; // 指向控制块

    // 辅助:增加 use_count
    void add_use_ref() {
        if (ctrl_block) {
            ctrl_block->use_count.fetch_add(1, std::memory_order_relaxed);
        }
    }

    // 辅助:减少 use_count,如果为0则销毁对象
    void release_use() {
        if (ctrl_block) {
            if (ctrl_block->use_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
                delete ptr;  // use_count == 0,销毁对象
                ptr = nullptr;

                // 如果也没有 weak_ptr 了,销毁控制块
                if (ctrl_block->weak_count.load(std::memory_order_relaxed) == 0) {
                    delete ctrl_block;
                }

                ctrl_block = nullptr;
            }
        }
    }
public:
    // 默认构造
    shared_ptr() : ptr(nullptr), ctrl_block(nullptr) {}

    // 从裸指针构造
    explicit shared_ptr(T* p)
        : ptr(p), ctrl_block(new ControlBlock<T>(p)) {}

    // 拷贝构造
    shared_ptr(const shared_ptr& other)
        : ptr(other.ptr), ctrl_block(other.ctrl_block) {
        add_use_ref();
    }

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

    // 拷贝赋值
    shared_ptr& operator=(const shared_ptr& other) {
        if (this != &other) {
            release_use();
            ptr = other.ptr;
            ctrl_block = other.ctrl_block;
            add_use_ref();
        }
        return *this;
    }

    // 移动赋值
    shared_ptr& operator=(shared_ptr&& other) noexcept {
        if (this != &other) {
            release_use();
            ptr = other.ptr;
            ctrl_block = other.ctrl_block;
            other.ptr = nullptr;
            other.ctrl_block = nullptr;
        }
        return *this;
    }

    // 析构
    ~shared_ptr() {
        release_use();
    }

    // 解引用
    T& operator*() const { return *ptr; }
    T* operator->() const { return ptr; }
    T* get() const { return ptr; }

    // 获取引用计数
    size_t use_count() const {
        return ctrl_block ? ctrl_block->use_count.load(std::memory_order_relaxed) : 0;
    }

    bool unique() const {
        return use_count() == 1;
    }

    // 重置
    void reset(T* p = nullptr) {
        release_use();
        if (p) {
            ptr = p;
            ctrl_block = new ControlBlock<T>(p);
        } else {
            ptr = nullptr;
            ctrl_block = nullptr;
        }
    }

    void swap(shared_ptr& other) {
        std::swap(ptr, other.ptr);
        std::swap(ctrl_block, other.ctrl_block);
    }
};
你写的这个shared_ptr,sizeof多大?

T* ptr;

ControlBlock<T>* ctrl_block;

就他俩,大小8+8=16

你写的这个shared_ptr,线程安全吗?

引用计数线程安全,所管理的资源不安全。

weak_ptr?
template <typename T>
class weak_ptr {
private:
    T* ptr;                      // 可能是悬空指针
    ControlBlock<T>* ctrl_block; // 指向控制块

    // 辅助:增加 weak_count
    void add_weak_ref() {
        if (ctrl_block) {
            ctrl_block->weak_count.fetch_add(1, std::memory_order_relaxed);
        }
    }

    // 辅助:减少 weak_count,必要时销毁控制块
    void release_weak() {
        if (ctrl_block) {
            if (ctrl_block->weak_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
                // 如果没有 use_count 了,也销毁控制块
                if (ctrl_block->use_count.load(std::memory_order_relaxed) == 0) {
                    delete ctrl_block;
                }
                ctrl_block = nullptr;
            }
        }
    }
public:
    // 默认构造
    weak_ptr() : ptr(nullptr), ctrl_block(nullptr) {}

    // 从 shared_ptr 构造
    weak_ptr(const shared_ptr<T>& sp)
        : ptr(sp.get()), ctrl_block(sp.ctrl_block) {
        add_weak_ref();
    }

    // 拷贝构造
    weak_ptr(const weak_ptr& other)
        : ptr(other.ptr), ctrl_block(other.ctrl_block) {
        add_weak_ref();
    }

    // 析构
    ~weak_ptr() {
        release_weak();
    }

    // 赋值
    weak_ptr& operator=(const weak_ptr& other) {
        if (this != &other) {
            release_weak();
            ptr = other.ptr;
            ctrl_block = other.ctrl_block;
            add_weak_ref();
        }
        return *this;
    }

    // 尝试提升为 shared_ptr
    shared_ptr<T> lock() const {
        if (ctrl_block && ctrl_block->use_count > 0) {
            // 简单起见,这里直接判断 use_count > 0;
            // 更严格的实现应该用原子 CAS 来确保线程安全地增加 use_count
            size_t old_use = ctrl_block->use_count.load();
            do {
                if (old_use == 0) return shared_ptr<T>();
            } while (!ctrl_block->use_count.compare_exchange_weak(old_use, old_use + 1));

            return shared_ptr<T>(ctrl_block->ptr, ctrl_block);
        }
        return shared_ptr<T>();
    }

    // 判断是否过期
    bool expired() const {
        return !ctrl_block || ctrl_block->use_count.load() == 0;
    }

    size_t use_count() const {
        return ctrl_block ? ctrl_block->use_count.load() : 0;
    }

    // 释放资源
    void release_weak() {
        if (ctrl_block) {
            if (ctrl_block->weak_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
                if (ctrl_block->use_count.load() == 0) {
                    delete ctrl_block;
                }
                ctrl_block = nullptr;
            }
        }
    }
};
你写的这个weak_ptr,sizeof多大?

8

你写的这个weak_ptr,线程安全吗?

引用计数线程安全,lock安全(CAS锁),所管理的资源不安全。

unique_ptr?
template <typename T>
class unique_ptr {
private:
    T* ptr;  // 指向管理的对象

public:
    // 默认构造函数 => 空指针
    unique_ptr() noexcept : ptr(nullptr) {}

    // 从裸指针构造(接管所有权)
    explicit unique_ptr(T* p) noexcept : ptr(p) {}

    // 禁止拷贝(独占!)
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;

    // 移动构造
    unique_ptr(unique_ptr&& other) noexcept : ptr(other.ptr) {
        other.ptr = nullptr;  // 移交所有权后,原指针置空
    }

    // 移动赋值
    unique_ptr& operator=(unique_ptr&& other) noexcept {
        if (this != &other) {
            reset();          // 先释放当前资源
            ptr = other.ptr;  // 接管新资源
            other.ptr = nullptr;
        }
        return *this;
    }

    // 析构函数:释放资源
    ~unique_ptr() {
        reset();
    }

    // 释放所有权,返回裸指针,并将内部指针置空
    T* release() noexcept {
        T* p = ptr;
        ptr = nullptr;
        return p;
    }

    // 重置指针,先 delete 原对象,再管理新对象
    void reset(T* p = nullptr) noexcept {
        if (ptr) {
            delete ptr;
        }
        ptr = p;
    }

    // 获取裸指针(不放弃所有权)
    T* get() const noexcept {
        return ptr;
    }

    // 解引用
    T& operator*() const noexcept {
        return *ptr;
    }

    // 成员访问
    T* operator->() const noexcept {
        return ptr;
    }

    // 检查是否为空
    explicit operator bool() const noexcept {
        return ptr != nullptr;
    }
};
你写的这个unique_ptr,sizeof多大?

4

你写的这个unique_ptr,线程安全吗?

不安全,如果面试官让你安全,你自己加锁。

【必问】手撕LRU Cache?

核心数据结构组合:

  1. 双向链表(Doubly Linked List)

    1. 用于维护 key 的访问顺序:链表头部是最近使用的,尾部是最久未使用的。

    2. 当访问(get)或更新(put)某个 key 时,将其对应的节点移到链表头部。

    3. 当需要淘汰时,直接删除链表尾部节点。

  2. 哈希表(std::unordered_map)

    1. 用于实现 O(1) 的 key 查找,存储 key → 链表节点指针 的映射。

    2. 通过哈希表,我们可以快速定位某个 key 对应的链表节点,进而进行移动、删除、更新等操作。

#include <unordered_map>
#include <list>
#include <utility>
#include <iostream>

template <typename Key, typename Value>
class LRUCache {
private:
    using ListType = std::list<std::pair<Key, Value>>;
    using MapType = std::unordered_map<Key, typename ListType::iterator>;

    ListType cacheList_;  // 双向链表:存储 [key, value],头部是最近使用,尾部是最久未使用
    MapType cacheMap_;    // 哈希表:key -> 链表中对应节点的迭代器
    size_t capacity_;     // 缓存容量

public:
    // 构造函数,指定容量
    explicit LRUCache(size_t capacity) : capacity_(capacity) {}

    // 获取 key 对应的 value,如果不存在返回 -1(或者可以改为返回 std::optional<Value>)
    Value get(const Key& key) {
        auto it = cacheMap_.find(key);
        if (it == cacheMap_.end()) {
            // key 不存在
            return Value(); // 或者 return -1; 或者抛异常,视情况而定
        }

        // key 存在,将对应节点移到链表头部(表示最近使用)
        cacheList_.splice(cacheList_.begin(), cacheList_, it->second);
        return it->second->second;
    }

    // 插入或更新 key-value
    void put(const Key& key, const Value& value) {
        auto it = cacheMap_.find(key);

        if (it != cacheMap_.end()) {
            // key 已存在,更新 value,并移动到链表头部
            it->second->second = value;
            cacheList_.splice(cacheList_.begin(), cacheList_, it->second);
            return;
        }

        if (cacheList_.size() >= capacity_) {
            // 缓存已满,删除链表尾部元素(最久未使用)
            auto last = cacheList_.back();
            cacheMap_.erase(last.first);            // 从 map 中删除
            cacheList_.pop_back();                  // 从链表中删除
        }

        // 插入新元素到链表头部,并在 map 中记录
        cacheList_.emplace_front(key, value);
        cacheMap_[key] = cacheList_.begin();
    }

    // (可选)打印当前缓存内容,用于调试
    void print() const {
        std::cout << "LRUCache (most recently used -> least recently used):" << std::endl;
        for (const auto& item : cacheList_) {
            std::cout << "[" << item.first << ":" << item.second << "] ";
        }
        std::cout << std::endl;
    }
};

【必问】手撕链表是否有环?

方法一:哈希表法(容易想到,但空间复杂度高 ✖ 不满足最优)

  • 遍历链表的每个节点,用一个哈希集合(如 std::unordered_set)记录已经访问过的节点。

  • 每访问一个节点,检查它是否已经在集合中:

    • 如果在,说明有环;

    • 如果遍历到 nullptr,说明无环。

方法二:快慢指针法(Floyd 判圈算法)✔ 推荐,最优解

  • 使用两个指针,一快一慢:

    • 慢指针(slow):每次走一步(slow = slow->next);

    • 快指针(fast):每次走两步(fast = fast->next->next)。

  • 如果链表中没有环,快指针最终会走到 nullptr,结束循环;

  • 如果链表有环,快指针最终会追上慢指针(即 fast == slow),因为它们都在环内,快指针每次比慢指针多走一步,迟早相遇。

/​**​
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(nullptr) {}
 * };
 */

class Solution {
public:
    bool hasCycle(ListNode *head) {
        if (!head || !head->next) {
            return false; // 空链表或只有一个节点且无环
        }

        ListNode *slow = head;
        ListNode *fast = head;

        while (fast && fast->next) {
            slow = slow->next;          // 慢指针走一步
            fast = fast->next->next;    // 快指针走两步

            if (slow == fast) {         // 相遇则有环
                return true;
            }
        }

        return false; // 快指针遇到 nullptr,说明无环
    }
};
(追问)如何确定环的大小(环中节点个数)?

方法:在快慢指针相遇后,保持一个指针不动,另一个指针继续前进并计数,直到再次相遇。

// 假设已经找到 slow == fast,即相遇了
int cycleLength = 0;
do {
    slow = slow->next;
    cycleLength++;
} while (slow != fast);

std::cout << "环的大小为: " << cycleLength << std::endl;
(追问)确定环的起点位置?

数学原理(重点!面试加分点)

设:

  • 链表头到环的入口点的距离为 A;

  • 环的入口点到快慢指针第一次相遇点的距离为 B;

  • 从相遇点到环的入口点的距离为 C;

  • 整个环的长度为 L = B + C。

第一次相遇时:

  • 慢指针 slow 走了:A + B

  • 快指针 fast 走了:A + B + n × L,其中 n 是快指针在环内绕的圈数(n ≥ 1)

  • 但因为 fast 走的是 slow 的两倍速度,所以:

  • 2(A + B) = A + B + nL ⇒ A + B = nL ⇒ A = nL - B

关键结论:

从链表头到环入口的距离 A,等于 从相遇点继续走 n 圈减去 B,也就是 从相遇点走 (L - B) = C,再走若干圈回到入口。

但更直观的理解是:

如果我们让一个指针从链表头开始,另一个指针从相遇点开始,两者都每次走一步,它们最终会在环的入口节点相遇!

手撕二叉树的前序、中序、后序遍历?

见数据结构与算法

手撕最小堆?

见数据结构与算法

如何设计一个线程安全的跳表?

为了实现并发安全,需要对修改操作(插入、删除)加锁,但为了提高并发度,不能锁整个跳表,而是采用 细粒度锁(如每层每节点锁、或节点级锁),或者使用 无锁(lock-free)或乐观锁策略。

方案一(常用、实用):使用节点级互斥锁(细粒度锁)

  • 每个跳表节点拥有一个 std::mutex

  • 查找操作通常不加锁(只读),或使用乐观策略;

  • 插入和删除操作需要按顺序(从上到下、从前往后)获取相关节点的锁,防止数据竞争和死锁;

  • 采用“锁耦合”技术,逐步加锁与解锁,只锁定必要的节点;

  • 这种方案实现较为可控,适合大多数工程场景。

方案二(高级、高性能):无锁(lock-free)跳表

  • 使用 CAS(Compare-And-Swap)等原子操作来修改节点的 next 指针;

  • 所有指针变更必须通过原子操作完成,通常要配合重试机制;

  • 实现非常复杂,需要处理 ABA 问题、内存回收等,一般用于极致性能要求的场景。

内存里有一个map,你如何设计持久化策略,使得忽然宕机时,能够保证数据最少丢失?

以下二者组合:

  1. 写前日志(类似AOF)

    1. 每次修改 map 前,先写一条 操作日志 到磁盘(比如 "SET key value""DEL key");

    2. 日志是 追加写(append-only),性能高且安全(一般写到磁盘文件末尾);

  2. 定时快照(类似RDB)

    1. 定期将内存中的整个 map 序列化(如二进制或文本)并写入磁盘,形成某个时间点的快照。宕机后从最近一次快照恢复。

C++如何用栈实现队列?

核心思想:使用两个栈,一个用于输入,一个用于输出

我们定义两个栈:

  • inStack(输入栈):用来处理 push() 操作,直接将新元素压入此栈

  • outStack(输出栈):用来处理 pop()peek() 操作

关键点:

  • 当你要 pop()peek() 时,如果 outStack 为空,就需要将 inStack 的所有元素依次弹出并压入 outStack,这样最先进入 inStack 的元素就跑到了 outStack 的栈顶,就可以直接操作了。

  • 如果 outStack 不为空,直接对 outStack 操作即可。

这样就能保证队列的 FIFO 特性。

#include <iostream>
#include <stack>
using namespace std;

class MyQueue {
private:
    stack<int> inStack;  // 用于 push 操作
    stack<int> outStack; // 用于 pop / peek 操作

    // 将 inStack 中的元素全部转移到 outStack 中(只在 outStack 为空时调用)
    void transferIfNeeded() {
        if (outStack.empty()) {
            while (!inStack.empty()) {
                outStack.push(inStack.top());
                inStack.pop();
            }
        }
    }

public:
    MyQueue() {}

    // 入队:直接 push 到 inStack
    void push(int x) {
        inStack.push(x);
    }

    // 出队:从 outStack 弹出;如果 outStack 为空,则先转移
    int pop() {
        transferIfNeeded();
        int front = outStack.top();
        outStack.pop();
        return front;
    }

    // 获取队首元素:同上
    int peek() {
        transferIfNeeded();
        return outStack.top();
    }

    // 判空:两个栈都为空时才为空
    bool empty() {
        return inStack.empty() && outStack.empty();
    }
};

14.3 手撕算法

【必问】手撕快排?

见数据结构与算法。

手撕堆排序?

#include <iostream>
#include <vector>

// 调整以 i 为根的子树成为最大堆,范围是 [start, end]
void heapify(std::vector<int>& arr, int n, int i) {
    int largest = i;            // 初始化最大值为根节点
    int left = 2 * i + 1;       // 左孩子
    int right = 2 * i + 2;      // 右孩子

    // 如果左孩子比根大
    if (left < n && arr[left] > arr[largest])
        largest = left;

    // 如果右孩子比当前最大值还大
    if (right < n && arr[right] > arr[largest])
        largest = right;

    // 如果最大值不是根节点,交换并递归调整
    if (largest != i) {
        std::swap(arr[i], arr[largest]);
        heapify(arr, n, largest);  // 递归调整受影响的子树
    }
}

// 堆排序主函数
void heapSort(std::vector<int>& arr) {
    int n = arr.size();

    // Step 1: 构建最大堆(从最后一个非叶子节点开始)
    // 最后一个非叶子节点索引 = (n / 2) - 1
    for (int i = n / 2 - 1; i >= 0; --i) {
        heapify(arr, n, i);
    }

    // Step 2: 一个个从堆顶取出最大值
    for (int i = n - 1; i > 0; --i) {
        // 将当前最大值(堆顶,即arr[0])与末尾元素交换
        std::swap(arr[0], arr[i]);

        // 重新调整剩余元素为最大堆,范围 [0, i-1]
        heapify(arr, i, 0);
    }
}

2G内存在20个亿个整数中找出出现次数最多的数?

哈希分片:

  1. 哈希分片:

    1. 选择一个哈希函数,将每个整数映射到一个固定的桶(bucket)中(比如0到N-1)。

    2. 将20亿个整数根据哈希值分配到N个不同的临时文件中(比如N=1000,每个文件大约2 million个整数)。

    3. 这样,相同的整数一定会被分配到同一个文件中(因为哈希值相同)。

  2. 逐个处理分片:

    1. 对于每个临时文件,将其加载到内存中,使用哈希表统计该文件中每个整数的出现次数。

    2. 记录该文件中出现次数最多的整数及其次数。

    3. 最后比较所有分片的结果,找出全局出现次数最多的整数。

给出一副4种花色的扑克牌,输出这幅扑克牌可以组成的所有顺子和同花?

数据结构已给定:

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <map>

using namespace std;

// 扑克牌结构
struct Card {
    int rank;  // 1=A, 2-10, 11=J, 12=Q, 13=K
    char suit; // S, H, D, C

    // 为了输出好看
    string rankStr() const {
        if (rank == 1) return "A";
        if (rank == 11) return "J";
        if (rank == 12) return "Q";
        if (rank == 13) return "K";
        return to_string(rank);
    }

    string suitStr() const {
        switch (suit) {
        case 'S': return "S"; case 'H': return "H";
        case 'D': return "D"; case 'C': return "C";
        default: return "?";
        }
    }

    friend ostream& operator<<(ostream& os, const Card& c) {
        os << c.rankStr() << c.suitStr();
        return os;
    }
};

// 生成一副牌:4花色 * 13点数
vector<Card> generateDeck() {
    vector<Card> deck;
    char suits[] = { 'S', 'H', 'D', 'C' };
    for (char suit : suits) {
        for (int rank = 1; rank <= 13; ++rank) {
            deck.push_back(Card{ rank, suit });
        }
    }
    return deck;
}

同花(n取k的组合):

// 从n个元素中选择k个的组合生成(索引版)
void generateCombinations(int n, int k, int start, vector<int>& current, vector<vector<int>>& result) {
    if (current.size() == k) {
        result.push_back(current);
        return;
    }
    for (int i = start; i < n; ++i) {
        current.push_back(i);
        generateCombinations(n, k, i + 1, current, result);
        current.pop_back();
    }
}

// 打印某个花色的所有五张牌组合
void printFlushCombinationsForSuit(const vector<Card>& suitCards) {
    int n = suitCards.size(); // 13
    int k = 5;
    vector<int> indices;
    vector<vector<int>> combos;

    generateCombinations(n, k, 0, indices, combos);

    cout << "=== 同花组合(花色有 " << suitCards[0].suitStr() << ",共 " << combos.size() << " 种组合)===" << endl;
    for (const auto& combo : combos) {
        vector<Card> flushCombo;
        for (int idx : combo) {
            flushCombo.push_back(suitCards[idx]);
        }
        for (const Card& c : flushCombo) {
            cout << c << " ";
        }
        cout << endl;
    }
    cout << "==================================================" << endl << endl;
}

int main() {
    vector<Card> deck = generateDeck();

    // 按花色分组
    map<char, vector<Card>> suitsMap;
    for (const Card& card : deck) {
        suitsMap[card.suit].push_back(card);
    }

    // 对每个花色,生成并打印所有5张牌的同花组合
    for (const auto& entry : suitsMap) {
        char suit = entry.first;
        const vector<Card>& suitCards = entry.second;

        // 只处理有13张牌的花色(正常情况)
        if (suitCards.size() == 13) {
            printFlushCombinationsForSuit(suitCards);
        }
    }

    return 0;
}

顺子(先生成顺子,再给每个顺子配花色):

// 从n个元素中选择k个的组合生成(索引版)
void generateRepeatedCombinations(int n, int k, int start, vector<int>& current, vector<vector<int>>& result) {
    if (current.size() == k) {
        result.push_back(current);
        return;
    }
    for (int i = 0; i < n; ++i) { // 可重复
        current.push_back(i);
        generateRepeatedCombinations(n, k, i + 1, current, result);
        current.pop_back();
    }
}

// 打印某个顺子的所有五张牌组合
void printStraightCombinationsForSuit(const vector<Card>& suitCards) {
    int n = suitCards.size(); // 4 梅花A 方片A 黑桃A 洪涛A
    int k = 5;
    vector<int> indices;
    vector<vector<int>> combos;

    generateRepeatedCombinations(n, k, 0, indices, combos);

    cout << "=== 顺子组合,共 " << combos.size() << " 种组合)===" << endl;
    for (const auto& combo : combos) { // S S S S S/S S S S D/S S S S H
        vector<Card> flushCombo;
        int i = 0;
        for (int idx : combo) {
            flushCombo.emplace_back(suitCards[idx].rank + i, suitCards[idx].suit);
            ++i;
        }
        for (const Card& c : flushCombo) {
            cout << c << " ";
        }
        cout << endl;
    }
    cout << "==================================================" << endl << endl;
}

int main() {
    vector<Card> deck = generateDeck();

    // 所有的顺子(示意)
    //vector<vector<int>> straightRankGroups = {
    //    {1, 2, 3, 4, 5},    // A-2-3-4-5
    //    {2, 3, 4, 5, 6},
    //    {3, 4, 5, 6, 7},
    //    {4, 5, 6, 7, 8},
    //    {5, 6, 7, 8, 9},
    //    {6, 7, 8, 9, 10},
    //    {7, 8, 9, 10, 11},  // 7-8-9-10-J
    //    {8, 9, 10, 11, 12}, // 8-9-10-J-Q
    //    {9, 10, 11, 12, 13} // 9-10-J-Q-K
    //};

    // 按数字分组
    map<int, vector<Card>> suitsMap;
    for (const Card& card : deck) {
        suitsMap[card.rank].push_back(card);
    }

    // 对每个数字,生成并打印所有5张牌的顺子组合
    for (const auto& entry : suitsMap) {
        int start_num_of_straight = entry.first;
        const vector<Card>& suitCards = entry.second;

        // 只处理起始数字<=9的情况,否则无法组成顺子
        if (start_num_of_straight <= 9) {
            printStraightCombinationsForSuit(suitCards);
        }
    }

    return 0;
}

40亿个非负整数中算中位数和找出现两次的数?

  • 数据规模:40 亿个 unsigned int,大约是 4,000,000,000 个数字。

  • 每个数字占 4 字节,如果全部加载进内存,大约需要:

  • 4,000,000,000 × 4 byte ≈ 16 GB

  • 一般机器的内存远远不够一次性加载所有数据,所以不能直接排序然后取中间值。

中位数:基于“分桶统计 + 二分查找”(Counting in Buckets)

思路:

  1. 将整个数字范围 [0, 2³²-1] 分成多个区间(比如 1024 或 65536 个桶 / 段)

  2. 统计每个区间内有多少个数字(即落在 [0~K], [K+1 ~ M], ... 的数字个数)

  3. 通过累加统计,找到那个包含第 20 亿个数字的区间

  4. 然后再单独加载该区间内的数据,进行排序或计数,最终找到中位数

核心思想是“分而治之” + 二分查找定位中位数所在区间

这种方法不需要一次性加载所有数据,也避免了对整个 43 亿范围做精确计数,大大节省了内存,适合面试手撕或实际工程优化

两次数:使用“哈希 + 计数,但分块处理 / 外部排序”

思路:

  1. 分批读取数据(比如每次读 1 亿个数到内存)

  2. 对每块数据,用哈希表统计数字出现次数

  3. 记录出现两次的数,并清理或合并统计信息

  4. 最终汇总所有块的结果,得到全局出现两次的数

适用于数据量极大但可以分片处理的场景,比如外部数据处理 / 流式处理

怎么在一个整数集合中,快速找到一个数,满足这个数是其他数的整数倍?

如果集合很大,可以:

  1. 将数字存入一个集合(哈希集合)以便快速查找。

  2. 对于每个数字 x,尝试找到其所有因数 y(除了 x本身),然后检查 y是否在集合中。

    1. 如何高效找到 x的所有因数?可以遍历 1 到 sqrt(x),检查是否能整除 x,然后对应的因数是 ix // i

14.4 手撕操作系统场景

手撕线程池?

见Linux系统编程。

如何判断Linux上两个文件是否完全相同?

  1. 使用 cmp 命令(推荐,高效,只比较内容)

  2. 使用 diff 命令(适合文本文件,可查看差异详情)

  3. 使用 md5sum / sha256sum(比较文件的哈希值,内容完全一致时哈希相同)

手撕epoll?

#include <iostream>
#include <unordered_map>
#include <vector>
#include <utility> // for std::pair
#include <thread>
#include <chrono>
#include <random>
#include <mutex>
#include <condition_variable>

// 模拟 Epoll 类
class SimpleEpoll {
public:
    using Event = int; // 简化:用 int 表示关注的事件,比如 1<<0 表示读,1<<1 表示写

    // 模拟 epoll 实例
    struct EpollInstance {
        std::unordered_map<int, Event> fd_events; // fd -> 关注的事件
    };

private:
    int next_epfd_ = 1; // 模拟 epoll_create() 返回的 fd
    std::unordered_map<int, EpollInstance> epoll_instances_; // epfd -> EpollInstance

    // 模拟“就绪事件”:在实际中是由内核通知的,这里我们用变量模拟
    std::unordered_map<int, Event> ready_events_; // fd -> 实际就绪的事件
    std::mutex mtx_;
    std::condition_variable cv_;
    bool ready_ = false;
public:
    // 模拟 epoll_create() -> 返回一个 epoll fd
    int create() {
        int epfd = next_epfd_++;
        epoll_instances_[epfd] = EpollInstance{};
        std::cout << "[SimEpoll] epoll_create() -> epfd = " << epfd << std::endl;
        return epfd;
    }
    // 模拟 epoll_ctl(epfd, fd, op, events)
    // op: 1=ADD, 2=MOD, 3=DEL (简化,实际是 EPOLL_CTL_ADD 等)
    void ctl(int epfd, int fd, int op, Event events) {
        std::cout << "[SimEpoll] epoll_ctl(epfd=" << epfd << ", fd=" << fd
                  << ", op=" << op << ", events=" << events << ")" << std::endl;

        if (epoll_instances_.find(epfd) == epoll_instances_.end()) {
            std::cerr << "Error: Invalid epfd" << std::endl;
            return;
        }

        auto& instance = epoll_instances_[epfd];

        if (op == 1) { // ADD
            instance.fd_events[fd] = events;
            std::cout << "  -> Add fd " << fd << " with events " << events << std::endl;
        } else if (op == 2) { // MOD
            if (instance.fd_events.count(fd))
                instance.fd_events[fd] = events;
            std::cout << "  -> Modify fd " << fd << " events to " << events << std::endl;
        } else if (op == 3) { // DEL
            instance.fd_events.erase(fd);
            std::cout << "  -> Delete fd " << fd << std::endl;
        } else {
            std::cerr << "Error: Unknown op " << op << std::endl;
        }
    }
    // 模拟:手动触发某个 fd 的事件就绪(用于模拟异步事件到达,比如数据到达 socket)
    void trigger_event(int fd, Event revents) {
        std::unique_lock<std::mutex> lock(mtx_);
        ready_events_[fd] = revents;
        ready_ = true;
        std::cout << "[SimEpoll] [INTERNAL] Trigger event on fd " << fd
                  << ", revents = " << revents << std::endl;
        cv_.notify_one(); // 通知等待的线程,有事件就绪了
    }
    // 模拟 epoll_wait(epfd, events[], maxevents, timeout)
    int wait(int epfd, std::vector<std::pair<int, int>>& out_events, int timeout_ms = 1000) {
        std::cout << "[SimEpoll] epoll_wait(epfd=" << epfd << ", timeout=" << timeout_ms << "ms)" << std::endl;

        if (epoll_instances_.find(epfd) == epoll_instances_.end()) {
            std::cerr << "Error: Invalid epfd" << std::endl;
            return -1;
        }

        std::unique_lock<std::mutex> lock(mtx_);
        // 等待某个 fd 就绪(模拟异步事件到达)
        if (cv_.wait_for(lock, std::chrono::milliseconds(timeout_ms), [this]() { return ready_; })) {
            // 模拟返回就绪的 fd 和事件
            for (const auto& [fd, revents] : ready_events_) {
                // 只返回该 epoll 实例管理的 fd(简化,真实 epoll 会关联)
                out_events.emplace_back(fd, revents);
                std::cout << "  -> Ready: fd=" << fd << ", events=" << revents << std::endl;
            }
            ready_events_.clear();
            ready_ = false;
            return out_events.size(); // 返回就绪事件数
        } else {
            std::cout << "  -> Timeout, no events ready." << std::endl;
            return 0; // 超时,无事件
        }
    }
};

手撕生产者消费者模型?

  • 1 个或多个生产者线程

  • 1 个或多个消费者线程

  • 1 个共享队列(std::queue)

  • 互斥锁 + 条件变量 控制同步

#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
#include <random>

class ProducerConsumer {
private:
    std::queue<int> buffer;               // 共享缓冲区(队列)
    const unsigned int max_size = 5;      // 缓冲区最大容量
    std::mutex mtx;                       // 互斥锁,保护共享数据
    std::condition_variable not_full;     // 条件变量:队列不满
    std::condition_variable not_empty;    // 条件变量:队列不空

public:
    // 生产者函数
    void producer(int id) {
        for (int i = 0; i < 10; ++i) {
            std::unique_lock<std::mutex> lock(mtx);

            // 如果队列满了,等待消费者消费
            not_full.wait(lock, [this]() { return buffer.size() < max_size; });

            // 模拟生产一个数据
            int num = rand() % 100;
            buffer.push(num);
            std::cout << "Producer " << id << " produced: " << num 
                      << " | Buffer size: " << buffer.size() << std::endl;

            lock.unlock();      // 解锁(其实 unique_lock 析构时也会解锁,但显式更好理解)
            not_empty.notify_one(); // 通知消费者可以消费了

            // 模拟生产耗时
            std::this_thread::sleep_for(std::chrono::milliseconds(rand() % 300));
        }
    }

    // 消费者函数
    void consumer(int id) {
        for (int i = 0; i < 10; ++i) {
            std::unique_lock<std::mutex> lock(mtx);

            // 如果队列空了,等待生产者生产
            not_empty.wait(lock, [this]() { return !buffer.empty(); });

            // 消费一个数据
            int num = buffer.front();
            buffer.pop();
            std::cout << "Consumer " << id << " consumed: " << num 
                      << " | Buffer size: " << buffer.size() << std::endl;

            lock.unlock();
            not_full.notify_one(); // 通知生产者可以生产了

            // 模拟消费耗时
            std::this_thread::sleep_for(std::chrono::milliseconds(rand() % 500));
        }
    }
};
int main() {
    ProducerConsumer pc;

    // 创建 2 个生产者线程 和 3 个消费者线程(数量可调整)
    std::thread producers[2];
    std::thread consumers[3];

    for (int i = 0; i < 2; ++i) {
        producers[i] = std::thread(&ProducerConsumer::producer, &pc, i + 1);
    }

    for (int i = 0; i < 3; ++i) {
        consumers[i] = std::thread(&ProducerConsumer::consumer, &pc, i + 1);
    }

    // 等待所有生产者完成
    for (int i = 0; i < 2; ++i) {
        producers[i].join();
    }

    // 等待所有消费者完成
    for (int i = 0; i < 3; ++i) {
        consumers[i].join();
    }

    std::cout << "All producers and consumers finished." << std::endl;
    return 0;
}

什么场景用线程?什么场景用协程?

线程适合用在 CPU 密集型、需要真正并行、利用多核 的场景,比如大规模计算、图像处理等。线程是操作系统调度的,可以分配到不同核心上并行执行,但线程的创建和切换开销较大,线程数过多时管理复杂,且需要处理同步与锁的问题。

协程适用于 I/O 密集型、高并发、异步任务调度 的场景,比如网络请求、Web 服务、爬虫等。协程是用户态轻量级任务,创建和切换成本极低,可以轻松支持数十万并发,而且用同步的代码风格写异步逻辑,代码更清晰。但协程一般运行在单线程内,不能直接利用多核,除非配合多线程调度器。

程序CPU占用率较高,如何排查?

  1. 找到高 CPU 的 进程

    1. top 或 htop

  2. 找到进程中高 CPU 的 线程

    1. top -H -p <PID>

  3. 采集调用栈,分析热点代码

    1. gdb 多线程

    2. perf 火焰图

  4. 分析代码逻辑,定位根因

程序崩了,你的第一反应是什么?

  1. 查看core文件:

  • ulimit -c unlimited 命令确保崩溃时core dump核心已转储。

  • 调用栈(Stack Trace):通过调试器(gdb ./XX core.XX.YY)加载core dump或附加到进程,查看崩溃时的函数调用链(命令:bt)。

  • 错误类型:常见崩溃原因包括:

    • 空指针解引用(Segmentation fault

    • 数组越界(Buffer Overflow)

    • 野指针/悬垂指针

    • 多线程竞争(如访问已释放内存)

    • 堆栈溢出(递归过深或局部变量过大)

    • 资源耗尽(如内存不足、文件句柄泄漏)。

  1. 日志:检查程序自身的日志(尤其是崩溃前的最后几条日志)。

  2. valgrind查看内存泄漏(以及不止内存泄漏)。

手撕读写锁?

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

class ReadWriteLock {
private:
    std::mutex mtx_;                    // 互斥锁,保护内部状态
    std::condition_variable read_cond_; // 读者条件变量
    std::condition_variable write_cond_;// 写者条件变量

    int readers_ = 0;                   // 当前正在读的线程数
    bool writing_ = false;              // 是否有写者正在写
    int waiting_writers_ = 0;           // 正在等待的写者数(用于公平性)

public:
    // 加读锁
    void lockRead() {
        std::unique_lock<std::mutex> lock(mtx_);
        // 如果有写者正在写,或者有写者在等待(可选策略:写者优先)
        read_cond_.wait(lock, [this]() {
            return !writing_ && waiting_writers_ == 0;
        });
        ++readers_;
    }

    // 解读锁
    void unlockRead() {
        std::unique_lock<std::mutex> lock(mtx_);
        --readers_;
        if (readers_ == 0) {
            // 唤醒等待的写者
            write_cond_.notify_one();
        }
    }

    // 加写锁
    void lockWrite() {
        std::unique_lock<std::mutex> lock(mtx_);
        ++waiting_writers_;  // 表示有一个写者在等待
        // 等待:没有读者,也没有其他写者
        write_cond_.wait(lock, [this]() {
            return readers_ == 0 && !writing_;
        });
        --waiting_writers_;
        writing_ = true;
    }

    // 解写锁
    void unlockWrite() {
        std::unique_lock<std::mutex> lock(mtx_);
        writing_ = false;
        // 优先唤醒写者还是读者?这里先唤醒写者(可调整策略)
        if (waiting_writers_ > 0) {
            write_cond_.notify_one();
        } else {
            // 没有写者在等,唤醒所有读者
            read_cond_.notify_all();
        }
    }
};

14.5 手撕设计模式

【必问】手撕单例模式(懒汉式)?

懒汉式(Lazy Initialization):第一次调用时才创建实例。

class Singleton {
private:
    static Singleton* instance;  // 静态成员指针

    // 私有构造函数,防止外部 new
    Singleton() {}

public:
    // 获取单例对象
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }

    // 删除拷贝构造和赋值操作(C++11 起推荐)
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

// 初始化静态成员
Singleton* Singleton::instance = nullptr;
(追问)你这玩意线程安全吗?

不安全。加锁。

#include <mutex>

class Singleton {
private:
    static Singleton* instance;
    static std::mutex mtx;

    Singleton() {}

public:
    static Singleton* getInstance() {
        std::lock_guard<std::mutex> lock(mtx); // 加锁
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }

    // 禁止拷贝和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

// 初始化静态成员
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
(追问)你加这么多锁,性能高吗?Meyers' Singleton是什么?

线程安全 + 高性能版本(双重检查锁定 DCLP,推荐)Double-Checked Locking Pattern(双重检查锁定)

目的:只在第一次创建时加锁,后续调用无需加锁,提升性能。

但还是太麻烦!

最推荐写法:Meyers' Singleton(局部静态变量,C++11 起线程安全)

🎯 利用 C++11 特性:局部静态变量的初始化是线程安全的!

这是目前 C++ 中最简洁、最安全、最高效、最推荐的单例实现方式,由 Scott Meyers 在《Effective Modern C++》中推荐,也叫 Meyers' Singleton。

class Singleton {
private:
    Singleton() {}  // 私有构造函数

public:
    // 获取单例对象
    static Singleton& getInstance() {
        static Singleton instance;  // C++11保证此处初始化线程安全
        return instance;
    }

    // 禁止拷贝和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

14.6 手撕网络问题

手撕ping?

Ping 是什么?

Ping 是一个常用的网络工具,用于测试两台主机之间是否可达,以及测量数据包的往返时间(RTT, Round-Trip Time)。

它基于 ICMP 协议(Internet Control Message Protocol,互联网控制消息协议),是 IP 层的辅助协议,不是传输层的 TCP 或 UDP。

Ping 的工作原理?(基于 ICMP Echo 请求/应答)

Ping 的核心是发送一个 ICMP Echo Request(类型 8) 报文到目标主机,然后等待对方返回一个 ICMP Echo Reply(类型 0) 报文。

  • 如果收到回复,说明目标主机可达,并且你能得到 RTT(往返时间)。

  • 如果收不到,可能目标不可达、网络不通、防火墙拦截等。

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <chrono>

using namespace std;

// 计算校验和
unsigned short checksum(void* buf, int len) {
    unsigned short* data = (unsigned short*)buf;
    unsigned int sum = 0;

    for (int i = 0; i < len / 2; ++i)
        sum += data[i];

    if (len % 2)
        sum += *((unsigned char*)data + len / 2);

    sum = (sum >> 16) + (sum & 0xFFFF);
    sum += (sum >> 16);
    return (unsigned short)(~sum);
}

int main() {
    const char* target_ip = "8.8.8.8";  // 可换成任何你想 ping 的 IP,比如 "127.0.0.1"

    // 1. 创建原始 ICMP 套接字
    int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (sock < 0) {
        perror("socket");
        cerr << "请使用 root / 管理员权限运行!" << endl;
        return 1;
    }

    // 2. 构造 ICMP Echo Request 报文
    char packet[64] = {0};
    struct icmphdr* icmp = (struct icmphdr*)packet;

    icmp->type = ICMP_ECHO;      // 8
    icmp->code = 0;
    icmp->un.echo.id = htons(getpid() & 0xFFFF);  // 可简单用 pid 做标识
    icmp->un.echo.sequence = htons(1);
    icmp->checksum = 0;

    // 填充一些数据
    strncpy(packet + sizeof(struct icmphdr), "PING", 4);

    // 计算校验和(包括整个 ICMP 报文:头 + 数据)
    icmp->checksum = checksum(packet, sizeof(struct icmphdr) + 4);

    // 3. 设置目标地址
    struct sockaddr_in dest;
    memset(&dest, 0, sizeof(dest));
    dest.sin_family = AF_INET;
    inet_pton(AF_INET, target_ip, &dest.sin_addr);

    // 4. 发送 ICMP 请求
    if (sendto(sock, packet, sizeof(struct icmphdr) + 4, 0,
               (struct sockaddr*)&dest, sizeof(dest)) <= 0) {
        perror("sendto");
        return 1;
    }
    cout << "Sent ICMP Echo Request to " << target_ip << endl;

    // 5. 尝试接收 ICMP Echo Reply(简化,不循环、不超时)
    char recv_buf[1024];
    struct sockaddr_in from;
    socklen_t from_len = sizeof(from);

    if (recvfrom(sock, recv_buf, sizeof(recv_buf), 0,
                 (struct sockaddr*)&from, &from_len) > 0) {
        // 跳过 IP 头(通常 20 字节),拿到 ICMP 部分
        struct iphdr* ip_header = (struct iphdr*)recv_buf;
        int ip_hdr_len = ip_header->ihl * 4;
        struct icmphdr* recv_icmp = (struct icmphdr*)(recv_buf + ip_hdr_len);

        // 判断是否是 Echo Reply (Type == 0),并且 ID 匹配
        if (recv_icmp->type == ICMP_ECHOREPLY && 
            ntohs(recv_icmp->un.echo.id) == (getpid() & 0xFFFF)) {
            cout << "Received ICMP Echo Reply from " 
                 << inet_ntoa(from.sin_addr) << endl;
        } else {
            cout << "Received something, but not our reply." << endl;
        }
    } else {
        perror("recvfrom");
        cout << "No reply received (可能目标不可达或防火墙拦截)" << endl;
    }

    close(sock);
    return 0;
}

【必问】一般在Linux系统下遇到网络问题用哪些命令或手段进行排查?

在 Linux 系统下排查网络问题,我们通常会使用一系列常用命令和工具,从不同的层次去定位问题是出在网络连接、路由、防火墙、还是应用层本身。

ping:首先,最基础也最常用的命令是 ping,它可以用来测试目标主机是否可达,以及大致的网络延迟和丢包情况。如果 ping 不通,可能说明网络层就有问题,比如目标不可达、本地网络配置错误,或者中间有防火墙阻断了 ICMP 包。

traceroute:接下来是 traceroute(或 tracepath),它可以显示数据包从本机到目标主机经过的每一跳路由,帮助我们定位网络中断或高延迟的具体位置,尤其适合排查网络路径上的问题。

telnetnc:如果目标服务是 TCP 类型的,比如 HTTP、MySQL 等,那可以用 telnet 或者更现代的 nc(netcat) 命令去测试目标主机的某个端口是否开放、是否能建立 TCP 连接。比如 telnet IP 端口 或者 nc -zv IP 端口,如果连不上,可能说明服务没起来、端口没监听、防火墙拦了,或者网络策略有问题。

netstatss:查看本机网络连接状态,最常用的命令是 netstat 或者更推荐的 ss(socket statistics),比如 ss -tulnp 可以列出当前所有监听的 TCP/UDP 端口以及对应的进程,方便确认服务是否正常监听。而 ss -antpnetstat -antp 则可以查看当前的 TCP 连接情况,包括连接状态(比如 ESTABLISHED、TIME_WAIT、CLOSE_WAIT 等),这在排查连接数过多、连接未正常释放等问题时非常有用。

iptables:如果怀疑是本地防火墙或安全组策略导致的问题,可以查看 iptables(或者 nftables,视发行版而定)的规则,比如 iptables -L -n -v,看是否有规则把相关流量给拦截了。在云服务器上还需要检查云平台自带的安全组配置。

对于 DNS 相关的问题,可以用 nslookup、dig 命令去测试域名解析是否正常,如果应用出现连接失败但 IP 是硬编码的,那可能不是 DNS 的问题,但如果用的是域名,解析失败就会导致连接不上。

tcpdump:另外,tcpdump 是一个非常强大的抓包工具,可以用来抓取指定网卡上的网络流量,比如 tcpdump -i eth0 port 80,通过分析抓到的包,我们可以看到请求有没有发出去、对方有没有回应、是不是被拦截、TCP 握手是否正常完成等,它是深入排查网络问题的利器。

ifconfig:最后,如果问题出在系统配置上,比如路由表不对,可以用 route -n 或者 ip route 查看路由信息;如果怀疑是本地网络接口配置问题,可以用 ifconfig 或者 ip addr 查看网卡状态和 IP 地址配置是否正确。

14.7 手撕中间件问题

目前没遇到。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值