Skip to content

C++ 作业

写一个动态数组

csharp-dynamic-array

标准答案

动态数组本质就是“底层数组 + 当前元素数量”。数组满了以后,它不会原地变大,而是创建一个更大的新数组,把旧元素复制过去,再把内部引用指向新数组。C# 的 List<T> 底层就是这种思路。

c
using System; // 引入异常类型
using System.Collections; // 引入 IEnumerable
using System.Collections.Generic; // 引入 IEnumerator 和 EqualityComparer

public sealed class DynamicArray<T> : IEnumerable<T> // 定义泛型动态数组
{ // DynamicArray 类开始
    private T[] items; // 真正存储元素的底层数组
    private int count; // 当前已经存放的元素数量
    private const int DefaultCapacity = 4; // 默认初始容量

    public int Count => count; // 返回当前元素数量
    public int Capacity => items.Length; // 返回底层数组容量

    public DynamicArray(int capacity = DefaultCapacity) // 定义构造函数
    { // 构造函数开始
        if (capacity < 0) // 如果容量小于 0
        { // 容量判断开始
            throw new ArgumentOutOfRangeException(nameof(capacity)); // 抛出参数异常
        } // 容量判断结束

        items = new T[Math.Max(capacity, 0)]; // 创建底层数组
        count = 0; // 初始化元素数量为 0
    } // 构造函数结束

    public T this[int index] // 定义索引器
    { // 索引器开始
        get // 读取元素
        { // get 开始
            CheckIndex(index); // 检查下标是否合法
            return items[index]; // 返回对应下标的元素
        } // get 结束
        set // 修改元素
        { // set 开始
            CheckIndex(index); // 检查下标是否合法
            items[index] = value; // 设置对应下标的元素
        } // set 结束
    } // 索引器结束

    public void Add(T value) // 尾部添加元素
    { // Add 方法开始
        EnsureCapacity(count + 1); // 确保容量足够
        items[count] = value; // 把新元素放到 count 位置
        count++; // 元素数量加 1
    } // Add 方法结束

    public void Insert(int index, T value) // 在指定位置插入元素
    { // Insert 方法开始
        if (index < 0 || index > count) // 插入位置可以等于 count
        { // 下标判断开始
            throw new ArgumentOutOfRangeException(nameof(index)); // 抛出下标异常
        } // 下标判断结束

        EnsureCapacity(count + 1); // 确保容量足够

        if (index < count) // 如果不是插入到末尾
        { // 移动判断开始
            Array.Copy(items, index, items, index + 1, count - index); // 把 index 后面的元素整体后移一格
        } // 移动判断结束

        items[index] = value; // 把新元素放到 index 位置
        count++; // 元素数量加 1
    } // Insert 方法结束

    public bool Remove(T value) // 删除第一个匹配的元素
    { // Remove 方法开始
        int index = IndexOf(value); // 查找元素下标

        if (index < 0) // 如果没有找到
        { // 未找到判断开始
            return false; // 删除失败
        } // 未找到判断结束

        RemoveAt(index); // 删除指定下标的元素
        return true; // 删除成功
    } // Remove 方法结束

    public void RemoveAt(int index) // 删除指定下标的元素
    { // RemoveAt 方法开始
        CheckIndex(index); // 检查下标是否合法
        int moveCount = count - index - 1; // 计算需要前移的元素数量

        if (moveCount > 0) // 如果后面还有元素
        { // 移动判断开始
            Array.Copy(items, index + 1, items, index, moveCount); // 把后面的元素整体前移一格
        } // 移动判断结束

        count--; // 元素数量减 1
        items[count] = default(T); // 清理最后一个位置,避免引用类型无法释放
    } // RemoveAt 方法结束

    public int IndexOf(T value) // 查找元素下标
    { // IndexOf 方法开始
        EqualityComparer<T> comparer = EqualityComparer<T>.Default; // 获取默认相等比较器

        for (int i = 0; i < count; i++) // 遍历有效元素
        { // for 开始
            if (comparer.Equals(items[i], value)) // 如果元素相等
            { // 相等判断开始
                return i; // 返回下标
            } // 相等判断结束
        } // for 结束

        return -1; // 没找到就返回 -1
    } // IndexOf 方法结束

    public bool Contains(T value) // 判断是否包含元素
    { // Contains 方法开始
        return IndexOf(value) >= 0; // 下标大于等于 0 表示存在
    } // Contains 方法结束

    public void Clear() // 清空动态数组
    { // Clear 方法开始
        Array.Clear(items, 0, count); // 清理有效元素,释放引用
        count = 0; // 元素数量归零
    } // Clear 方法结束

    public void EnsureCapacity(int minCapacity) // 确保容量至少达到指定大小
    { // EnsureCapacity 方法开始
        if (items.Length >= minCapacity) // 如果当前容量已经足够
        { // 容量足够判断开始
            return; // 不需要扩容
        } // 容量足够判断结束

        int newCapacity = items.Length == 0 ? DefaultCapacity : items.Length * 2; // 计算新容量
        newCapacity = Math.Max(newCapacity, minCapacity); // 确保新容量不小于需求容量
        T[] newItems = new T[newCapacity]; // 创建更大的新数组
        Array.Copy(items, 0, newItems, 0, count); // 把旧数组的有效元素复制过去
        items = newItems; // 替换底层数组引用
    } // EnsureCapacity 方法结束

    private void CheckIndex(int index) // 检查访问下标
    { // CheckIndex 方法开始
        if (index < 0 || index >= count) // 如果下标越界
        { // 越界判断开始
            throw new ArgumentOutOfRangeException(nameof(index)); // 抛出下标越界异常
        } // 越界判断结束
    } // CheckIndex 方法结束

    public IEnumerator<T> GetEnumerator() // 获取泛型迭代器
    { // GetEnumerator 方法开始
        for (int i = 0; i < count; i++) // 遍历有效元素
        { // for 开始
            yield return items[i]; // 返回当前元素
        } // for 结束
    } // GetEnumerator 方法结束

    IEnumerator IEnumerable.GetEnumerator() // 获取非泛型迭代器
    { // 非泛型 GetEnumerator 方法开始
        return GetEnumerator(); // 复用泛型迭代器
    } // 非泛型 GetEnumerator 方法结束
} // DynamicArray 类结束

使用示例

c
DynamicArray<int> array = new DynamicArray<int>(); // 创建动态数组
array.Add(10); // 添加元素 10
array.Add(20); // 添加元素 20
array.Insert(1, 15); // 在下标 1 插入 15
int value = array[0]; // 读取下标 0 的元素
array.RemoveAt(1); // 删除下标 1 的元素
bool hasTwenty = array.Contains(20); // 判断是否包含 20
array.Clear(); // 清空所有元素

复杂度

尾部 Add:均摊 O(1),扩容那一次是 O(n)。 下标访问:O(1)。 中间 Insert / RemoveAt:最坏 O(n),因为要移动元素。 空间复杂度:O(n)

Unity 里如果已知大概数量,最好提前设置容量,避免运行时频繁扩容带来 GC 和复制开销。

写一个字符串类

csharp-string-class

标准答案

这里按你前面一直用的 C# 风格,写一个教学版 MyString。它不是替代 C# 原生 string,而是用来理解字符串底层:char[] 存字符,Length 表示有效长度,Capacity 表示底层数组容量,追加时容量不够就扩容。

c
using System; // 引入异常类型和 Array
using System.Collections; // 引入 IEnumerable
using System.Collections.Generic; // 引入 IEnumerator

public sealed class MyString : IEnumerable<char> // 定义一个教学版字符串类
{ // MyString 类开始
    private const int DefaultCapacity = 16; // 默认初始容量
    private char[] buffer; // 底层字符缓冲区
    private int length; // 当前有效字符数量

    public int Length => length; // 返回字符串有效长度
    public int Capacity => buffer.Length; // 返回底层数组容量
    public bool IsEmpty => length == 0; // 返回字符串是否为空

    public MyString() // 默认构造函数
    { // 默认构造函数开始
        buffer = new char[DefaultCapacity]; // 创建默认容量的字符数组
        length = 0; // 初始长度为 0
    } // 默认构造函数结束

    public MyString(string value) // 用 C# string 构造 MyString
    { // string 构造函数开始
        value = value ?? string.Empty; // 如果传入 null,就当作空字符串
        int capacity = Math.Max(DefaultCapacity, value.Length); // 计算初始容量
        buffer = new char[capacity]; // 创建底层字符数组
        value.CopyTo(0, buffer, 0, value.Length); // 把 string 的字符复制到 buffer
        length = value.Length; // 设置有效字符数量
    } // string 构造函数结束

