数字华夏 软件开发-C++ 一面

1. 单例模式有哪些注意事项

答案:单例模式最核心的目标是“全局唯一实例 + 全局访问点”,但真正落到工程里,注意事项比写法本身更重要。第一是线程安全。如果是懒汉式单例,在多线程环境下初始化过程必须保证只有一次。C++11 之后函数内局部静态变量的初始化已经有线程安全保证,所以这是最常见的写法。第二是拷贝和赋值要禁掉,否则“单例”就可能被复制出多个对象。第三是析构时机要谨慎。单例对象通常会在进程退出阶段析构,如果它依赖其他静态对象,可能踩到静态析构顺序问题。第四是不要滥用。单例很容易变成“全局变量的升级版”,会让模块耦合增强、测试困难、依赖关系隐藏。第五是如果单例里维护线程、文件句柄、连接池这类资源,要考虑程序退出时的资源清理和并发退出顺序。

代码:

#include <iostream>
using namespace std;

class Singleton {
public:
    static Singleton& instance() {
        static Singleton obj;
        return obj;
    }

    void work() {
        cout << "singleton work\n";
    }

private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

int main() {
    Singleton::instance().work();
    return 0;
}

2. 简单说一下线程池的原理

答案:线程池本质上是“预先创建一批工作线程 + 一个任务队列 + 一套调度机制”。任务提交进来之后,不是每次都新建线程,而是把任务放入队列,空闲线程从队列里取任务执行。这样可以减少线程频繁创建销毁的开销,也更容易做统一的并发控制。线程池的核心组成一般包括:工作线程集合、任务队列、互斥锁、条件变量、停止标记、拒绝策略以及线程数量管理。如果继续往深一点问,线程池还要考虑核心线程数怎么定、队列是有界还是无界、任务执行时间过长时如何隔离、线程池关闭时是否等待任务完成、任务是否支持返回值。在服务端开发里,线程池通常和网络模型、定时器、任务优先级、背压机制一起考虑,单独一个线程池只是基础设施的一部分。

代码:

#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
using namespace std;

class ThreadPool {
public:
    ThreadPool(size_t n) {
        for (size_t i = 0; i < n; ++i) {
            workers_.emplace_back([this] {
                while (true) {
                    function<void()> task;
                    {
                        unique_lock<mutex> lock(mtx_);
                        cv_.wait(lock, [&] { return stop_ || !tasks_.empty(); });
                        if (stop_ && tasks_.empty()) return;
                        task = move(tasks_.front());
                        tasks_.pop();
                    }
                    task();
                }
            });
        }
    }

    void submit(function<void()> task) {
        {
            lock_guard<mutex> lock(mtx_);
            tasks_.push(move(task));
        }
        cv_.notify_one();
    }

