面试真题 | 字节推荐架构C++

以下是整理后的C++面试问题列表,按照面试流程和时间顺序排列:

一面

开局自我介绍

1. 教育背景

  • 询问本科背景。
  • 询问研究生研究方向。

2. 简历项目

  • 请介绍简历中的项目经历。

3. 智能指针

  • 解释智能指针的概念和作用。

智能指针的概念与作用

1. 概念

智能指针是 C++ 中一种封装原生指针的类模板,通过 RAII(资源获取即初始化) 机制,实现对动态分配内存的自动管理。其核心设计思想是:
对象生命周期绑定:智能指针对象的析构函数会自动释放所管理的内存,无需手动 delete
所有权语义:通过类型系统(如独占或共享)明确资源的所有权归属,避免内存泄漏或非法访问。

常见智能指针类型包括:
std::unique_ptr:独占所有权,同一时间仅允许一个指针管理资源。
std::shared_ptr:共享所有权,通过引用计数管理资源生命周期。
std::weak_ptr:弱引用,配合 shared_ptr 解决循环引用问题,不增加引用计数。

2. 核心作用

(1) 自动内存管理,避免内存泄漏
资源自动释放:当智能指针对象超出作用域时,析构函数自动调用 delete 或自定义删除器释放资源。
异常安全性:即使程序因异常中断,智能指针仍能确保资源释放,避免传统指针因代码分支遗漏释放的问题。

(2) 防止悬空指针(Dangling Pointer)
自动置空机制:部分智能指针(如 shared_ptr)在引用计数归零后,自动将内部指针置为 nullptr,避免访问已释放的内存区域。