    public MyString(char[] source) // 用 char 数组构造 MyString
    { // char 数组构造函数开始
        int sourceLength = source == null ? 0 : source.Length; // 计算源数组长度
        int capacity = Math.Max(DefaultCapacity, sourceLength); // 计算初始容量
        buffer = new char[capacity]; // 创建自己的底层数组
        if (sourceLength > 0) // 如果源数组有内容
        { // 源数组判断开始
            Array.Copy(source, 0, buffer, 0, sourceLength); // 深拷贝源数组字符
        } // 源数组判断结束
        length = sourceLength; // 设置有效字符数量
    } // char 数组构造函数结束

    public char this[int index] // 定义下标访问器
    { // 索引器开始
        get // 读取字符
        { // get 开始
            CheckIndex(index); // 检查下标是否合法
            return buffer[index]; // 返回指定位置的字符
        } // get 结束
        set // 修改字符
        { // set 开始
            CheckIndex(index); // 检查下标是否合法
            buffer[index] = value; // 修改指定位置的字符
        } // set 结束
    } // 索引器结束

    public MyString Append(char value) // 追加单个字符
    { // Append(char) 方法开始
        EnsureCapacity(length + 1); // 确保容量足够放下新字符
        buffer[length] = value; // 把新字符写到末尾
        length++; // 有效长度加 1
        return this; // 返回自身,方便链式调用
    } // Append(char) 方法结束

    public MyString Append(string value) // 追加 C# string
    { // Append(string) 方法开始
        if (string.IsNullOrEmpty(value)) // 如果追加内容为空
        { // 空字符串判断开始
            return this; // 不做任何修改
        } // 空字符串判断结束
        EnsureCapacity(length + value.Length); // 确保容量足够
        value.CopyTo(0, buffer, length, value.Length); // 把字符串复制到末尾
        length += value.Length; // 更新有效长度
        return this; // 返回自身
    } // Append(string) 方法结束

    public MyString Append(MyString value) // 追加另一个 MyString
    { // Append(MyString) 方法开始
        if (value == null || value.length == 0) // 如果追加对象为空或没内容
        { // 空对象判断开始
            return this; // 不做任何修改
        } // 空对象判断结束
        EnsureCapacity(length + value.length); // 确保容量足够
        Array.Copy(value.buffer, 0, buffer, length, value.length); // 复制对方有效字符
        length += value.length; // 更新有效长度
        return this; // 返回自身
    } // Append(MyString) 方法结束

    public MyString Insert(int index, string value) // 在指定位置插入字符串
    { // Insert 方法开始
        CheckInsertIndex(index); // 检查插入位置是否合法
        if (string.IsNullOrEmpty(value)) // 如果插入内容为空
        { // 空内容判断开始
            return this; // 不做任何修改
        } // 空内容判断结束
        EnsureCapacity(length + value.Length); // 确保容量足够
        Array.Copy(buffer, index, buffer, index + value.Length, length - index); // 把插入点后的字符整体后移
        value.CopyTo(0, buffer, index, value.Length); // 把新字符串复制到插入位置
        length += value.Length; // 更新有效长度
        return this; // 返回自身
    } // Insert 方法结束

    public MyString Remove(int index, int removeCount) // 删除指定范围的字符
    { // Remove 方法开始
        CheckRemoveRange(index, removeCount); // 检查删除范围是否合法
        if (removeCount == 0) // 如果删除数量为 0
        { // 数量判断开始
            return this; // 不做任何修改
        } // 数量判断结束
        int moveCount = length - index - removeCount; // 计算需要前移的字符数量
        if (moveCount > 0) // 如果删除位置后面还有字符
        { // 移动判断开始
            Array.Copy(buffer, index + removeCount, buffer, index, moveCount); // 把后面的字符前移
        } // 移动判断结束
        Array.Clear(buffer, length - removeCount, removeCount); // 清理末尾无效区域
        length -= removeCount; // 更新有效长度
        return this; // 返回自身
    } // Remove 方法结束

    public int IndexOf(char value) // 查找字符第一次出现的位置
    { // IndexOf 方法开始
        for (int i = 0; i < length; i++) // 遍历有效字符
        { // for 开始
            if (buffer[i] == value) // 如果当前字符匹配
            { // 匹配判断开始
                return i; // 返回当前位置
            } // 匹配判断结束
        } // for 结束
        return -1; // 没找到返回 -1
    } // IndexOf 方法结束

    public bool Contains(char value) // 判断是否包含某个字符
    { // Contains 方法开始
        return IndexOf(value) >= 0; // 下标大于等于 0 表示存在
    } // Contains 方法结束

    public void Clear() // 清空字符串内容
    { // Clear 方法开始
        Array.Clear(buffer, 0, length); // 清理有效字符区域
        length = 0; // 长度归零
    } // Clear 方法结束

    public void TrimExcess() // 裁剪多余容量
    { // TrimExcess 方法开始
        if (buffer.Length == length) // 如果容量刚好等于长度
        { // 容量判断开始
            return; // 不需要裁剪
        } // 容量判断结束
        int newCapacity = Math.Max(length, DefaultCapacity); // 计算新容量
        char[] newBuffer = new char[newCapacity]; // 创建新缓冲区
        Array.Copy(buffer, 0, newBuffer, 0, length); // 复制有效字符
        buffer = newBuffer; // 替换底层数组
    } // TrimExcess 方法结束

    public override string ToString() // 转成 C# 原生 string
    { // ToString 方法开始
        return new string(buffer, 0, length); // 只把有效字符区间转成 string
    } // ToString 方法结束

    private void EnsureCapacity(int minCapacity) // 确保容量足够
    { // EnsureCapacity 方法开始
        if (buffer.Length >= minCapacity) // 如果当前容量足够
        { // 容量足够判断开始
            return; // 不需要扩容
        } // 容量足够判断结束
        int newCapacity = buffer.Length == 0 ? DefaultCapacity : buffer.Length * 2; // 计算初始新容量
        while (newCapacity < minCapacity) // 如果新容量仍然不够
        { // while 开始
            newCapacity *= 2; // 容量继续翻倍
        } // while 结束
        char[] newBuffer = new char[newCapacity]; // 创建更大的字符数组
        Array.Copy(buffer, 0, newBuffer, 0, length); // 复制旧字符
        buffer = newBuffer; // 替换底层缓冲区
    } // EnsureCapacity 方法结束

    private void CheckIndex(int index) // 检查访问下标
    { // CheckIndex 方法开始
        if (index < 0 || index >= length) // 如果下标越界
        { // 越界判断开始
            throw new ArgumentOutOfRangeException(nameof(index)); // 抛出下标越界异常
        } // 越界判断结束
    } // CheckIndex 方法结束

    private void CheckInsertIndex(int index) // 检查插入下标
    { // CheckInsertIndex 方法开始
        if (index < 0 || index > length) // 插入位置允许等于 length
        { // 越界判断开始
            throw new ArgumentOutOfRangeException(nameof(index)); // 抛出插入位置异常
        } // 越界判断结束
    } // CheckInsertIndex 方法结束

    private void CheckRemoveRange(int index, int removeCount) // 检查删除范围
    { // CheckRemoveRange 方法开始
        if (index < 0 || index > length) // 如果起始位置越界
        { // 起始位置判断开始
            throw new ArgumentOutOfRangeException(nameof(index)); // 抛出起始位置异常
        } // 起始位置判断结束
        if (removeCount < 0 || index + removeCount > length) // 如果删除数量非法
        { // 删除数量判断开始
            throw new ArgumentOutOfRangeException(nameof(removeCount)); // 抛出删除数量异常
        } // 删除数量判断结束
    } // CheckRemoveRange 方法结束

    public IEnumerator<char> GetEnumerator() // 获取字符迭代器
    { // GetEnumerator 方法开始
        for (int i = 0; i < length; i++) // 遍历有效字符
        { // for 开始
            yield return buffer[i]; // 返回当前字符
        } // for 结束
    } // GetEnumerator 方法结束

    IEnumerator IEnumerable.GetEnumerator() // 获取非泛型迭代器
    { // 非泛型 GetEnumerator 方法开始
        return GetEnumerator(); // 复用泛型迭代器
    } // 非泛型 GetEnumerator 方法结束
} // MyString 类结束

使用示例

c
MyString text = new MyString("Hello"); // 创建字符串对象
text.Append(' '); // 追加空格
text.Append("Unity"); // 追加字符串
text.Insert(5, ","); // 在 Hello 后面插入逗号
text.Remove(5, 1); // 删除刚才插入的逗号
string result = text.ToString(); // 转成 C# 原生 string

复杂度

下标访问是 O(1),尾部追加均摊是 O(1),扩容那一次是 O(n),中间插入和删除最坏是 O(n),因为要移动字符。