    ~ThreadPool() {
        {
            lock_guard<mutex> lock(mtx_);
            stop_ = true;
        }
        cv_.notify_all();
        for (auto& t : workers_) t.join();
    }

private:
    vector<thread> workers_;
    queue<function<void()>> tasks_;
    mutex mtx_;
    condition_variable cv_;
    bool stop_ = false;
};

int main() {
    ThreadPool pool(2);
    pool.submit([] { cout << "task1\n"; });
    pool.submit([] { cout << "task2\n"; });
    return 0;
}

3. 在你做的高性能日志聚合系统里,进程间通信使用的是什么模型

答案:我这边换成一个更贴近高并发场景的项目来讲:做过一个高性能日志聚合系统,主要负责多进程日志采集、解析、落盘和实时转发。这个系统里本机多进程之间的高频数据交换我会优先用共享内存 + 事件通知的模型。共享内存负责承载真正的数据,事件通知可以用 eventfd、管道、信号量或者条件变量这一类机制做“有数据可读”的通知。如果是跨机器通信,就走 TCP 长连接或者 RPC,不会直接拿共享内存去做分布式。这种设计的原因很简单:共享内存避免了内核态和用户态之间的重复拷贝,吞吐会比较高;但共享内存本身只解决“大家看到同一块数据”,并不解决同步和唤醒问题,所以要再叠加一个通知机制。如果面试官继续追问,我一般会补充:单机高吞吐部分偏共享内存环形队列,跨机部分偏 Reactor + 线程池。

4. 共享内存的使用流程是什么样子的

答案:共享内存的完整流程一般是:先创建或打开共享内存对象,然后设置大小,再把这块共享内存映射到进程地址空间,之后多个进程通过映射地址直接读写,最后在退出时解除映射并关闭资源。如果是 System V 共享内存,常见流程是 shmget 创建、shmat 挂接、读写数据、shmdt 分离、shmctl 删除。如果是 POSIX 共享内存,通常是 shm_openftruncatemmap、读写、munmapcloseshm_unlink。真正工程里不能只停在接口流程,还要考虑权限、生命周期、是否需要持久存在、进程异常退出后的清理,以及同步保护。

代码:

#include <iostream>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
using namespace std;

int main() {
    const char* name = "/demo_shm";
    int fd = shm_open(name, O_CREAT | O_RDWR, 0666);
    ftruncate(fd, 4096);

    void* ptr = mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    strcpy((char*)ptr, "hello shared memory");
    cout << (char*)ptr << endl;

    munmap(ptr, 4096);
    close(fd);
    shm_unlink(name);
    return 0;
}

5. 共享内存有哪些接口

答案:Linux 下主要分两套。一套是 System V 共享内存shmgetshmatshmdtshmctl。另一套是 POSIX 共享内存shm_openftruncatemmapmunmapcloseshm_unlink。从现代工程使用习惯来看,POSIX 这一套通常更直观,能更自然地和文件描述符、mmap 统一起来。另外如果面试官把问题扩大一点,也可以顺带提匿名共享映射,比如父子进程通过 mmap(MAP_SHARED | MAP_ANONYMOUS) 共享内存。但不管哪种接口,共享内存只负责“共享数据”,同步依然要靠锁、信号量、原子变量之类的机制补上。

6. 共享内存线程安全吗,怎么保护

答案:共享内存本身不提供线程安全,也不提供进程安全。它只是把同一块物理内存映射给多个执行单元看到。如果多个线程或多个进程同时读写同一块共享区域,不做同步保护就会出现竞态条件、脏读、写覆盖、读到中间状态等问题。怎么保护取决于场景。如果是简单互斥访问,可以用进程共享的互斥锁或信号量;如果是单生产者单消费者模型,可以用环形缓冲区 + 原子变量;如果是多生产者多消费者,就要更谨慎地设计锁粒度、缓存行对齐和唤醒机制。工程里比较常见的是“共享内存存数据 + 信号量/互斥锁做同步 + 状态位或序号避免读脏数据”。

7. 操作系统怎么申请内存

答案:从应用程序视角看,像 mallocnew 这种申请最终并不是每次都直接找操作系统要一小块内存。通常是先进入运行时内存分配器,比如 glibc 的 malloc,它会维护自己的内存池、小块分配链表、空闲块管理结构。只有当现有堆空间不够时,才会向内核申请更大的内存区域。内核层面常见的方式有 brk/sbrk 扩展堆顶,以及 mmap 映射新的匿名内存区域。小块内存很多时候来自堆区,大块内存常常直接走 mmap。所以“程序申请内存”实际是应用分配器和操作系统一起完成的,两层都要知道。

8. 操作系统怎么管理内存

答案:现代操作系统主要靠虚拟内存机制管理内存。每个进程看到的是自己的虚拟地址空间,真正访问时再通过页表映射到物理内存。这样做的好处是地址空间隔离、便于权限控制、支持按需分配、支持页换入换出。内存管理里常见的关键概念包括页、页表、TLB、缺页异常、匿名页、文件映射页、伙伴系统、slab 分配器。从内核角度,还要负责物理页帧分配、页回收、内存换页、缓存管理、NUMA 策略等。如果面试官追得深一点,往往会继续问缺页异常、用户态到内核态怎么切换、堆和栈区别、mmapmalloc 的关系。

9. 发生一个中断,整个处理流程是什么样子的

答案:中断到来后,CPU 会先根据中断机制暂停当前正在执行的指令流,保存必要现场,然后切换到内核态,根据中断向量表找到对应的中断处理入口。进入内核之后,通常会先执行比较短的上半部处理,完成最紧急、最不能延迟的工作,比如读取设备状态、确认中断来源、快速

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++ 常考面试题总结 文章被收录于专栏

本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务