51mee - AI智能招聘平台Logo
模拟面试题目大全招聘中心会员专区

解释std::shared_ptr的引用计数机制,并举例说明其可能导致循环引用的问题。设计一个自定义内存池(如对象池),用于高频分配/释放的缓冲区(如视频帧数据),说明其实现思路(预分配、链表管理)和适用场景(避免频繁malloc/free的性能开销)。

快手C++开发工程师 📦 工程类难度:中等

答案

1) 【一句话结论】
std::shared_ptr通过引用计数管理对象生命周期,循环引用会导致内存泄漏;自定义内存池通过预分配大块内存并管理空闲链表,减少频繁malloc/free的性能开销,适用于视频帧等高频临时对象。

2) 【原理/概念讲解】
std::shared_ptr的引用计数机制是核心:创建时,对象计数器初始为1。

  • 拷贝构造:新shared_ptr的计数加1(表示新增一个引用);
  • 赋值操作:原shared_ptr的计数先减1(若减为0则调用析构函数释放对象),再将新shared_ptr的计数加1;
  • 销毁时:计数减1,若减为0则调用析构函数。
    循环引用是指两个或多个shared_ptr互相持有,导致每次计数减1后,另一个的计数仍为正,因此计数永远不会为0,对象无法被释放,造成内存泄漏。解决循环引用需用weak_ptr,当shared_ptr的引用计数为0时,weak_ptr可转换为shared_ptr,打破循环。类比:两个人互相请客,用weak_ptr相当于留个“临时邀请”,若对方不买单可取消,避免钱永远在账上。

3) 【对比与适用场景】

特性/场景std::shared_ptr自定义内存池
核心机制引用计数(管理生命周期)直接管理内存块(预分配+链表)
循环引用会导致内存泄漏无引用计数,不会因循环引用泄漏
内存分配默认new/delete(或自定义分配器)预分配大块内存(如mmap),切分固定大小缓冲区
适用场景对象需多部分共享,所有权转移(如资源管理)高频分配/释放的临时缓冲区(如视频帧、网络包)
注意点循环引用需用weak_ptr预分配大小需工程计算(如帧率、缓冲区大小、并发数),多线程需加锁(可优化锁粒度)

4) 【示例】
循环引用及解决:

class Node {
public:
    std::weak_ptr<Node> other; // 持有weak_ptr
    ~Node() { std::cout << "Node destroyed\n"; }
};

int main() {
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    a->other = b;
    b->other = a;
    if (a->other.lock()) { // 转换为shared_ptr
        std::cout << "other still alive\n";
    }
}

内存池伪代码(考虑预分配计算与动态调整):

class VideoFramePool {
private:
    size_t buffer_size;
    size_t pool_size;
    std::vector<char*> buffers;
    std::list<char*> free_list;
    std::mutex mtx;

public:
    VideoFramePool(size_t size, size_t fps, size_t concurrency) {
        buffer_size = size;
        pool_size = fps * concurrency * 2; // 预估最大并发数
        buffers.resize(pool_size);
        for (size_t i = 0; i < pool_size; ++i) {
            buffers[i] = new char[buffer_size];
            free_list.push_front(buffers[i]);
        }
    }

    char* allocate() {
        std::lock_guard<std::mutex> lock(mtx);
        if (free_list.empty()) return nullptr;
        char* buf = free_list.front();
        free_list.pop_front();
        return buf;
    }

    void release(char* buf) {
        std::lock_guard<std::mutex> lock(mtx);
        free_list.push_front(buf);
    }

    ~VideoFramePool() {
        for (char* b : buffers) delete[] b;
    }
};

5) 【面试口播版答案】
(约90秒)
“std::shared_ptr通过引用计数管理对象生命周期,当引用数减为0时释放。循环引用时,多个shared_ptr互相持有,计数永远不会归零,导致内存泄漏。解决循环引用可以用weak_ptr,比如一个shared_ptr持有另一个的weak_ptr,当需要访问时再转换。对于视频帧等高频分配释放的缓冲区,可以设计内存池:预分配大块内存,切分为固定大小的缓冲区,用链表管理空闲状态。分配时从空闲链表取,释放时放回。预分配大小根据帧率(如30fps)、缓冲区大小(如1KB)和并发数(如10个线程)计算,比如30102≈600个缓冲区,减少malloc/free的性能开销。多线程环境下用互斥锁保护链表操作,避免数据竞争。”

6) 【追问清单】

  • 如何解决循环引用?
    回答:使用std::weak_ptr,当shared_ptr的引用计数为0时,weak_ptr可转换为shared_ptr,打破循环。例如,循环引用中用weak_ptr代替直接shared_ptr。
  • 内存池的预分配大小如何计算?
    回答:根据场景预估最大并发数,公式为:pool_size = fps * n * k(k为缓冲区轮换系数,如2),避免预估不足或过大。
  • 多线程环境下内存池是否需要加锁?
    回答:需要加锁(如std::mutex),保护链表操作,避免数据竞争。可优化锁粒度(如分段锁或无锁结构)。
  • 自定义内存池和std::vector的区别?
    回答:vector是连续内存,适合顺序访问;内存池是离散内存,适合高频分配释放,减少内存碎片,适用于临时缓冲区。
  • 引用计数机制中赋值操作的具体步骤?
    回答:赋值时原计数减1(若为0则析构),新计数加1,确保计数正确更新。

7) 【常见坑/雷区】

  • 忽略循环引用的解决方法,只描述问题而不提weak_ptr的使用,会被认为知识不全面。
  • 内存池实现时未考虑线程安全,导致多线程竞争时出现数据不一致或内存泄漏。
  • 预分配大小计算错误,比如预估不足导致缓冲区不足,或预估过大导致内存浪费。
  • 与std::vector的区别分析不充分,未结合性能数据,显得对比不够深入。
  • 引用计数机制解释中遗漏赋值操作的具体步骤(如原计数减1后检查是否为0),导致机制描述不完整。
51mee.com致力于为招聘者提供最新、最全的招聘信息。AI智能解析岗位要求,聚合全网优质机会。
产品招聘中心面经会员专区简历解析Resume API
联系我们南京浅度求索科技有限公司admin@51mee.com
联系客服
51mee客服微信二维码 - 扫码添加客服获取帮助
© 2025 南京浅度求索科技有限公司. All rights reserved.
公安备案图标苏公网安备32010602012192号苏ICP备2025178433号-1