C# 原生 string 是不可变的,每次拼接通常会产生新对象;这个教学版更像简化版 StringBuilder,适合理解“字符数组 + 长度 + 容量 + 扩容”的底层思路。

写一个智能指针

cpp-smart-pointer-shared-ptr

标准答案

面试里“写一个智能指针”一般默认写简化版 shared_ptr,因为它能考到 RAII、引用计数、拷贝构造、赋值运算符、析构释放这些核心点。

c
#include <cstddef> // 引入空指针相关基础定义

template <typename T> // 定义模板,让智能指针可以管理任意类型
class SharedPtr // 定义简化版 shared_ptr
{ // 类开始
private: // 私有成员开始
    T* ptr_; // 保存真正被管理的对象指针
    int* count_; // 保存引用计数指针,多个 SharedPtr 共享同一个计数

    void AddRef() // 增加引用计数
    { // AddRef 开始
        if (count_ != nullptr) // 如果计数指针有效
        { // 判断开始
            ++(*count_); // 引用计数加一
        } // 判断结束
    } // AddRef 结束

    void Release() // 释放当前持有的资源
    { // Release 开始
        if (count_ == nullptr) // 如果当前没有管理任何资源
        { // 判断开始
            return; // 直接返回
        } // 判断结束

        --(*count_); // 引用计数减一

        if (*count_ == 0) // 如果当前是最后一个拥有者
        { // 判断开始
            delete ptr_; // 释放真正的对象
            delete count_; // 释放引用计数
        } // 判断结束

        ptr_ = nullptr; // 清空对象指针
        count_ = nullptr; // 清空计数指针
    } // Release 结束

public: // 公有成员开始
    explicit SharedPtr(T* ptr = nullptr) // 构造函数,接管裸指针
        : ptr_(ptr), // 保存对象指针
          count_(ptr == nullptr ? nullptr : new int(1)) // 如果 ptr 不为空,就创建引用计数 1
    { // 构造函数开始
    } // 构造函数结束

    SharedPtr(const SharedPtr& other) // 拷贝构造函数
        : ptr_(other.ptr_), // 共享同一个对象指针
          count_(other.count_) // 共享同一个引用计数
    { // 拷贝构造开始
        AddRef(); // 拷贝后引用计数加一
    } // 拷贝构造结束

    SharedPtr(SharedPtr&& other) noexcept // 移动构造函数
        : ptr_(other.ptr_), // 接管对方的对象指针
          count_(other.count_) // 接管对方的引用计数
    { // 移动构造开始
        other.ptr_ = nullptr; // 清空对方对象指针
        other.count_ = nullptr; // 清空对方计数指针
    } // 移动构造结束

    SharedPtr& operator=(const SharedPtr& other) // 拷贝赋值运算符
    { // 拷贝赋值开始
        if (this == &other) // 如果是自己给自己赋值
        { // 自赋值判断开始
            return *this; // 直接返回自己
        } // 自赋值判断结束

        Release(); // 先释放自己原来管理的资源
        ptr_ = other.ptr_; // 共享对方的对象指针
        count_ = other.count_; // 共享对方的引用计数
        AddRef(); // 新资源引用计数加一
        return *this; // 返回当前对象
    } // 拷贝赋值结束

    SharedPtr& operator=(SharedPtr&& other) noexcept // 移动赋值运算符
    { // 移动赋值开始
        if (this == &other) // 如果是自己移动给自己
        { // 自赋值判断开始
            return *this; // 直接返回自己
        } // 自赋值判断结束

        Release(); // 先释放自己原来管理的资源
        ptr_ = other.ptr_; // 接管对方对象指针
        count_ = other.count_; // 接管对方引用计数
        other.ptr_ = nullptr; // 清空对方对象指针
        other.count_ = nullptr; // 清空对方计数指针
        return *this; // 返回当前对象
    } // 移动赋值结束

    ~SharedPtr() // 析构函数
    { // 析构开始
        Release(); // 对引用计数减一,必要时释放资源
    } // 析构结束

    T& operator*() const // 解引用运算符
    { // operator* 开始
        return *ptr_; // 返回对象引用
    } // operator* 结束

    T* operator->() const // 箭头运算符
    { // operator-> 开始
        return ptr_; // 返回对象指针
    } // operator-> 结束

    T* Get() const // 获取裸指针
    { // Get 开始
        return ptr_; // 返回内部对象指针
    } // Get 结束

    int UseCount() const // 获取引用计数
    { // UseCount 开始
        return count_ == nullptr ? 0 : *count_; // 没有资源返回 0,否则返回计数
    } // UseCount 结束

    explicit operator bool() const // 判断是否持有对象
    { // bool 转换开始
        return ptr_ != nullptr; // 对象指针不为空就表示有效
    } // bool 转换结束

    void Reset(T* ptr = nullptr) // 重置管理的新对象
    { // Reset 开始
        if (ptr_ == ptr) // 如果传入的就是当前指针
        { // 相同指针判断开始
            return; // 直接返回,避免重复释放
        } // 相同指针判断结束

        Release(); // 释放旧资源
        ptr_ = ptr; // 保存新对象指针
        count_ = ptr == nullptr ? nullptr : new int(1); // 新对象引用计数从 1 开始
    } // Reset 结束
}; // 类结束

使用示例

c
SharedPtr<int> a(new int(10)); // 创建智能指针 a,引用计数是 1
SharedPtr<int> b = a; // 拷贝给 b,a 和 b 共享对象,引用计数变成 2
*b = 20; // 通过 b 修改对象,a 看到的也是同一个值
int count = a.UseCount(); // 获取引用计数,此时是 2
b.Reset(); // b 放弃资源,引用计数变成 1
a.Reset(); // a 放弃资源,引用计数变成 0,对象被 delete

面试关键点

这个版本是教学版,不等于标准库 std::shared_ptr。真正的 shared_ptr 有控制块、线程安全引用计数、自定义 deleter、weak count、allocator 等机制。它的最大坑是循环引用,比如 A 持有 B,B 又持有 A,引用计数永远不归零,这时要用 weak_ptr 打破环。

写一个线程安全队列

cpp-thread-safe-queue

标准答案

线程安全队列的核心是:普通 std::queue 不能被多个线程同时读写,所以要用 std::mutex 保护队列操作;如果消费者发现队列为空,就用 std::condition_variable 睡眠等待,避免一直空转浪费 CPU。

c
#include <condition_variable> // 引入条件变量
#include <mutex> // 引入互斥锁
#include <queue> // 引入 std::queue
#include <utility> // 引入 std::move

template <typename T> // 定义模板队列
class ThreadSafeQueue // 定义线程安全队列
{ // 类开始
public: // 公有接口开始
    ThreadSafeQueue() = default; // 使用默认构造函数

    ThreadSafeQueue(const ThreadSafeQueue&) = delete; // 禁止拷贝构造,避免锁和队列被错误复制
    ThreadSafeQueue& operator=(const ThreadSafeQueue&) = delete; // 禁止拷贝赋值,避免多个对象管理同一状态

    void Push(const T& value) // 推入左值任务
    { // Push 左值开始
        { // 加锁作用域开始
            std::lock_guard<std::mutex> lock(mutex_); // 自动加锁并在作用域结束时解锁

            if (closed_) // 如果队列已经关闭
            { // 关闭判断开始
                return; // 不再接受新任务
            } // 关闭判断结束

            queue_.push(value); // 把任务放入队列尾部
        } // 加锁作用域结束

        condition_.notify_one(); // 唤醒一个等待中的消费者
    } // Push 左值结束

    void Push(T&& value) // 推入右值任务
    { // Push 右值开始
        { // 加锁作用域开始
            std::lock_guard<std::mutex> lock(mutex_); // 保护队列写入

            if (closed_) // 如果队列已经关闭
            { // 关闭判断开始
                return; // 不再接受新任务
            } // 关闭判断结束

            queue_.push(std::move(value)); // 移动任务到队列尾部
        } // 加锁作用域结束

        condition_.notify_one(); // 通知一个消费者可以取任务
    } // Push 右值结束

    bool TryPop(T& out) // 非阻塞取出任务
    { // TryPop 开始
        std::lock_guard<std::mutex> lock(mutex_); // 加锁保护队列读取

        if (queue_.empty()) // 如果队列为空
        { // 空队列判断开始
            return false; // 立即返回失败,不阻塞
        } // 空队列判断结束

        out = std::move(queue_.front()); // 取出队头任务
        queue_.pop(); // 删除队头任务
        return true; // 返回取出成功
    } // TryPop 结束