(3) 管理共享资源的所有权
共享所有权(shared_ptr:通过引用计数机制,允许多个指针共享同一资源,计数归零时自动释放。
独占所有权(unique_ptr:确保资源仅被单一所有者控制,适用于文件句柄等需严格独占的场景。

(4) 解决循环引用问题
弱引用(weak_ptr:打破 shared_ptr 的循环依赖(如两个对象互相持有对方的 shared_ptr),防止引用计数永不为零导致的内存泄漏。

class B;  
class A { 
public:
    std::shared_ptr<B> b_ptr;  
};  
class B { 
public:
    std::weak_ptr<A> a_ptr;  // 使用 weak_ptr 替代 shared_ptr
};

(5) 简化代码并提升可维护性
隐藏手动管理细节:减少 new/delete 的显式调用,降低代码冗余和出错概率。
明确资源所有权:通过类型名称(如 unique_ptr)直观表达设计意图,增强代码可读性。

3. 技术实现与扩展

引用计数shared_ptr 内部维护引用计数器,拷贝时递增,析构时递减,归零时释放资源。
移动语义unique_ptr 支持移动构造/赋值,实现资源所有权的安全转移。
自定义删除器:允许指定资源释放方式(如 fclose 关闭文件句柄),适配非 new 分配的资源。

总结

智能指针通过封装原生指针并引入所有权模型,解决了 C++ 手动内存管理的核心痛点(泄漏、悬空指针、所有权混乱)。合理选择 unique_ptrshared_ptrweak_ptr,可显著提升代码的安全性可维护性

4. 手写shared_ptr

  • 要求手写一个shared_ptr的简单实现。

手写简化版 shared_ptr 实现

以下是基于 C++11 标准的 shared_ptr 核心实现代码,聚焦引用计数机制和资源管理逻辑,省略线程安全等复杂扩展功能。实现包含构造函数、拷贝控制、运算符重载和基础接口,完整支持共享所有权语义。

1. 核心组件与类定义

#include <iostream>
#include <utility> // for std::swap

// 引用计数管理类
class RefCount {
private:
    int count = 1; // 初始引用计数为1
public:
    void increment() noexcept { ++count; }
    int decrement() noexcept { return --count; }
    int get() const noexcept { return count; }
};

// shared_ptr 类模板
template <typename T>
class SharedPtr {
private:
    T* ptr = nullptr;       // 托管对象的原始指针
    RefCount* ref = nullptr; // 引用计数对象指针

public:
    // 默认构造函数(托管空指针)
    SharedPtr() noexcept = default;

    // 显式构造函数(接管原始指针)
    explicit SharedPtr(T* raw_ptr) noexcept : ptr(raw_ptr), ref(new RefCount()) {}

    // 拷贝构造函数(共享所有权)
    SharedPtr(const SharedPtr& other) noexcept : ptr(other.ptr), ref(other.ref) {
        if (ref) ref->increment();
    }

    // 移动构造函数(转移所有权)
    SharedPtr(SharedPtr&& other) noexcept : ptr(other.ptr), ref(other.ref) {
        other.ptr = nullptr;
        other.ref = nullptr;
    }

    // 析构函数(释放资源)
    ~SharedPtr() noexcept {
        if (ref && ref->decrement() == 0) {
            delete ptr;
            delete ref;
        }
    }

    // 拷贝赋值运算符
    SharedPtr& operator=(const SharedPtr& rhs) noexcept {
        if (this != &rhs) {
            // 释放当前资源
            this->~SharedPtr();
            // 接管新资源
            ptr = rhs.ptr;
            ref = rhs.ref;
            if (ref) ref->increment();
        }
        return *this;
    }

    // 移动赋值运算符
    SharedPtr& operator=(SharedPtr&& rhs) noexcept {
        if (this != &rhs) {
            // 释放当前资源
            this->~SharedPtr();
            // 转移新资源
            ptr = rhs.ptr;
            ref = rhs.ref;
            rhs.ptr = nullptr;
            rhs.ref = nullptr;
        }
        return *this;
    }

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

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

    // 获取原始指针
    T* get() const noexcept { return ptr; }

    // 获取引用计数
    int use_count() const noexcept { return ref ? ref->get() : 0; }

    // 重置指针(释放当前资源)
    void reset(T* raw_ptr = nullptr) noexcept {
        SharedPtr temp(raw_ptr);
        swap(temp);
    }

    // 交换两个 SharedPtr 的内容
    void swap(SharedPtr& other) noexcept {
        std::swap(ptr, other.ptr);
        std::swap(ref, other.ref);
    }
};

2. 核心机制解析

  1. 引用计数管理
    • 每个 SharedPtr 对象共享一个 RefCount 实例。 • 构造函数初始化 RefCount 对象,拷贝构造时递增计数,析构时递减计数。当计数归零时,释放托管对象和 RefCount 对象。

  2. 资源所有权转移
    • 移动构造函数和移动赋值运算符直接接管资源,原对象置空,避免引用计数错误更新。

  3. 运算符重载
    operator*operator-> 提供与原生指针一致的访问语义。 • reset() 方法通过临时对象交换实现资源释放和接管,避免中间状态风险。

3. 测试用例

class TestObject {
public:
    TestObject() { std::cout << "TestObject constructed\n"; }
    ~TestObject() { std::cout << "TestObject destroyed\n"; }
};

int main() {
    // 场景1:基本生命周期管理
    {
        SharedPtr<TestObject> p1(new TestObject); // 构造
        SharedPtr<TestObject> p2 = p1;            // 拷贝构造(计数=2)
        std::cout << "Use count: " << p2.use_count() << "\n"; // 输出2
    } // p1和p2析构,计数归零,对象销毁

    // 场景2:移动语义
    SharedPtr<TestObject> p3(new TestObject);
    SharedPtr<TestObject> p4 = std::move(p3); // 移动构造,p3置空
    std::cout << "p3 is null? " << (p3.get() == nullptr) << "\n"; // 输出1

    // 场景3:reset 方法
    p4.reset(new TestObject); // 原对象销毁,托管新对象
    return 0;
}

输出结果

TestObject constructed
Use count: 2
TestObject destroyed
TestObject constructed
p3 is null? 1
TestObject destroyed
TestObject constructed
TestObject destroyed

4. 扩展优化方向

  1. 线程安全
    RefCount 中使用原子操作(如 std::atomic<int>)实现线程安全的引用计数更新。

  2. 自定义删除器
    添加模板参数 Deleter,允许通过 std::function 或函数指针自定义资源释放逻辑。

  3. 循环引用处理
    引入 WeakPtr 类,通过弱引用计数避免 SharedPtr 循环引用导致的内存泄漏。

总结

此实现完整覆盖了 shared_ptr 的核心功能,通过引用计数机制实现了动态资源的共享所有权管理。重点需关注拷贝/移动语义的正确性及引用计数的线程安全扩展。实际应用中建议直接使用标准库 std::shared_ptr,但其底层原理与此简化版高度一致。

5. TCP与UDP的区别

  • 描述TCP(传输控制协议)和UDP(用户数据报协议)的主要区别。

TCP与UDP的核心区别

1. 连接机制

TCP(传输控制协议)面向连接,需通过“三次握手”建立稳定连接(类似打电话前拨号),确保通信双方准备好数据传输。传输结束后通过“四次挥手”断开连接。
UDP(用户数据报协议)无连接,直接发送数据包(类似发短信),无需预先建立通道,也无需断开连接。

2. 可靠性

TCP可靠传输,通过确认应答(ACK)、超时重传、数据排序、流量控制、拥塞控制等机制,保证数据无差错、不丢失、按序到达。
UDP不可靠传输,无确认、重传或排序机制,数据可能丢失、重复或乱序(如视频丢帧或语音断续)。

3. 传输效率与速度

TCP效率较低,因需维护连接状态、处理重传和流量控制,额外开销大(首部20~60字节)。适用于对延迟不敏感的场景(如文件传输、邮件)。
UDP效率高,无连接和复杂控制机制,首部仅8字节,传输速度快、延迟低。适合实时性要求高的场景(如直播、在线游戏、语音通话)。

4. 流量控制与拥塞控制

TCP:内置动态调整机制,根据网络状况(如带宽、丢包率)调节发送速率,避免网络过载。例如,慢启动、拥塞避免算法。
UDP:无流量或拥塞控制,以恒定速率发送数据,可能加剧网络拥堵。

5. 数据结构和应用场景

TCP面向字节流,数据按顺序分割为字节流传输,保证完整性。典型应用:
• 高可靠性需求:网页浏览(HTTP/HTTPS)、文件传输(FTP)、数据库通信。
UDP面向数据报,每个数据包独立处理,支持广播和多播。典型应用:
• 实时性优先:视频会议(Zoom)、在线游戏(王者荣耀)、DNS查询。
• 容忍部分数据丢失:物联网传感器数据上报。

总结对比表

特性TCPUDP
连接方式 面向连接(三次握手) 无连接
可靠性 高(确认、重传、排序) 低(无保障)
传输效率 慢(额外控制开销) 快(无控制机制)
流量控制 有(动态调整速率)
数据顺序 保证按序到达 可能乱序
首部开销 20~60字节 8字节
适用场景 文件传输、邮件、网页 直播、游戏、实时通信

实际案例说明

视频网站(如B站):视频流用UDP传输(容忍丢帧),但暂停、快进等操作切换为TCP以确保请求准确。
银行转账:必须使用TCP保证交易数据完整,而语音通话(如微信)用UDP降低延迟。
QQ消息:默认UDP发送,若网络差导致丢包,可能切换TCP重传重要信息。

6. 堆和栈的区别

  • 解释堆(heap)和栈(stack)在内存管理上的区别。

堆(Heap)和栈(Stack)是内存管理中两种核心机制,它们在分配方式、生命周期、性能特征和应用场景上有本质区别。以下从技术实现和实际应用角度深入解析两者的差异:

一、内存分配机制

  1. 分配方式
    :由编译器自动分配和释放,遵循后进先出(LIFO)原则。例如函数中的局部变量int a会由编译器在栈上分配空间,函数结束后自动回收。
    :需程序员手动申请(如malloc/new)和释放(如free/delete)。堆内存的生命周期由程序员控制,若未释放则可能导致内存泄漏。

  2. 内存结构
    :连续的内存区域,通过调整栈顶指针(如x86的ESP寄存器)实现快速分配。在Windows中默认大小约1-2MB,超出会导致栈溢出(Stack Overflow)。
    :非连续内存,通过链表管理空闲内存块。分配时需要遍历链表寻找合适空间,可能产生内存碎片,但可动态扩容(受限于系统虚拟内存)。

二、性能与访问效率

  1. 分配速度
    :分配仅需移动栈指针,速度极快(纳秒级)。例如函数调用时参数入栈仅需一条CPU指令。
    :需通过系统调用(如brkmmap)动态分配,速度较慢(微秒级)。频繁分配可能触发垃圾回收(GC)或内存整理。

  2. 数据访问
    :直接通过变量名访问,无需指针解引用。例如char s1[] = "abc"的数组元素直接存储在栈中,读取时无需二次寻址。
    :需通过指针间接访问。例如char* s2 = "abc"的指针变量在栈中,实际数据在堆中,访问需两次内存操作。

三、生命周期与作用域

  1. 作用域限制
    :变量仅在作用域内有效。例如函数内的局部变量无法被外部访问,函数返回后栈帧销毁。
    :数据可跨作用域存在。例如通过new创建的全局对象,需显式释放才能回收。

  2. 内存泄漏风险
    :无泄漏风险,由编译器自动管理。
    :若未释放则持续占用内存。例如未配对的malloc/free会导致程序运行时内存逐渐耗尽。

四、应用场景对比

特性
适用场景 局部变量、函数调用上下文 动态数据结构(如链表、树)、大型数据
容量 较小(默认MB级) 较大(受系统虚拟内存限制)
线程安全 线程私有,无需同步 全局共享,需锁机制保护
碎片问题 频繁分配释放易产生碎片

五、典型案例分析

  1. 栈溢出

    void recursive() {
        char buffer[1024];  // 每次递归消耗1KB栈空间
        recursive();         // 递归过深导致栈溢出
    }
    

    递归调用未设终止条件,栈空间迅速耗尽。

  2. 堆内存泄漏

    void create_leak() {
        int* ptr = new int[100];  // 分配后未释放
    }  // 函数结束,ptr被销毁,但堆内存仍存在
    

    未调用delete[]导致100个int内存永久泄漏。

六、现代技术演进

智能指针:C++的std::unique_ptr和Qt的QScopedPointer通过RAII(资源获取即初始化)自动管理堆内存,减少手动释放的负担。
内存池:通过预分配大块堆内存并自行管理,减少碎片和分配开销,常见于高频交易系统。

堆和栈的区别本质是自动化与灵活性的权衡。理解两者差异,能帮助开发者在性能优化(如高频计算优先用栈)与资源管理(如跨作用域数据用堆)间做出合理选择。

7. HTTP协议版本

  • 询问最新的HTTP协议版本是基于什么。

HTTP协议的最新版本:HTTP/3

1. 基于QUIC协议构建

最新的HTTP协议版本是HTTP/3,其核心创新在于底层传输协议的改变。HTTP/3完全摒弃了传统的TCP协议,转而基于UDP协议和**QUIC(Quick UDP Internet Connections)**框架实现。QUIC由Google设计,旨在通过UDP的高效性解决TCP的固有缺陷(如队头阻塞、高延迟)。

2. QUIC协议的核心优势

基于UDP的高效传输:UDP无需建立复杂连接,减少了三次握手的开销,连接建立时间可缩短至0-RTT(首次连接1-RTT),显著降低延迟。 • 内置TLS加密:QUIC将TLS 1.3加密协议集成到传输层,所有通信默认加密,提升了安全性。 • 多路复用与独立流:每个HTTP请求/响应作为独立的数据流传输,彻底解决队头阻塞问题(TCP中一个数据包丢失会影响后续所有数据),提升并发性能。 • 连接迁移支持:设备在网络切换(如Wi-Fi转4G)时,QUIC通过连接ID保持会话连续性,无需重新握手。

3. HTTP/3的改进亮点

性能优化:相比HTTP/2的TCP多路复用,HTTP/3在丢包率高的网络环境下(如移动网络)表现更稳定,传输效率提升30%-50%。 • 头部压缩升级:采用QPACK算法替代HTTP/2的HPACK,支持乱序传输下的动态压缩,减少冗余数据。 • 兼容性增强:QUIC协议在用户空间实现,无需操作系统内核更新,更易部署。

4. 实际应用场景

实时通信:如视频会议、在线游戏,对低延迟和抗抖动要求极高。 • 移动端服务:适应网络频繁切换和高丢包环境,提升移动应用体验。 • 大规模内容分发:CDN服务商(如Cloudflare)已广泛支持HTTP/3,加速全球资源加载。

5. 与HTTP/2的对比

特性HTTP/2HTTP/3
底层协议 TCP UDP + QUIC
加密支持 可选(依赖HTTPS) 强制集成TLS 1.3
队头阻塞 存在(TCP层) 完全解决(独立流)
连接建立速度 1-RTT(需握手) 0-RTT(已缓存会话)

总结

HTTP/3通过QUIC协议重构了传输层,结合UDP的高效性和QUIC的可靠性,成为下一代Web通信的核心标准。其设计解决了TCP的固有瓶颈,并在安全性、性能、移动适配等方面实现突破。目前主流浏览器(如Chrome、Firefox)和服务器(如Nginx、Apache)已逐步支持HTTP/3,未来将推动更快速、稳定的互联网服务。

8. selectpoll的区别

  • 描述selectpoll系统调用在I/O多路复用上的区别。

selectpoll系统调用的核心区别

1. 文件描述符数量限制

select:使用固定大小的位掩码(fd_set),默认最大支持1024个文件描述符。虽然可通过修改FD_SETSIZE宏调整上限,但会导致内核与用户态数据结构不匹配(如Solaris系统存在兼容性问题)。 • poll:采用动态长度的struct pollfd数组,理论上无硬性限制,仅受系统资源(如内存)约束。例如在BSD/OS 2.1系统中,poll可支持数万个并发连接。

2. 数据结构和参数设计

select:通过三个独立的fd_set位掩码(读/写/异常)传递描述符集合,每次调用需重置所有参数。例如:

fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(fd1, &read_fds);
select(max_fd+1, &read_fds, NULL, NULL, NULL);

每次调用需遍历0到max_fd所有描述符,存在大量无效检查。

poll:使用统一的事件结构体数组,通过eventsrevents分离输入输出参数。例如:

struct pollfd fds[2];
fds[0].fd = fd1; 
fds[0].events = POLLIN;  // 仅需设置一次
poll(fds, 2, 5000);      // 无需重新初始化

这种设计减少冗余参数传递,避免重复初始化。

3. 性能与时间复杂度

时间复杂度:两者均为**O(n)**轮询,需遍历所有监控的描述符。但poll通过指定数组长度减少无效遍历(例如仅监控10个活跃描述符时,poll只需检查10项,而select需遍历1024位掩码)。 • 内存拷贝开销select每次调用需将完整位掩码从用户态拷贝至内核态,而poll仅传递数组指针和长度,减少数据拷贝量。

4. 超时处理与返回值

超时精度select使用timeval结构(微秒级精度),但实际受系统时钟分辨率限制(通常10ms);poll使用毫秒级超时参数,精度略低但更易用。 • 剩余超时select修改timeval参数返回剩余时间,需用户重新计算;poll不修改超时参数,简化重试逻辑。

5. 兼容性与扩展性

系统兼容性select在BSD和Windows系统广泛支持,移植性更好;早期poll仅在System V系统原生支持(如SVR3需额外处理fd=-1的特殊情况)。 • 事件类型poll支持更丰富的事件类型(如POLLRDHUP用于TCP对端关闭连接),而select仅支持读/写/异常三类基础事件。

性能对比实测

在相同并发量下(如1000个活跃连接),poll因避免无效遍历,吞吐量比select高约15-20%。但两者本质仍为线性轮询,与epoll的事件驱动模型(时间复杂度O(1))相比,性能差距在万级连接时呈指数级扩大。

设计演进关系

pollselect的改进版,解决了描述符限制和参数冗余问题,但未改变轮询本质。后续epoll通过事件回调机制(红黑树+就绪链表)彻底突破性能瓶颈,成为高并发场景首选。

9. select支持的最大数量

  • 询问select系统调用支持的最大文件描述符数量。

select系统调用支持的最大文件描述符数量

1. 默认限制:1024

select系统调用默认支持的最大文件描述符数量为1024。这是由于Linux内核中fd_set结构体的设计限制,其底层通过一个固定大小的位图(bitmap)表示文件描述符集合,每个位对应一个文件描述符的监视状态。
实现细节fd_set结构体由long int数组构成,数组大小为__FD_SETSIZE / (8 * sizeof(long)),而__FD_SETSIZE在标准库中被定义为1024。因此,位图最多只能覆盖0~1023的文件描述符。
历史原因:早期硬件资源有限,1024的设计基于当时的硬件条件和性能权衡,同时与进程默认打开的最大文件描述符数量(通常为1024)保持一致。

2. 限制的根源

位图数据结构select通过位图轮询文件描述符的状态,每个文件描述符占用一个比特位。例如,文件描述符4对应位图的第4位。这种设计导致扩展性受限。
内核宏定义硬编码__FD_SETSIZE的值为1024,直接限制了位图容量。即使系统支持更大的文件描述符数量(如修改ulimit),select也无法突破这一限制。

3. 突破限制的尝试与局限性

虽然理论上可以通过修改__FD_SETSIZE宏并重新编译内核来扩展select的支持上限,但实际应用中存在以下问题:

  1. 兼容性风险:修改宏定义可能导致标准库函数(如FD_SETFD_ISSET)的未定义行为,甚至内存越界。
  2. 性能瓶颈select采用轮询机制,时间复杂度为O(n)。即使支持更多文件描述符,遍历大量描述符的开销会导致性能急剧下降。
  3. 替代方案更优:对于高并发场景,推荐使用epoll(Linux特有)或poll(无数量限制),两者均避免了select的性能和扩展性缺陷。

4. 设计缺陷与优化建议

缺陷
覆盖式位图:每次调用select后,内核会修改传入的fd_set位图,导致用户需在循环中重复初始化。
超时机制异常:返回的剩余超时时间会覆盖原值,需每次调用前重置超时参数。
优化方向
• 使用poll替代:支持无限制文件描述符,且无需重新初始化数据结构。
• 结合多线程:将连接分片到多个s实例中处理,但需额外管理复杂性。

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

C/C++面试必考必会 文章被收录于专栏

【C/C++面试必考必会】专栏,直击面试核心,精选C/C++及相关技术栈中面试官最爱的必考点!从基础语法到高级特性,从内存管理到多线程编程,再到算法与数据结构深度剖析,一网打尽。助你快速构建知识体系,轻松应对技术挑战。希望专栏能让你在面试中脱颖而出,成为技术岗的抢手人才。

全部评论
佬,项目是做的什么
点赞 回复 分享
发布于 今天 08:58 北京

相关推荐

03-21 19:40
已编辑
华中科技大学
2.26暑期第四面,淘天某部门研发工程师C/C++,提前约好了时间并告知是电话面,下午快六点面完后晚上八点多就接到电话约了二面,一面面试官听声音感觉很年轻,面了大概40min1.&nbsp;自我介绍,然后开始逐个聊项目细节2.&nbsp;问websocket协议+消息格式的封装3.&nbsp;问第一个项目能支撑到多少的并发、单个事务的延迟是多少4.&nbsp;问不能支持更高并发的瓶颈是什么5.&nbsp;项目中访问的服务商API实测延迟是多少6.&nbsp;问第二个项目中的关键设计,简历上写到的一些功能是如何去实现的7.&nbsp;第三个项目涉及到grpc和pb,问技术选型的考量8.&nbsp;grpc作为一个类似二进制的RPC框架,底层是什么原理?比如说协议设计、封装格式等等方面9.&nbsp;接下来开始聊实习,细说实习内容,顺便问了一下了不了解当时用的一些仪器的原理,我没答上来,面试官说实习四个月都没有好奇底层原理想把它弄明白嘛(是的没错)10.&nbsp;对于k8s的了解大概是什么程度11.&nbsp;给了一道算法题,思考后说怎么做+时间空间复杂度:有n个点的有向无环图,邻接矩阵为d[i][j],数值范围是1到10e9,如果d[i][j]为0的话就说明i到j没有边,求满足点1到点n的路径长度为17的倍数这个约束条件的最短路径(不知道为什么电话面要做算法,听题目听了好久想了一会儿做出来了,用二维动态规划,不过时间复杂度好像答错了,面试官说回去再想一想)12.&nbsp;反问环节:问了一下业务和base这次面试还是比较简单的,感谢善良的面试官小哥哥虽然二面后还在泡着#淘天##面经##C++##牛客AI配图神器#
查看12道真题和解析
点赞 评论 收藏
分享
评论
9
26
分享

创作者周榜

更多
牛客网
牛客企业服务