    bool WaitPop(T& out) // 阻塞等待并取出任务
    { // WaitPop 开始
        std::unique_lock<std::mutex> lock(mutex_); // 条件变量需要 unique_lock

        condition_.wait(lock, [this]() // 等待直到有任务或队列关闭
        { // wait 谓词开始
            return closed_ || !queue_.empty(); // 防止虚假唤醒,醒来后再次检查条件
        }); // wait 谓词结束

        if (queue_.empty()) // 如果醒来后队列仍为空
        { // 空队列判断开始
            return false; // 说明队列已关闭并且没有任务了
        } // 空队列判断结束

        out = std::move(queue_.front()); // 取出队头任务
        queue_.pop(); // 删除队头任务
        return true; // 返回取出成功
    } // WaitPop 结束

    void Close() // 关闭队列
    { // Close 开始
        { // 加锁作用域开始
            std::lock_guard<std::mutex> lock(mutex_); // 加锁保护 closed_ 状态
            closed_ = true; // 标记队列已经关闭
        } // 加锁作用域结束

        condition_.notify_all(); // 唤醒所有等待线程,让它们能正常退出
    } // Close 结束

    bool IsClosed() const // 判断队列是否关闭
    { // IsClosed 开始
        std::lock_guard<std::mutex> lock(mutex_); // 加锁读取 closed_
        return closed_; // 返回关闭状态
    } // IsClosed 结束

    bool Empty() const // 判断队列是否为空
    { // Empty 开始
        std::lock_guard<std::mutex> lock(mutex_); // 加锁读取队列状态
        return queue_.empty(); // 返回是否为空
    } // Empty 结束

    std::size_t Size() const // 获取队列大小
    { // Size 开始
        std::lock_guard<std::mutex> lock(mutex_); // 加锁读取队列大小
        return queue_.size(); // 返回元素数量
    } // Size 结束

private: // 私有成员开始
    mutable std::mutex mutex_; // 保护队列和关闭状态的互斥锁
    std::condition_variable condition_; // 用于阻塞和唤醒消费者线程
    std::queue<T> queue_; // 真正保存任务的普通队列
    bool closed_ = false; // 队列是否已经关闭
}; // 类结束

使用示例

c
ThreadSafeQueue<int> queue; // 创建线程安全队列
queue.Push(100); // 生产者放入任务
int value = 0; // 准备接收任务
bool ok = queue.WaitPop(value); // 消费者阻塞等待任务
queue.Close(); // 关闭队列并唤醒所有等待线程

面试关键点

TryPop 是非阻塞的,队列空就直接返回;WaitPop 是阻塞的,队列空就睡眠等待。wait 一定要带谓词,因为条件变量可能虚假唤醒。Close 很重要,否则消费者线程可能永远卡在 WaitPop

游戏里常见用途是日志队列、资源加载任务队列、异步 IO 任务队列。注意任务处理不要放在锁里,锁只保护“队列结构本身”。

写一个内存池

cpp-memory-pool

标准答案

内存池的核心是:先向系统申请一大块内存 Chunk,再切成很多固定大小的 Block。申请对象时从空闲链表 FreeList 取一个块,释放对象时把块放回 FreeList,这样就避免频繁 new/deletemalloc/free

c
#include <cstddef> // 引入 std::size_t
#include <new> // 引入 placement new
#include <utility> // 引入 std::forward
#include <vector> // 引入 std::vector

class FixedBlockMemoryPool // 定义固定块内存池
{ // 类开始
private: // 私有成员开始
    struct FreeNode // 定义空闲块节点
    { // FreeNode 开始
        FreeNode* next; // 指向下一个空闲块
    }; // FreeNode 结束

    std::size_t blockSize_; // 每个块的大小
    std::size_t blockCount_; // 每个 Chunk 里有多少个块
    FreeNode* freeList_; // 空闲块链表头
    std::vector<void*> chunks_; // 保存所有申请过的大块内存

public: // 公有成员开始
    FixedBlockMemoryPool(std::size_t blockSize, std::size_t blockCount) // 构造函数
        : blockSize_(blockSize < sizeof(FreeNode*) ? sizeof(FreeNode*) : blockSize), // 块大小至少能放下 next 指针
          blockCount_(blockCount), // 保存每个 Chunk 的块数量
          freeList_(nullptr) // 初始化空闲链表为空
    { // 构造函数开始
        AllocateChunk(); // 先申请一个 Chunk
    } // 构造函数结束

    FixedBlockMemoryPool(const FixedBlockMemoryPool&) = delete; // 禁止拷贝构造
    FixedBlockMemoryPool& operator=(const FixedBlockMemoryPool&) = delete; // 禁止拷贝赋值

    ~FixedBlockMemoryPool() // 析构函数
    { // 析构开始
        for (void* chunk : chunks_) // 遍历所有 Chunk
        { // for 开始
            ::operator delete(chunk); // 释放整块原始内存
        } // for 结束
    } // 析构结束

    void* Allocate() // 申请一块内存
    { // Allocate 开始
        if (freeList_ == nullptr) // 如果没有空闲块
        { // 空闲链表判断开始
            AllocateChunk(); // 申请新的 Chunk
        } // 空闲链表判断结束

        FreeNode* node = freeList_; // 取出空闲链表头
        freeList_ = freeList_->next; // 链表头移动到下一个节点
        return node; // 返回这块内存
    } // Allocate 结束

    void Deallocate(void* ptr) // 归还一块内存
    { // Deallocate 开始
        if (ptr == nullptr) // 如果传入空指针
        { // 空指针判断开始
            return; // 直接返回
        } // 空指针判断结束

        FreeNode* node = static_cast<FreeNode*>(ptr); // 把内存块当成 FreeNode 使用
        node->next = freeList_; // 新归还的块指向旧链表头
        freeList_ = node; // 更新空闲链表头
    } // Deallocate 结束

private: // 私有函数开始
    void AllocateChunk() // 申请一个新的 Chunk
    { // AllocateChunk 开始
        void* chunk = ::operator new(blockSize_ * blockCount_); // 一次申请一大块原始内存
        chunks_.push_back(chunk); // 保存 Chunk 指针,析构时统一释放

        char* start = static_cast<char*>(chunk); // 转成 char* 方便按字节偏移

        for (std::size_t i = 0; i < blockCount_; ++i) // 遍历 Chunk 中每个块
        { // for 开始
            void* block = start + i * blockSize_; // 计算第 i 个块的地址
            Deallocate(block); // 把这个块加入空闲链表
        } // for 结束
    } // AllocateChunk 结束
}; // FixedBlockMemoryPool 类结束

template <typename T> // 定义对象池模板
class ObjectPool // 定义面向对象的内存池封装
{ // ObjectPool 类开始
private: // 私有成员开始
    FixedBlockMemoryPool pool_; // 底层固定块内存池

public: // 公有成员开始
    explicit ObjectPool(std::size_t blockCount = 64) // 构造函数
        : pool_(sizeof(T), blockCount) // 每个块大小等于 T 的大小
    { // 构造函数开始
    } // 构造函数结束

    ObjectPool(const ObjectPool&) = delete; // 禁止拷贝构造
    ObjectPool& operator=(const ObjectPool&) = delete; // 禁止拷贝赋值

    template <typename... Args> // 支持任意构造参数
    T* Create(Args&&... args) // 创建对象
    { // Create 开始
        void* memory = pool_.Allocate(); // 从内存池申请原始内存
        return new (memory) T(std::forward<Args>(args)...); // 用 placement new 在这块内存上构造对象
    } // Create 结束

    void Destroy(T* object) // 销毁对象
    { // Destroy 开始
        if (object == nullptr) // 如果对象为空
        { // 空对象判断开始
            return; // 直接返回
        } // 空对象判断结束

        object->~T(); // 手动调用析构函数
        pool_.Deallocate(object); // 把内存块归还给内存池
    } // Destroy 结束
}; // ObjectPool 类结束

使用示例

c
struct Bullet // 定义子弹对象
{ // Bullet 开始
    int id; // 子弹 ID
    float speed; // 子弹速度

    Bullet(int id, float speed) // 子弹构造函数
        : id(id), // 初始化 id
          speed(speed) // 初始化 speed
    { // 构造函数开始
    } // 构造函数结束
}; // Bullet 结束

ObjectPool<Bullet> bulletPool(128); // 创建子弹对象池
Bullet* bullet = bulletPool.Create(1, 30.0f); // 从对象池创建子弹
bulletPool.Destroy(bullet); // 销毁子弹并归还内存

面试关键点

内存池把“申请内存”和“构造对象”分开了:Allocate 只拿原始内存,placement new 才真正构造对象;释放时也要先手动调用析构函数,再把内存块归还池子。

它适合大量同尺寸对象频繁创建销毁,比如子弹、粒子、消息节点。常见坑是重复释放、对象尺寸不匹配、多线程访问没加锁,以及没有做泄漏检测。

写一个组件容器

cpp-component-container

标准答案

组件容器常用于 ECS:Entity 只是 ID,真正的组件数据连续存在 vector<T> 里;再用 unordered_map<Entity, index> 把实体映射到组件下标。这样查询快,遍历也快。

c
#include <cstdint> // 引入固定宽度整数类型
#include <stdexcept> // 引入异常类型
#include <unordered_map> // 引入哈希表
#include <utility> // 引入 std::forward 和 std::move
#include <vector> // 引入 std::vector

using Entity = std::uint32_t; // 定义实体 ID 类型

template <typename T> // 定义组件容器模板
class ComponentContainer // 定义组件容器类
{ // 类开始
private: // 私有成员开始
    std::vector<T> components_; // 稠密存储同类型组件
    std::vector<Entity> entities_; // 保存每个组件对应的实体 ID
    std::unordered_map<Entity, std::size_t> entityToIndex_; // 保存实体 ID 到组件下标的映射

public: // 公有接口开始
    bool Has(Entity entity) const // 判断实体是否拥有该组件
    { // Has 开始
        return entityToIndex_.find(entity) != entityToIndex_.end(); // 哈希表中存在就表示有组件
    } // Has 结束

    template <typename... Args> // 支持任意构造参数
    T& Emplace(Entity entity, Args&&... args) // 给实体添加组件
    { // Emplace 开始
        if (Has(entity)) // 如果实体已经有该组件
        { // 判断开始
            throw std::runtime_error("Entity already has this component."); // 抛出重复添加异常
        } // 判断结束

        std::size_t index = components_.size(); // 新组件下标就是当前数组长度
        components_.emplace_back(std::forward<Args>(args)...); // 在组件数组尾部构造组件
        entities_.push_back(entity); // 记录这个下标对应的实体
        entityToIndex_[entity] = index; // 建立实体到下标的映射
        return components_.back(); // 返回刚创建的组件引用
    } // Emplace 结束

    template <typename... Args> // 支持任意构造参数
    T& AddOrReplace(Entity entity, Args&&... args) // 添加或替换组件
    { // AddOrReplace 开始
        auto it = entityToIndex_.find(entity); // 查找实体是否已有组件

        if (it != entityToIndex_.end()) // 如果已经有组件
        { // 判断开始
            components_[it->second] = T(std::forward<Args>(args)...); // 用新组件覆盖旧组件
            return components_[it->second]; // 返回替换后的组件引用
        } // 判断结束

        return Emplace(entity, std::forward<Args>(args)...); // 没有组件就新建组件
    } // AddOrReplace 结束

    T& Get(Entity entity) // 获取组件引用
    { // Get 开始
        auto it = entityToIndex_.find(entity); // 查找实体对应下标

        if (it == entityToIndex_.end()) // 如果找不到组件
        { // 判断开始
            throw std::runtime_error("Entity does not have this component."); // 抛出不存在异常
        } // 判断结束

        return components_[it->second]; // 返回组件引用
    } // Get 结束

    const T& Get(Entity entity) const // 获取只读组件引用
    { // const Get 开始
        auto it = entityToIndex_.find(entity); // 查找实体对应下标

        if (it == entityToIndex_.end()) // 如果找不到组件
        { // 判断开始
            throw std::runtime_error("Entity does not have this component."); // 抛出不存在异常
        } // 判断结束

        return components_[it->second]; // 返回只读组件引用
    } // const Get 结束

    T* TryGet(Entity entity) // 尝试获取组件指针
    { // TryGet 开始
        auto it = entityToIndex_.find(entity); // 查找实体对应下标
        return it == entityToIndex_.end() ? nullptr : &components_[it->second]; // 找不到返回空指针
    } // TryGet 结束

    bool Remove(Entity entity) // 删除实体身上的组件
    { // Remove 开始
        auto it = entityToIndex_.find(entity); // 查找实体对应下标

        if (it == entityToIndex_.end()) // 如果实体没有该组件
        { // 判断开始
            return false; // 删除失败
        } // 判断结束

        std::size_t removeIndex = it->second; // 记录要删除的位置
        std::size_t lastIndex = components_.size() - 1; // 记录最后一个组件的位置
        Entity lastEntity = entities_[lastIndex]; // 记录最后一个组件对应的实体

        if (removeIndex != lastIndex) // 如果删除的不是最后一个
        { // 判断开始
            components_[removeIndex] = std::move(components_[lastIndex]); // 用最后一个组件填补空洞
            entities_[removeIndex] = lastEntity; // 同步移动实体 ID
            entityToIndex_[lastEntity] = removeIndex; // 更新被移动实体的新下标
        } // 判断结束

        components_.pop_back(); // 删除最后一个组件
        entities_.pop_back(); // 删除最后一个实体记录
        entityToIndex_.erase(entity); // 删除被移除实体的映射
        return true; // 删除成功
    } // Remove 结束

    template <typename Func> // 支持传入任意可调用对象
    void ForEach(Func&& func) // 遍历所有组件
    { // ForEach 开始
        for (std::size_t i = 0; i < components_.size(); ++i) // 遍历组件数组
        { // for 开始
            func(entities_[i], components_[i]); // 把实体 ID 和组件传给外部逻辑
        } // for 结束
    } // ForEach 结束

    void Clear() // 清空容器
    { // Clear 开始
        components_.clear(); // 清空组件数组
        entities_.clear(); // 清空实体数组
        entityToIndex_.clear(); // 清空映射表
    } // Clear 结束

    std::size_t Size() const // 获取组件数量
    { // Size 开始
        return components_.size(); // 返回组件数组大小
    } // Size 结束
}; // 类结束

使用示例

c
struct Transform // 定义 Transform 组件
{ // Transform 开始
    float x; // x 坐标
    float y; // y 坐标
}; // Transform 结束

ComponentContainer<Transform> transforms; // 创建 Transform 组件容器
transforms.Emplace(100, Transform{1.0f, 2.0f}); // 给实体 100 添加组件
Transform& t = transforms.Get(100); // 获取实体 100 的组件
t.x += 10.0f; // 修改组件数据
transforms.Remove(100); // 删除实体 100 的组件

面试关键点

这个设计的优点是遍历快,因为组件连续存储,CPU 缓存友好;查询也快,因为 Entity -> index 是哈希表。删除用 swap-remove 可以做到 O(1),但代价是组件顺序不稳定。

写一个事件分发器

cpp-event-dispatcher

标准答案

事件分发器的核心是:发布者只负责发事件,不关心谁处理;监听者按事件类型注册回调;分发器根据事件类型找到监听列表并依次调用。它适合 UI 刷新、任务进度、成就触发、日志通知这类“通知型逻辑”。

c
#include <algorithm> // 引入 std::remove_if
#include <cstdint> // 引入 std::uint64_t
#include <functional> // 引入 std::function
#include <typeindex> // 引入 std::type_index
#include <typeinfo> // 引入 typeid
#include <unordered_map> // 引入 std::unordered_map
#include <utility> // 引入 std::move
#include <vector> // 引入 std::vector

class EventDispatcher // 定义事件分发器
{ // 类开始
public: // 公有类型开始
    using ListenerId = std::uint64_t; // 定义监听器 ID 类型

private: // 私有成员开始
    struct Listener // 定义监听器结构
    { // Listener 开始
        ListenerId id = 0; // 保存监听器唯一 ID
        std::function<void(const void*)> callback; // 保存类型擦除后的回调
        bool removed = false; // 标记该监听器是否已被移除
    }; // Listener 结束

    std::unordered_map<std::type_index, std::vector<Listener>> listeners_; // 事件类型到监听列表的映射
    std::unordered_map<std::type_index, int> dispatchDepth_; // 记录某类事件当前是否正在派发
    ListenerId nextId_ = 1; // 下一个监听器 ID

public: // 公有函数开始
    template <typename Event> // 订阅某种事件类型
    ListenerId Subscribe(std::function<void(const Event&)> callback) // 注册监听回调
    { // Subscribe 开始
        if (!callback) // 如果回调为空
        { // 空回调判断开始
            return 0; // 返回无效 ID
        } // 空回调判断结束

        std::type_index type(typeid(Event)); // 获取事件类型标识
        Listener listener; // 创建监听器对象
        listener.id = nextId_++; // 分配监听器 ID
        listener.removed = false; // 默认没有被移除
        listener.callback = [callback](const void* eventPtr) // 包装成 void* 回调
        { // lambda 开始
            callback(*static_cast<const Event*>(eventPtr)); // 转回真实事件类型并调用
        }; // lambda 结束

        listeners_[type].push_back(std::move(listener)); // 加入对应事件类型的监听列表
        return nextId_ - 1; // 返回刚刚分配的监听器 ID
    } // Subscribe 结束

    template <typename Event> // 取消订阅某种事件类型
    void Unsubscribe(ListenerId id) // 注销监听器
    { // Unsubscribe 开始
        std::type_index type(typeid(Event)); // 获取事件类型标识
        auto it = listeners_.find(type); // 查找该事件类型的监听列表

        if (it == listeners_.end()) // 如果该事件类型没有监听器
        { // 判断开始
            return; // 直接返回
        } // 判断结束

        std::vector<Listener>& list = it->second; // 取出监听列表引用
        bool dispatching = IsDispatching(type); // 判断当前是否正在派发该事件

        for (auto listenerIt = list.begin(); listenerIt != list.end(); ++listenerIt) // 遍历监听列表
        { // for 开始
            if (listenerIt->id != id) // 如果 ID 不匹配
            { // 判断开始
                continue; // 继续找下一个
            } // 判断结束

            if (dispatching) // 如果正在派发中
            { // 派发中分支开始
                listenerIt->removed = true; // 只做标记,避免迭代器失效
            } // 派发中分支结束
            else // 如果当前没有派发
            { // 非派发分支开始
                list.erase(listenerIt); // 直接从列表删除
            } // 非派发分支结束

            break; // 找到目标后结束循环
        } // for 结束

        if (!dispatching && list.empty()) // 如果没有派发且列表已经空了
        { // 空列表判断开始
            listeners_.erase(it); // 移除该事件类型
        } // 空列表判断结束
    } // Unsubscribe 结束

    template <typename Event> // 派发某种事件
    void Dispatch(const Event& event) // 分发事件
    { // Dispatch 开始
        std::type_index type(typeid(Event)); // 获取事件类型标识
        auto it = listeners_.find(type); // 查找监听列表

        if (it == listeners_.end()) // 如果没有监听者
        { // 判断开始
            return; // 直接返回
        } // 判断结束

        BeginDispatch(type); // 标记该事件正在派发
        std::vector<Listener>& list = it->second; // 获取监听列表引用
        std::size_t originalSize = list.size(); // 记录派发开始时的监听数量

        for (std::size_t i = 0; i < originalSize && i < list.size(); ++i) // 按注册顺序遍历监听器
        { // for 开始
            if (list[i].removed) // 如果监听器已经被标记删除
            { // 判断开始
                continue; // 跳过这个监听器
            } // 判断结束

            list[i].callback(&event); // 调用监听器回调
        } // for 结束

        EndDispatch(type); // 标记派发结束并清理删除项
    } // Dispatch 结束

    void Clear() // 清空所有事件监听
    { // Clear 开始
        listeners_.clear(); // 清空所有监听列表
        dispatchDepth_.clear(); // 清空派发状态
    } // Clear 结束

private: // 私有函数开始
    bool IsDispatching(const std::type_index& type) const // 判断某类事件是否正在派发
    { // IsDispatching 开始
        auto it = dispatchDepth_.find(type); // 查找派发深度
        return it != dispatchDepth_.end() && it->second > 0; // 深度大于 0 表示正在派发
    } // IsDispatching 结束

    void BeginDispatch(const std::type_index& type) // 开始派发
    { // BeginDispatch 开始
        dispatchDepth_[type]++; // 派发深度加一
    } // BeginDispatch 结束

    void EndDispatch(const std::type_index& type) // 结束派发
    { // EndDispatch 开始
        dispatchDepth_[type]--; // 派发深度减一

        if (dispatchDepth_[type] == 0) // 如果该事件派发完全结束
        { // 判断开始
            dispatchDepth_.erase(type); // 移除派发状态
            Compact(type); // 清理被标记删除的监听器
        } // 判断结束
    } // EndDispatch 结束

    void Compact(const std::type_index& type) // 清理删除标记
    { // Compact 开始
        auto it = listeners_.find(type); // 查找监听列表

        if (it == listeners_.end()) // 如果列表不存在
        { // 判断开始
            return; // 直接返回
        } // 判断结束

        std::vector<Listener>& list = it->second; // 获取监听列表
        list.erase(std::remove_if(list.begin(), list.end(), [](const Listener& listener) // 删除被标记的监听器
        { // lambda 开始
            return listener.removed; // removed 为 true 就删除
        }), list.end()); // erase-remove 惯用法结束

        if (list.empty()) // 如果清理后列表为空
        { // 空列表判断开始
            listeners_.erase(it); // 删除该事件类型
        } // 空列表判断结束
    } // Compact 结束
}; // 类结束

使用示例

c
struct DamageEvent // 定义伤害事件
{ // DamageEvent 开始
    int targetId; // 受击目标 ID
    int damage; // 伤害值
}; // DamageEvent 结束

EventDispatcher dispatcher; // 创建事件分发器

EventDispatcher::ListenerId id = dispatcher.Subscribe<DamageEvent>([](const DamageEvent& event) // 注册伤害事件监听
{ // lambda 开始
    int target = event.targetId; // 读取受击目标
    int damage = event.damage; // 读取伤害值
}); // lambda 结束

dispatcher.Dispatch(DamageEvent{1001, 50}); // 派发一次伤害事件
dispatcher.Unsubscribe<DamageEvent>(id); // 取消监听

面试关键点

事件分发器能降低模块耦合,比如战斗模块发 DamageEvent,UI、任务、成就系统各自监听。但它不是万能的:核心流程不要全靠事件串起来,否则调用链会很难追踪。派发过程中注销监听时,不能直接 erase,否则容易导致迭代器失效,所以这里用 removed 标记,派发结束后统一清理。

写一个二叉树遍历

csharp-binary-tree-traversal

标准答案

事件分发器的核心是:发布者只负责发事件,不关心谁处理;监听者按事件类型注册回调;分发器根据事件类型找到监听列表并依次调用。它适合 UI 刷新、任务进度、成就触发、日志通知这类“通知型逻辑”。

c
#include <algorithm> // 引入 std::remove_if
#include <cstdint> // 引入 std::uint64_t
#include <functional> // 引入 std::function
#include <typeindex> // 引入 std::type_index
#include <typeinfo> // 引入 typeid
#include <unordered_map> // 引入 std::unordered_map
#include <utility> // 引入 std::move
#include <vector> // 引入 std::vector

class EventDispatcher // 定义事件分发器
{ // 类开始
public: // 公有类型开始
    using ListenerId = std::uint64_t; // 定义监听器 ID 类型

private: // 私有成员开始
    struct Listener // 定义监听器结构
    { // Listener 开始
        ListenerId id = 0; // 保存监听器唯一 ID
        std::function<void(const void*)> callback; // 保存类型擦除后的回调
        bool removed = false; // 标记该监听器是否已被移除
    }; // Listener 结束

    std::unordered_map<std::type_index, std::vector<Listener>> listeners_; // 事件类型到监听列表的映射
    std::unordered_map<std::type_index, int> dispatchDepth_; // 记录某类事件当前是否正在派发
    ListenerId nextId_ = 1; // 下一个监听器 ID

public: // 公有函数开始
    template <typename Event> // 订阅某种事件类型
    ListenerId Subscribe(std::function<void(const Event&)> callback) // 注册监听回调
    { // Subscribe 开始
        if (!callback) // 如果回调为空
        { // 空回调判断开始
            return 0; // 返回无效 ID
        } // 空回调判断结束

        std::type_index type(typeid(Event)); // 获取事件类型标识
        Listener listener; // 创建监听器对象
        listener.id = nextId_++; // 分配监听器 ID
        listener.removed = false; // 默认没有被移除
        listener.callback = [callback](const void* eventPtr) // 包装成 void* 回调
        { // lambda 开始
            callback(*static_cast<const Event*>(eventPtr)); // 转回真实事件类型并调用
        }; // lambda 结束

        listeners_[type].push_back(std::move(listener)); // 加入对应事件类型的监听列表
        return nextId_ - 1; // 返回刚刚分配的监听器 ID
    } // Subscribe 结束

    template <typename Event> // 取消订阅某种事件类型
    void Unsubscribe(ListenerId id) // 注销监听器
    { // Unsubscribe 开始
        std::type_index type(typeid(Event)); // 获取事件类型标识
        auto it = listeners_.find(type); // 查找该事件类型的监听列表

        if (it == listeners_.end()) // 如果该事件类型没有监听器
        { // 判断开始
            return; // 直接返回
        } // 判断结束

        std::vector<Listener>& list = it->second; // 取出监听列表引用
        bool dispatching = IsDispatching(type); // 判断当前是否正在派发该事件

        for (auto listenerIt = list.begin(); listenerIt != list.end(); ++listenerIt) // 遍历监听列表
        { // for 开始
            if (listenerIt->id != id) // 如果 ID 不匹配
            { // 判断开始
                continue; // 继续找下一个
            } // 判断结束

            if (dispatching) // 如果正在派发中
            { // 派发中分支开始
                listenerIt->removed = true; // 只做标记,避免迭代器失效
            } // 派发中分支结束
            else // 如果当前没有派发
            { // 非派发分支开始
                list.erase(listenerIt); // 直接从列表删除
            } // 非派发分支结束

            break; // 找到目标后结束循环
        } // for 结束

        if (!dispatching && list.empty()) // 如果没有派发且列表已经空了
        { // 空列表判断开始
            listeners_.erase(it); // 移除该事件类型
        } // 空列表判断结束
    } // Unsubscribe 结束

    template <typename Event> // 派发某种事件
    void Dispatch(const Event& event) // 分发事件
    { // Dispatch 开始
        std::type_index type(typeid(Event)); // 获取事件类型标识
        auto it = listeners_.find(type); // 查找监听列表

        if (it == listeners_.end()) // 如果没有监听者
        { // 判断开始
            return; // 直接返回
        } // 判断结束

        BeginDispatch(type); // 标记该事件正在派发
        std::vector<Listener>& list = it->second; // 获取监听列表引用
        std::size_t originalSize = list.size(); // 记录派发开始时的监听数量

        for (std::size_t i = 0; i < originalSize && i < list.size(); ++i) // 按注册顺序遍历监听器
        { // for 开始
            if (list[i].removed) // 如果监听器已经被标记删除
            { // 判断开始
                continue; // 跳过这个监听器
            } // 判断结束

            list[i].callback(&event); // 调用监听器回调
        } // for 结束

        EndDispatch(type); // 标记派发结束并清理删除项
    } // Dispatch 结束

    void Clear() // 清空所有事件监听
    { // Clear 开始
        listeners_.clear(); // 清空所有监听列表
        dispatchDepth_.clear(); // 清空派发状态
    } // Clear 结束

private: // 私有函数开始
    bool IsDispatching(const std::type_index& type) const // 判断某类事件是否正在派发
    { // IsDispatching 开始
        auto it = dispatchDepth_.find(type); // 查找派发深度
        return it != dispatchDepth_.end() && it->second > 0; // 深度大于 0 表示正在派发
    } // IsDispatching 结束

    void BeginDispatch(const std::type_index& type) // 开始派发
    { // BeginDispatch 开始
        dispatchDepth_[type]++; // 派发深度加一
    } // BeginDispatch 结束

    void EndDispatch(const std::type_index& type) // 结束派发
    { // EndDispatch 开始
        dispatchDepth_[type]--; // 派发深度减一

        if (dispatchDepth_[type] == 0) // 如果该事件派发完全结束
        { // 判断开始
            dispatchDepth_.erase(type); // 移除派发状态
            Compact(type); // 清理被标记删除的监听器
        } // 判断结束
    } // EndDispatch 结束

    void Compact(const std::type_index& type) // 清理删除标记
    { // Compact 开始
        auto it = listeners_.find(type); // 查找监听列表

        if (it == listeners_.end()) // 如果列表不存在
        { // 判断开始
            return; // 直接返回
        } // 判断结束

        std::vector<Listener>& list = it->second; // 获取监听列表
        list.erase(std::remove_if(list.begin(), list.end(), [](const Listener& listener) // 删除被标记的监听器
        { // lambda 开始
            return listener.removed; // removed 为 true 就删除
        }), list.end()); // erase-remove 惯用法结束

        if (list.empty()) // 如果清理后列表为空
        { // 空列表判断开始
            listeners_.erase(it); // 删除该事件类型
        } // 空列表判断结束
    } // Compact 结束
}; // 类结束

使用示例

c
struct DamageEvent // 定义伤害事件
{ // DamageEvent 开始
    int targetId; // 受击目标 ID
    int damage; // 伤害值
}; // DamageEvent 结束

EventDispatcher dispatcher; // 创建事件分发器

EventDispatcher::ListenerId id = dispatcher.Subscribe<DamageEvent>([](const DamageEvent& event) // 注册伤害事件监听
{ // lambda 开始
    int target = event.targetId; // 读取受击目标
    int damage = event.damage; // 读取伤害值
}); // lambda 结束

dispatcher.Dispatch(DamageEvent{1001, 50}); // 派发一次伤害事件
dispatcher.Unsubscribe<DamageEvent>(id); // 取消监听

面试关键点

事件分发器能降低模块耦合,比如战斗模块发 DamageEvent,UI、任务、成就系统各自监听。但它不是万能的:核心流程不要全靠事件串起来,否则调用链会很难追踪。派发过程中注销监听时,不能直接 erase,否则容易导致迭代器失效,所以这里用 removed 标记,派发结束后统一清理。

写一个哈希表

csharp-hash-table

一句话定义 哈希表就是用 hash(key) 把 key 映射到数组下标,通过“桶数组 + 冲突处理”实现平均 O(1) 的增删查。

底层原理

  1. 先算 hashCode
  2. hash % buckets.Length 找到桶下标。
  3. 如果多个 key 落到同一个桶,就形成冲突。
  4. 这里用“链地址法”:同一个桶里挂一条链表。
  5. 元素太多时扩容,然后重新计算桶位置。
c
using System; // 引入异常类型
using System.Collections.Generic; // 引入 EqualityComparer 和 KeyNotFoundException

public sealed class SimpleHashTable<TKey, TValue> // 定义一个简化版哈希表
{ // 哈希表类开始
    private sealed class Entry // 定义桶里的链表节点
    { // 节点类开始
        public TKey Key; // 保存键
        public TValue Value; // 保存值
        public int Hash; // 保存 hash 值,避免反复计算
        public Entry Next; // 指向同一个桶中的下一个节点

        public Entry(TKey key, TValue value, int hash, Entry next) // 节点构造函数
        { // 构造函数开始
            Key = key; // 初始化键
            Value = value; // 初始化值
            Hash = hash; // 初始化 hash
            Next = next; // 初始化下一个节点
        } // 构造函数结束
    } // 节点类结束

    private Entry[] buckets; // 桶数组,每个位置挂一条链表
    private int count; // 当前键值对数量
    private readonly float loadFactor; // 负载因子,超过后扩容
    private readonly EqualityComparer<TKey> comparer; // key 的相等比较器

    public int Count => count; // 对外暴露元素数量
    public int Capacity => buckets.Length; // 对外暴露桶数组容量

    public SimpleHashTable(int capacity = 16, float loadFactor = 0.75f) // 构造哈希表
    { // 构造函数开始
        if (capacity <= 0) // 判断初始容量是否合法
        { // if 开始
            throw new ArgumentOutOfRangeException(nameof(capacity)); // 容量非法就抛异常
        } // if 结束

        this.loadFactor = loadFactor; // 保存负载因子
        buckets = new Entry[capacity]; // 创建桶数组
        comparer = EqualityComparer<TKey>.Default; // 使用默认 key 比较器
    } // 构造函数结束

    public void Put(TKey key, TValue value) // 添加或更新键值对
    { // Put 开始
        EnsureCapacity(count + 1); // 插入前检查是否需要扩容
        int hash = GetHash(key); // 计算 key 的 hash
        int index = GetBucketIndex(hash, buckets.Length); // 根据 hash 算桶下标
        Entry current = buckets[index]; // 取出当前桶的链表头

        while (current != null) // 遍历当前桶的冲突链
        { // while 开始
            if (current.Hash == hash && comparer.Equals(current.Key, key)) // 先比 hash 再比 Equals
            { // if 开始
                current.Value = value; // key 已存在就覆盖 value
                return; // 更新完成后直接返回
            } // if 结束

            current = current.Next; // 继续检查下一个节点
        } // while 结束

        buckets[index] = new Entry(key, value, hash, buckets[index]); // 头插法插入新节点
        count++; // 元素数量加一
    } // Put 结束

    public bool TryGet(TKey key, out TValue value) // 尝试根据 key 获取 value
    { // TryGet 开始
        int hash = GetHash(key); // 计算 key 的 hash
        int index = GetBucketIndex(hash, buckets.Length); // 找到桶下标
        Entry current = buckets[index]; // 取出桶头节点

        while (current != null) // 遍历冲突链
        { // while 开始
            if (current.Hash == hash && comparer.Equals(current.Key, key)) // 判断是否找到目标 key
            { // if 开始
                value = current.Value; // 输出找到的 value
                return true; // 返回查找成功
            } // if 结束

            current = current.Next; // 继续找下一个节点
        } // while 结束

        value = default(TValue); // 找不到时返回默认值
        return false; // 返回查找失败
    } // TryGet 结束

    public TValue Get(TKey key) // 根据 key 获取 value,找不到就抛异常
    { // Get 开始
        if (TryGet(key, out TValue value)) // 调用 TryGet 查找
        { // if 开始
            return value; // 找到就返回 value
        } // if 结束

        throw new KeyNotFoundException("Key not found."); // 找不到就抛异常
    } // Get 结束

    public bool Remove(TKey key) // 删除指定 key
    { // Remove 开始
        int hash = GetHash(key); // 计算 key 的 hash
        int index = GetBucketIndex(hash, buckets.Length); // 找到桶下标
        Entry current = buckets[index]; // 当前节点从桶头开始
        Entry previous = null; // 前一个节点初始为空

        while (current != null) // 遍历冲突链
        { // while 开始
            if (current.Hash == hash && comparer.Equals(current.Key, key)) // 判断是否找到目标节点
            { // if 开始
                if (previous == null) // 如果删除的是桶头节点
                { // if 开始
                    buckets[index] = current.Next; // 桶头改成下一个节点
                } // if 结束
                else // 如果删除的不是桶头节点
                { // else 开始
                    previous.Next = current.Next; // 让前一个节点跳过当前节点
                } // else 结束

                count--; // 元素数量减一
                return true; // 返回删除成功
            } // if 结束

            previous = current; // 当前节点变成前一个节点
            current = current.Next; // 当前节点往后移动
        } // while 结束

        return false; // 没找到就返回删除失败
    } // Remove 结束

    private void EnsureCapacity(int targetCount) // 检查是否需要扩容
    { // EnsureCapacity 开始
        if (targetCount <= buckets.Length * loadFactor) // 如果数量没有超过阈值
        { // if 开始
            return; // 不需要扩容
        } // if 结束

        Resize(buckets.Length * 2); // 扩容为原来的两倍
    } // EnsureCapacity 结束

    private void Resize(int newCapacity) // 扩容并重新分桶
    { // Resize 开始
        Entry[] oldBuckets = buckets; // 保存旧桶数组
        buckets = new Entry[newCapacity]; // 创建新桶数组
        count = 0; // 数量清零,后面重新统计

        for (int i = 0; i < oldBuckets.Length; i++) // 遍历旧桶数组
        { // for 开始
            Entry current = oldBuckets[i]; // 取出旧桶头节点

            while (current != null) // 遍历旧桶链表
            { // while 开始
                Entry next = current.Next; // 先保存下一个节点
                int index = GetBucketIndex(current.Hash, buckets.Length); // 用新容量重新计算桶下标
                current.Next = buckets[index]; // 当前节点头插到新桶
                buckets[index] = current; // 更新新桶头节点
                count++; // 元素数量加一
                current = next; // 继续处理旧链表下一个节点
            } // while 结束
        } // for 结束
    } // Resize 结束

    private int GetHash(TKey key) // 计算非负 hash
    { // GetHash 开始
        if (key == null) // 这里简单禁止 null key
        { // if 开始
            throw new ArgumentNullException(nameof(key)); // key 为空就抛异常
        } // if 结束

        return comparer.GetHashCode(key) & 0x7fffffff; // 去掉符号位,保证 hash 非负
    } // GetHash 结束

    private int GetBucketIndex(int hash, int bucketLength) // 根据 hash 计算桶下标
    { // GetBucketIndex 开始
        return hash % bucketLength; // 对桶数组长度取模
    } // GetBucketIndex 结束
} // 哈希表类结束

复杂度 平均查找、插入、删除都是 O(1);如果冲突非常严重,最坏会退化成 O(n)。空间复杂度是 O(n)

面试补一句 真实的 C# Dictionary<TKey, TValue> 也核心依赖 hashCode + bucket + entry,但实现更工程化,比如用数组下标串链、处理空槽复用、版本号、防止遍历时修改等。

写一个 A* 伪代码

csharp-astar-pseudocode

标准答案 A* 的核心是:每次优先选择 f = g + h 最小的节点继续搜索。

g 是从起点走到当前节点的真实代价,h 是从当前节点到终点的预估代价,f 是综合评分。找到终点后,通过每个节点保存的 parent 指针反向回溯出路径。

A* 伪代码,C# 风格

c
List<Node> AStar(Node start, Node end, Grid grid) // 从起点 start 搜索到终点 end
{ // 方法开始
    PriorityQueue<Node> openSet = new PriorityQueue<Node>(); // OpenSet 保存待搜索节点,并按 f 最小优先
    HashSet<Node> closedSet = new HashSet<Node>(); // ClosedSet 保存已经处理过的节点,避免重复搜索

    start.G = 0; // 起点到自己的真实代价是 0
    start.H = Heuristic(start, end); // 计算起点到终点的预估代价
    start.F = start.G + start.H; // 起点的综合评分等于 g + h
    start.Parent = null; // 起点没有父节点

    openSet.Push(start, start.F); // 把起点放入 OpenSet

    while (openSet.Count > 0) // 只要还有候选节点就继续搜索
    { // 循环开始
        Node current = openSet.PopMin(); // 取出 f 最小的节点作为当前节点

        if (current == end) // 如果当前节点就是终点
        { // 判断开始
            return BuildPath(end); // 沿 parent 反向回溯路径并返回
        } // 判断结束

        closedSet.Add(current); // 当前节点处理完,加入 ClosedSet

        foreach (Node neighbor in grid.GetNeighbors(current)) // 遍历当前节点的所有邻居
        { // 遍历开始
            if (neighbor.IsWall) // 如果邻居是墙或者障碍物
            { // 判断开始
                continue; // 跳过这个邻居
            } // 判断结束

            if (closedSet.Contains(neighbor)) // 如果邻居已经处理过
            { // 判断开始
                continue; // 跳过这个邻居
            } // 判断结束

            int newG = current.G + MoveCost(current, neighbor); // 计算从当前节点走到邻居的新 g 值

            if (!openSet.Contains(neighbor) || newG < neighbor.G) // 如果邻居没进过 OpenSet,或者找到了更短路径
            { // 判断开始
                neighbor.G = newG; // 更新邻居的真实路径代价
                neighbor.H = Heuristic(neighbor, end); // 更新邻居到终点的预估代价
                neighbor.F = neighbor.G + neighbor.H; // 更新邻居的综合评分
                neighbor.Parent = current; // 记录父节点,方便最后回溯路径

                if (!openSet.Contains(neighbor)) // 如果邻居还不在 OpenSet 中
                { // 判断开始
                    openSet.Push(neighbor, neighbor.F); // 加入 OpenSet 等待后续搜索
                } // 判断结束
                else // 如果邻居已经在 OpenSet 中
                { // 否则开始
                    openSet.UpdatePriority(neighbor, neighbor.F); // 更新它在优先队列里的优先级
                } // 否则结束
            } // 判断结束
        } // 遍历结束
    } // 循环结束

    return null; // OpenSet 空了还没找到终点,说明没有路径
} // 方法结束

int Heuristic(Node a, Node b) // 计算启发函数 h
{ // 方法开始
    return Abs(a.X - b.X) + Abs(a.Y - b.Y); // 四方向网格常用曼哈顿距离
} // 方法结束

int MoveCost(Node a, Node b) // 计算两个相邻节点之间的移动代价
{ // 方法开始
    return 1; // 普通四方向网格每走一格代价为 1
} // 方法结束

List<Node> BuildPath(Node end) // 根据终点反向构建路径
{ // 方法开始
    List<Node> path = new List<Node>(); // 创建路径列表
    Node current = end; // 从终点开始回溯

    while (current != null) // 只要当前节点不为空
    { // 循环开始
        path.Add(current); // 把当前节点加入路径
        current = current.Parent; // 移动到父节点
    } // 循环结束

    path.Reverse(); // 因为是从终点回溯到起点,所以需要反转
    return path; // 返回从起点到终点的路径
} // 方法结束

面试里这样说更稳 A* 比 BFS 多了一个启发函数 h,所以它不是盲目一圈圈扩散,而是优先往“看起来更接近终点”的方向走。h = 0 时,A* 会退化成 Dijkstra;如果 h 估得过大,可能更快,但不一定保证最短路径。

复杂度 如果 OpenSet 用堆或优先队列维护,常见复杂度是 O(E log V);空间复杂度是 O(V)。在 Unity 里,地图很大时不要一次性全图寻路,可以分帧、异步、分块,或者用寻路缓存减少重复计算。

文章评价

读完这篇,留下你的看法

暂无审核通过的评价。

登录账号后才能评价。

本站访客数0总站访问量0本页访问量0