腾讯的最新C++面试面经,问的都是一些高质量的问题,看看你能答上来几个:

1. 面试官:请谈谈 Proactor 和 Reactor 两种 I/O 模型的区别,以及 C++ 中的典型实现?

参考答案:

核心区别在于事件触发时机I/O 操作的执行主体,二者均用于高效处理多 I/O 任务,但流程差异显著:

维度

Reactor 模式

Proactor 模式

核心机制

基于同步 I/O,监听 “事件就绪”(如可读 / 可写)

基于异步 I/O,监听 “I/O 操作完成” 事件

处理流程

1. 主线程(Reactor)监听事件;2. 事件就绪后,通知工作线程执行 I/O 操作 + 业务逻辑

1. 主线程发起异步 I/O 请求;2. I/O 完成后,主线程通知工作线程仅处理业务逻辑

阻塞情况

I/O 操作阶段可能阻塞工作线程

发起 I/O 请求后主线程不阻塞,等待完成通知

C++ 典型实现

Linux 下用epoll、BSD 下用kqueue,封装库如muduo、libevent

Windows 下用IOCP,C++ asio库对两种模式均有封装

适用场景:Reactor 适合 I/O 密集型场景(如高并发网络服务器),Proactor 适合对延迟敏感、需减少线程阻塞的场景,但实现复杂度更高。

Linux教程

分享Linux、Unix、C/C++后端开发、面试题等技术知识讲解

2. 面试官:C++ 中栈内存和堆内存的管理差异有哪些?

参考答案:

栈和堆是 C++ 中两种核心内存区域,差异体现在分配、释放、性能等多个维度:

维度

栈内存(Stack)

堆内存(Heap)

分配主体

编译器自动分配(局部变量、函数参数)

程序员手动分配(new/malloc)

释放方式

作用域结束自动回收(无需手动操作)

需手动释放(delete/free),或通过智能指针自动回收

内存碎片

连续分配,无碎片(栈帧严格进出栈)

动态分配 / 释放易产生外部碎片(小块空闲内存无法利用)

访问速度

快(CPU 缓存友好,地址连续,直接通过栈指针寻址)

慢(需通过指针间接寻址,可能跨缓存页)

大小限制

固定大小(通常几 MB,编译时确定)

灵活(理论上可使用系统大部分空闲内存)

逃逸特性

无逃逸(仅在函数 / 作用域内可见)

可能逃逸(如new的对象传递到作用域外)

关键注意点:堆内存若未正确释放会导致内存泄漏,C++ 中推荐用

std::unique_ptr/std::shared_ptr等智能指针实现 RAII 管理,避免手动操作失误。

3. 面试官:C++ 中std::vector的底层存储结构是什么?它和 Go 的 Slice 有什么异同?

参考答案:

std::vector基于动态数组实现,核心包含三个成员(不同编译器实现细节可能略有差异,但逻辑一致):

template <typename T> class vector { private:     T* data;       // 指向底层连续内存的指针     size_t size;   // 当前已存储的元素个数     size_t capacity;// 底层数组的最大容量(可容纳元素数) };

扩容机制:当size == capacity时,会分配一块更大的内存(通常是原容量的 1.5 倍,GCC)或 2 倍(MSVC),拷贝旧数据到新内存,释放旧内存。

与 Go Slice 的异同:

维度

C++ std::vector

Go Slice

相同点

均基于动态数组,支持自动扩容;支持随机访问(O (1))

均基于动态数组,支持自动扩容;支持随机访问(O (1))

内存管理

需手动reserve()预扩容(避免频繁拷贝),析构时自动释放底层数组

底层数组由 Go GC 管理,无需手动释放

切片机制

无内置 “切片” 功能(需手动通过data+索引实现,易导致迭代器失效)

支持slice[low:high]切片,新切片与原切片共享底层数组

元素构造

严格遵循 RAII(元素构造 / 析构均自动调用)

仅存储值类型,无析构函数调用逻辑

扩容策略

编译器相关(1.5 倍 / 2 倍)

容量 <1024 时 2 倍扩容,>1024 时 1.25 倍扩容

4. 面试官:请介绍 gRPC 和 HTTP/2.0,以及 C++ 中如何使用 gRPC?

参考答案:

gRPC 是 Google 开源的高性能 RPC 框架,其底层传输协议基于 HTTP/2.0,二者协同实现 “跨语言、低延迟” 的远程调用:

HTTP/2.0 提供基础能力:二进制分帧、多路复用、头部压缩,解决 HTTP/1.x 的队头阻塞问题;gRPC 在 HTTP/2.0 之上封装:定义服务接口(.proto)、使用 Protobuf 序列化数据、支持四种调用模式(简单 RPC、服务端流、客户端流、双向流)。

C++ 中使用 gRPC 的核心步骤:

定义服务接口(.proto 文件):声明服务方法和数据结构,例如:

syntax = "proto3"; service UserService {     // 简单RPC:获取用户信息     rpc GetUser(GetUserRequest) returns (GetUserResponse); } // 请求消息 message GetUserRequest {     string user_id = 1; } // 响应消息 message GetUserResponse {     string user_id = 1;     string name = 2;     int32 age = 3; }

使用protoc编译器 +grpc_cpp_plugin插件,生成服务端 / 客户端代码:

protoc --cpp_out=. --grpc_out=. --plugin=protoc-gen-grpc=/usr/bin/grpc_cpp_plugin user.proto

生成文件:user.pb.h(消息类定义)、user.pb.cc(序列化 / 反序列化逻辑)、user.grpc.pb.h(服务接口)、user.grpc.pb.cc(RPC 调用逻辑)。

继承生成的服务类(实现服务端),重写业务方法:

#include "user.grpc.pb.h" using grpc::Server; using grpc::ServerBuilder; using grpc::ServerContext; using grpc::Status; class UserServiceImpl : public UserService::Service {     Status GetUser(ServerContext* context, const GetUserRequest* request,                    GetUserResponse* response) override {         // 业务逻辑:根据user_id查询用户         response->set_user_id(request->user_id());         response->set_name("Zhang San");         response->set_age(25);         return Status::OK;     } }; int main() {     std::string server_addr("0.0.0.0:50051");     UserServiceImpl service;     ServerBuilder builder;     builder.AddListeningPort(server_addr, grpc::InsecureServerCredentials());     builder.RegisterService(&service);     std::unique_ptr<Server> server(builder.BuildAndStart());     server->Wait();     return 0; }

实现客户端:通过Stub调用远程服务:

#include "user.grpc.pb.h" using grpc::Channel; using grpc::ClientContext; using grpc::Status; class UserClient { public:     UserClient(std::shared_ptr<Channel> channel)         : stub_(UserService::NewStub(channel)) {}     void GetUser(const std::string& user_id) {         GetUserRequest request;         request.set_user_id(user_id);         GetUserResponse response;         ClientContext context;         Status status = stub_->GetUser(&context, request, &response);         if (status.ok()) {             std::cout << "Name: " << response.name() << ", Age: " << response.age() << std::endl;         }     } private:     std::unique_ptr<UserService::Stub> stub_; }; int main() {     UserClient client(grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials()));     client.GetUser("123");     return 0; }

5. 面试官:Protobuf 是什么?在 C++ 中如何实现数据的序列化与反序列化?

参考答案:

Protobuf(Protocol Buffers)是 Google 开源的跨语言、跨平台序列化协议,用于将结构化数据转换为二进制流(便于存储 / 传输),或从二进制流恢复数据。相比 XML/JSON,其优势是体积小、解析快、可扩展

C++ 中 Protobuf 的使用流程:

1. 编写.proto 文件(定义数据结构)

syntax = "proto3"; // 使用proto3语法 package demo;      // 命名空间,避免类名冲突 message Person {     int32 id = 1;          // 字段ID(序列化时的标识,不可重复)     string name = 2;       // 字符串类型     repeated int32 tags = 3;// repeated表示“可重复字段”(类似数组)     enum Gender {          // 枚举类型         MALE = 0;         FEMALE = 1;     }     Gender gender = 4; }

2. 通过protoc编译器生成对应语言的代码(包含消息类、序列化 / 反序列化方法):

protoc --cpp_out=. person.proto

生成文件:person.pb.h(类定义)和person.pb.cc(实现逻辑)。

3. 序列化与反序列化代码实现

#include "person.pb.h" #include <fstream> #include <iostream> using namespace demo; // 对应.proto中的package int main() {     // -------------------------- 1. 构造消息对象 --------------------------     Person person;     person.set_id(1);     person.set_name("Li Si");     person.add_tags(10); // 给repeated字段添加元素     person.add_tags(20);     person.set_gender(Person::MALE);     // -------------------------- 2. 序列化(对象 → 二进制流) --------------------------     // 方式1:序列化到字符串(内存)     std::string serialized_str;     person.SerializeToString(&serialized_str);     std::cout << "Serialized size: " << serialized_str.size() << " bytes" << std::endl;     // 方式2:序列化到文件     std::ofstream out("person.bin", std::ios::binary);     person.SerializeToOstream(&out);     out.close();     // -------------------------- 3. 反序列化(二进制流 → 对象) --------------------------     // 方式1:从字符串反序列化     Person person1;     if (person1.ParseFromString(serialized_str)) {         std::cout << "ID: " << person1.id() << ", Name: " << person1.name() << std::endl;         for (int i = 0; i < person1.tags_size(); ++i) {             std::cout << "Tag " << i << ": " << person1.tags(i) << std::endl;         }     }     // 方式2:从文件反序列化     Person person2;     std::ifstream in("person.bin", std::ios::binary);     if (person2.ParseFromIstream(&in)) {         std::cout << "Gender: " << (person2.gender() == Person::MALE ? "MALE" : "FEMALE") << std::endl;     }     in.close();     return 0; }

Protobuf 核心优势:

高效:二进制编码,比 JSON 小 30%-50%,解析速度快 5-10 倍;强类型:编译时检查字段类型,避免运行时类型错误;可扩展:新增字段时,旧代码可忽略新字段(需设置optional或repeated),保证向前兼容;多语言兼容:同一.proto文件可生成 Java/Python/Go 等多语言代码,便于跨服务通信。

6. 面试官:C++ 中的std::thread与协程(如 C++20 coroutine、boost.coroutine)核心区别是什么?

参考答案:

二者均用于实现 “并发 / 并行”,但本质是内核态线程用户态协程的差异,核心区别如下:

维度

std::thread(内核线程)

C++ 协程(用户态)

内存开销

初始栈大小 1-2MB(系统级,不可动态调整)

初始栈几 KB 到几十 KB(可动态扩容,如 C++20 协程)

创建速度

慢(约 10μs,需内核态资源分配)

快(约几百 ns,仅用户态操作)

调度主体

操作系统内核(抢占式调度)

用户态调度器(协作式或半抢占式)

切换成本

高(1-2μs,需保存 / 恢复内核上下文)

低(几十 ns,仅保存用户态寄存器)

上下文内容

包含内核栈、页表、寄存器等

仅包含用户态栈、程序计数器等

适用场景

CPU 密集型任务(如计算、排序)

I/O 密集型任务(如网络请求、文件读写)

C++ 支持

C++11 标准库原生支持

C++20 标准库提供核心机制,需手动封装(如co_yield/co_return)

关键示例(C++20 协程简化版)

协程通过co_await挂起,避免 I/O 阻塞:

#include <coroutine> #include <iostream> #include <chrono> #include <thread> // 协程返回类型(需实现coroutine_traits) struct Task {     struct promise_type {         Task get_return_object() { return {}; }         std::suspend_never initial_suspend() { return {}; }         std::suspend_never final_suspend() noexcept { return {}; }         void return_void() {}         void unhandled_exception() {}     }; }; // 模拟I/O操作(如网络请求) struct IoAwaiter {     bool await_ready() const noexcept { return false; } // 需挂起     void await_suspend(std::coroutine_handle<> handle) {         // 异步执行I/O,完成后唤醒协程         std::thread([handle]() {             std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 模拟I/O耗时             handle.resume(); // 唤醒协程         }).detach();     }     void await_resume() const noexcept {} // 无返回值 }; // 协程函数 Task async_io_task() {     std::cout << "Start I/O..." << std::endl;     co_await IoAwaiter{}; // 挂起,等待I/O完成     std::cout << "I/O completed" << std::endl; // I/O完成后执行 } int main() {     async_io_task();     std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待协程完成     return 0; }

7. 面试官:如何用 C++ 实现线程安全的 LRU 缓存?请说明思路和关键代码。

参考答案:

LRU(最近最少使用)缓存的核心是 “淘汰最近最少访问的元素”,线程安全需解决并发读写冲突,实现思路如下:

1. 底层数据结构

双向链表:维护元素访问顺序(头部 = 最近访问,尾部 = 最少访问),支持 O (1) 插入 / 删除;哈希表(std::unordered_map):映射 “键→链表节点”,支持 O (1) 查找。

2. 线程安全保障

使用std::shared_mutex(读写锁,C++17):

读操作(Get):加共享锁(多个线程可同时读);写操作(Put/Delete):加独占锁(仅一个线程可写,阻塞所有读)。

3. 核心操作逻辑

Get:查找键→存在则移到链表头部(标记为最近访问)→返回值;Put:若键存在→更新值并移到头部;若不存在→插入新节点到头部→若超容量→删除链表尾部节点。

关键代码:

#include <iostream> #include <unordered_map> #include <shared_mutex> #include <memory> // 用于unique_ptr // 1. 双向链表节点(存储键值对,避免哈希表存值导致冗余) struct LRUNode {     int key;     int val;     LRUNode* prev;     LRUNode* next;     LRUNode(int k, int v) : key(k), val(v), prev(nullptr), next(nullptr) {} }; // 2. 非线程安全的LRU基础类(封装核心逻辑) class LRUCache { public:     explicit LRUCache(size_t cap) : capacity_(cap) {         // 虚拟头/尾节点,简化边界操作         head_ = std::make_unique<LRUNode>(0, 0);         tail_ = std::make_unique<LRUNode>(0, 0);         head_->next = tail_.get();         tail_->prev = head_.get();     }     ~LRUCache() = default;     // 查找元素(非线程安全,供线程安全类调用)     int Get(int key) {         if (cache_map_.find(key) == cache_map_.end()) {             return -1; // 键不存在         }         LRUNode* node = cache_map_[key];         MoveToHead(node); // 移到头部(标记最近访问)         return node->val;     }     // 插入/更新元素(非线程安全)     void Put(int key, int val) {         if (cache_map_.find(key) != cache_map_.end()) {             // 键已存在:更新值并移到头部             LRUNode* node = cache_map_[key];             node->val = val;             MoveToHead(node);             return;         }         // 键不存在:创建新节点         LRUNode* new_node = new LRUNode(key, val);         cache_map_[key] = new_node;         AddToHead(new_node); // 插入头部         // 检查容量:超容则删除尾部         if (cache_map_.size() > capacity_) {             LRUNode* del_node = RemoveTail();             cache_map_.erase(del_node->key);             delete del_node; // 释放内存(避免泄漏)         }     } private:     // 辅助函数:将节点添加到头部     void AddToHead(LRUNode* node) {         node->prev = head_.get();         node->next = head_->next;         head_->next->prev = node;         head_->next = node;     }     // 辅助函数:将节点移到头部(先移除再添加)     void MoveToHead(LRUNode* node) {         RemoveNode(node);         AddToHead(node);     }     // 辅助函数:移除指定节点     void RemoveNode(LRUNode* node) {         node->prev->next = node->next;         node->next->prev = node->prev;     }     // 辅助函数:移除尾部节点(最少访问)     LRUNode* RemoveTail() {         LRUNode* del_node = tail_->prev;         RemoveNode(del_node);         return del_node;     } private:     size_t capacity_;                          // 缓存容量     std::unordered_map<int, LRUNode*> cache_map_; // 键→节点映射     std::unique_ptr<LRUNode> head_;            // 虚拟头节点(RAII管理)     std::unique_ptr<LRUNode> tail_;            // 虚拟尾节点(RAII管理) }; // 3. 线程安全的LRU类(包装读写锁) class ThreadSafeLRU { public:     explicit ThreadSafeLRU(size_t capacity) : lru_(capacity) {}     // 线程安全的Get(读操作→共享锁)     int Get(int key) {         std::shared_lock<std::shared_mutex> lock(mutex_);         return lru_.Get(key);     }     // 线程安全的Put(写操作→独占锁)     void Put(int key, int val) {         std::unique_lock<std::shared_mutex> lock(mutex_);         lru_.Put(key, val);     } private:     LRUCache lru_;                          // 基础LRU实例     mutable std::shared_mutex mutex_;       // 读写锁(mutable允许const函数加锁) }; // 测试代码 int main() {     ThreadSafeLRU lru(2); // 容量为2     // 线程1:Put操作     std::thread t1([&]() {         lru.Put(1, 10);         lru.Put(2, 20);         std::cout << "Thread1: Put (1,10), (2,20)" << std::endl;     });     // 线程2:Get操作     std::thread t2([&]() {         std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 等待t1写入         std::cout << "Thread2: Get(1) = " << lru.Get(1) << std::endl; // 10         lru.Put(3, 30); // 此时容量超2,淘汰最少访问的2         std::cout << "Thread2: Put (3,30), Get(2) = " << lru.Get(2) << std::endl; // -1     });     t1.join();     t2.join();     return 0; }

8. 面试官:C++ 中没有 GC,如何进行内存优化,避免内存泄漏和减少内存开销?

参考答案:

C++ 内存优化的核心是 “合理分配、自动释放、复用对象”,主要策略分为 “避免泄漏” 和 “减少开销” 两类:

1、避免内存泄漏(核心是自动管理)

a. 严格使用 RAII 机制:

智能指针:用std::unique_ptr(独占所有权)、std::shared_ptr(共享所有权)、std::weak_ptr(避免循环引用)管理动态内存,自动调用析构释放。

示例:

// 错误:手动new未delete,导致泄漏 int* p = new int(10);  // 正确:unique_ptr自动释放 std::unique_ptr<int> p(new int(10)); 容器 / 资源封装:用std::vector/std::string代替char*,用std::fstream代替FILE*,避免手动释放资源。

b. 避免野指针与循环引用:

野指针:指针赋值前检查nullptr,不使用已释放的指针,动态分配后检查是否成功(new失败抛std::bad_alloc);循环引用:std::shared_ptr相互引用会导致内存泄漏,需用std::weak_ptr打破循环,示例:struct A { std::shared_ptr<B> b; }; struct B { std::weak_ptr<A> a; }; // 用weak_ptr避免循环 auto a = std::make_shared<A>(); auto b = std::make_shared<B>(); a->b = b; b->a = a; // 无循环引用,析构时正常释放

c. 工具检测:

编译期:启用-fsanitize=address(GCC/Clang)、/fsanitize=address(MSVC),运行时检测内存泄漏;调试工具:Valgrind(Linux)、Visual Leak Detector(Windows)、Xcode Instruments(macOS)。

2、减少内存开销(核心是复用与预分配)

a. 对象复用:对象池模式

对频繁创建 / 销毁的对象(如网络缓冲区、日志对象),用对象池缓存空闲对象,减少malloc/free调用(系统调用开销高)。

示例(简单对象池):

#include <queue> #include <mutex> #include <memory> template <typename T> class ObjectPool { public:     explicit ObjectPool(size_t max_size) : max_size_(max_size) {}     // 获取对象(无空闲则创建,有则复用)     std::unique_ptr<T, std::function<void(T*)>> Get() {         std::lock_guard<std::mutex> lock(mutex_);         if (!pool_.empty()) {             auto obj = std::move(pool_.front());             pool_.pop();             // 自定义释放:放回池而非销毁             return {obj.release(), [this](T* p) {                 std::lock_guard<std::mutex> lk(this->mutex_);                 if (this->pool_.size() < this->max_size_) {                     p->Reset(); // 重置对象状态(需T实现Reset)                     this->pool_.push(std::unique_ptr<T>(p));                 } else {                     delete p; // 超容量则销毁                 }             }};         }         // 无空闲对象,创建新的         return {new T(), [this](T* p) { /* 同上述释放逻辑 */ }};     } private:     std::queue<std::unique_ptr<T>> pool_; // 空闲对象队列     std::mutex mutex_;                    // 线程安全锁     size_t max_size_;                     // 池最大容量 };

b. 预分配内存:减少扩容开销

std::vector/std::string用reserve()预分配容量,避免频繁扩容(扩容会拷贝旧数据);

示例:

std::vector<int> vec; vec.reserve(1000); // 预分配1000个元素容量,避免多次扩容 for (int i = 0; i < 1000; ++i) {     vec.push_back(i); // 无扩容,效率高 }

栈内存优先:小对象、短生命周期对象用栈内存(局部变量),避免动态分配(栈操作是编译器指令,速度快)。

c. 数据结构优化

选择合适容器:std::vector(随机访问快)、std::unordered_map(哈希查找快),避免滥用std::list(缓存友好度差);紧凑数据结构:按成员大小排序减少内存对齐浪费,示例:// 优化前:内存对齐浪费3字节(char占1字节,对齐到4字节) struct Bad { char c; int i; }; // 总大小8字节 // 优化后:无浪费(int占4字节,char占1字节,总大小5字节,对齐到8字节) struct Good { int i; char c; }; // 总大小8字节(仍有3字节浪费,但比之前少)

d. 避免不必要的拷贝

引用传递:函数参数用const T&传递大对象(如void Func(const std::string& s)),避免拷贝;移动语义:用std::move()转移对象所有权(如std::vector<int> v2 = std::move(v1)),无拷贝开销;返回值优化(RVO/NRVO):编译器自动优化函数返回对象的拷贝,示例:std::vector<int> CreateVec() {     return {1,2,3}; // NRVO:直接在调用方构造,无拷贝 } auto vec = CreateVec(); // 无拷贝

9. 面试官:在 C++ 网络编程中,如何解决 TCP 粘包问题?请列举方案并说明实现思路。

参考答案:

TCP 是 “流式协议”,无消息边界,发送方连续发送的数据包可能被接收方合并为一个 “粘包”,解决方案核心是在应用层定义消息边界,常见方案如下:

方案 1:定长分包(固定消息长度)

约定每个数据包的固定长度(如 1024 字节),接收方每次读取固定字节数,不足则补 0,解析时忽略补位。

实现示例(C++)

// 发送方:每次发送1024字节(不足补0) void SendFixedLength(int sockfd, const std::string& data) {     char buf[1024] = {0}; // 固定1024字节缓冲区     memcpy(buf, data.data(), std::min(data.size(), 1024ul));     send(sockfd, buf, 1024, 0); } // 接收方:每次读取1024字节 void RecvFixedLength(int sockfd) {     char buf[1024];     while (true) {         ssize_t n = recv(sockfd, buf, 1024, 0);         if (n <= 0) break;         // 解析数据(忽略末尾补0)         std::string data(buf, strnlen(buf, 1024));         std::cout << "Received: " << data << std::endl;     } }

适用场景:

数据长度固定的场景(如传感器固定格式数据、心跳包),缺点是灵活性差,数据不足时浪费带宽。

方案 2:特殊字符分隔(终止符标记)

在每个数据包末尾添加特殊字符(如'\n'、"\r\n"),接收方读取数据到缓冲区,遍历缓冲区查找分隔符,找到则拆分出一个完整数据包。

实现示例(C++)

// 发送方:每个数据包末尾加'\n' void SendWithDelimiter(int sockfd, const std::string& data) {     std::string msg = data + "\n";     send(sockfd, msg.data(), msg.size(), 0); } // 接收方:按'\n'拆分缓冲区 void RecvWithDelimiter(int sockfd) {     char buf[1024];     std::string recv_buf; // 累积缓冲区     while (true) {         ssize_t n = recv(sockfd, buf, 1024, 0);         if (n <= 0) break;         recv_buf.append(buf, n);         // 查找分隔符'\n',拆分数据包         size_t pos;         while ((pos = recv_buf.find('\n')) != std::string::npos) {             std::string msg = recv_buf.substr(0, pos); // 完整数据包             std::cout << "Received: " << msg << std::endl;             recv_buf.erase(0, pos + 1); // 移除已处理数据         }     } }

注意点:需避免数据中包含分隔符(如文本中含'\n'),解决方案是 “转义”(如'\n'转义为"\\n"),解析时再还原:

// 发送方:转义处理 std::string Escape(const std::string& data) {     std::string res;     for (char c : data) {         if (c == '\n') res += "\\n";         else if (c == '\\') res += "\\\\";         else res += c;     }     return res; } // 接收方:还原处理 std::string Unescape(const std::string& data) {     std::string res;     for (size_t i = 0; i < data.size(); ++i) {         if (data[i] == '\\' && i+1 < data.size()) {             if (data[i+1] == 'n') { res += '\n'; i++; }             else if (data[i+1] == '\\') { res += '\\'; i++; }         } else {             res += data[i];         }     }     return res; }

适用场景:

文本协议(如 HTTP 头部用\r\n分隔),实现简单但转义逻辑增加复杂度。

方案 3:消息头记录长度(最常用)

每个数据包分为 “消息头” 和 “消息体”:

消息头:固定长度(如 4 字节),存储消息体的字节数(用uint32_t表示);消息体:实际数据,长度由消息头指定。

接收方先读消息头获取长度,再按长度读取消息体,彻底解决粘包。

实现示例(C++)

#include <cstdint> #include <arpa/inet.h> // 网络字节序转换(htons/ntohs) // 发送方:先发送长度(网络字节序),再发送数据 void SendWithHeader(int sockfd, const std::string& data) {     uint32_t len = htonl(data.size()); // 转换为网络字节序(大端)     // 发送消息头(4字节)     send(sockfd, &len, sizeof(len), 0);     // 发送消息体     send(sockfd, data.data(), data.size(), 0); } // 接收方:先读长度,再读对应字节数的数据 void RecvWithHeader(int sockfd) {     uint32_t len;     // 1. 读取消息头(4字节)     ssize_t n = recv(sockfd, &len, sizeof(len), 0);     if (n != sizeof(len)) return; // 读取失败     len = ntohl(len); // 转换为主机字节序     // 2. 读取消息体(len字节)     std::string data;     data.resize(len);     size_t total_read = 0;     while (total_read < len) {         n = recv(sockfd, &data[total_read], len - total_read, 0);         if (n <= 0) break;         total_read += n;     }     if (total_read == len) {         std::cout << "Received: " << data << std::endl;     } }

优势与适用场景

无数据格式限制(支持二进制 / 文本);通用性强,是大多数自定义协议的首选(如 Redis 协议、私有 RPC 协议)。

方案 4:基于成熟框架(如 Netty、muduo)

C++ 网络库(如 muduo)内置粘包处理,通过

LengthFieldBasedFrameDecoder等组件自动拆分数据包,无需手动实现:

// muduo示例:使用LengthFieldPrepender和LengthFieldBasedFrameDecoder void onConnection(const TcpConnectionPtr& conn) {     if (conn->connected()) {         // 发送时自动添加长度字段(4字节,网络字节序)         conn->setTcpNoDelay(true);         auto codec = std::make_shared<LengthFieldBasedFrameCodec>(             4, 0, 4, 0, 4); // 长度字段占4字节         conn->setMessageCallback([codec](const TcpConnectionPtr&, Buffer*, Timestamp) {             // 回调中已自动拆分出完整数据包         });     } }

10. 面试官:C++ 中值传递、指针传递和引用传递的区别是什么?如何选择?

参考答案:

三种传递方式的核心差异在于是否拷贝对象能否修改原值,选择需结合性能和安全性:

维度

值传递(pass by value)

指针传递(pass by pointer)

引用传递(pass by reference)

本质

拷贝实参到形参(新对象)

传递实参的地址(指针变量)

实参的别名(无拷贝,共享内存)

修改影响

形参修改不影响实参

解引用后修改会影响实参

直接修改会影响实参

空值情况

无(形参是有效对象)

可传递 nullptr(需检查有效性)

不可为 null(必须绑定有效对象)

语法形式

void func(Type param)

void func(Type* param)

void func(Type& param)

拷贝开销

大(尤其大对象 / 深拷贝)

小(仅拷贝指针地址,8 字节)

无(零开销)

适用场景

小对象(如 int、char)、无需修改原值

需传递 nullptr、动态内存对象

大对象(避免拷贝)、需修改原值且非空

示例代码

#include <iostream> #include <string> // 值传递:拷贝字符串(开销大) void ValuePass(std::string s) {     s = "modified"; } // 指针传递:可修改原值,需检查nullptr void PointerPass(std::string* s) {     if (s != nullptr) { // 必须检查,否则可能崩溃         *s = "modified";     } } // 引用传递:零开销,直接修改原值(非空) void ReferencePass(std::string& s) {     s = "modified"; // 无需解引用,语法简洁 } int main() {     std::string str = "original";     ValuePass(str);     std::cout << str << std::endl; // 输出"original"(未修改)     PointerPass(&str);     std::cout << str << std::endl; // 输出"modified"     ReferencePass(str);     std::cout << str << std::endl; // 输出"modified"     return 0; }

最佳实践

小对象(如int、double)用值传递(拷贝开销可忽略);大对象(如std::string、自定义结构体)用 **const引用传递 **(const Type&)避免修改且无拷贝:void ReadOnly(const std::string& s) { // 只读,不修改     std::cout << s << std::endl; }

3.需修改原值且对象非空时用引用传递

4.需支持 “空值” 场景(如可选参数)用指针传递,且必须在函数内检查nullptr。

11. 面试官:C++ 中如何实现大文件的分片上传?请说明核心步骤和代码思路。

参考答案:

分片上传适用于 GB 级大文件,核心是 “拆分文件→并行上传→服务端合并”,步骤如下:

1. 前端 / 客户端处理

计算文件唯一标识(如 MD5/SHA256),用于断点续传和校验;拆分文件为固定大小分片(如 4MB / 片);并行上传分片(多线程 / 协程),记录已上传分片;所有分片上传完成后,请求服务端合并。

2. 服务端处理

接收分片,临时存储(如/tmp/{file_hash}/{chunk_index});校验分片完整性(可选,如 CRC32);收到合并请求后,按顺序拼接所有分片为完整文件。

C++ 代码实现

1. 客户端:文件分片与上传

#include <fstream> #include <vector> #include <thread> #include <future> #include <cryptopp/md5.h> // 需Crypto++库计算MD5 #include <cryptopp/hex.h> // 计算文件MD5(唯一标识) std::string FileMD5(const std::string& file_path) {     CryptoPP::MD5 hash;     byte digest[CryptoPP::MD5::DIGESTSIZE];     std::ifstream file(file_path, std::ios::binary);     byte buf[1024 * 1024]; // 1MB缓冲区     while (file.good()) {         file.read((char*)buf, sizeof(buf));         hash.Update(buf, file.gcount());     }     hash.Final(digest);     // 转换为十六进制字符串     std::string md5_str;     CryptoPP::StringSource(digest, sizeof(digest), true,         new CryptoPP::HexEncoder(new CryptoPP::StringSink(md5_str)));     return md5_str; } // 分片上传函数(单分片) bool UploadChunk(const std::string& file_hash, int chunk_idx,                  const std::string& chunk_data, const std::string& server_url) {     // 模拟HTTP上传:使用libcurl发送POST请求     // 实际实现需用libcurl设置URL、请求体(chunk_data)、 headers(含file_hash和chunk_idx)     std::cout << "Uploading chunk " << chunk_idx << "..." << std::endl;     return true; // 假设上传成功 } // 分片上传主函数 bool MultipartUpload(const std::string& file_path, size_t chunk_size = 4 * 1024 * 1024) {     // 1. 计算文件MD5     std::string file_hash = FileMD5(file_path);     std::cout << "File MD5: " << file_hash << std::endl;     // 2. 获取文件大小     std::ifstream file(file_path, std::ios::binary | std::ios::ate);     size_t file_size = file.tellg();     file.seekg(0);     // 3. 计算分片数     size_t chunk_count = (file_size + chunk_size - 1) / chunk_size;     std::vector<std::future<bool>> futures;     // 4. 读取并上传分片(多线程并行)     for (size_t i = 0; i < chunk_count; ++i) {         size_t offset = i * chunk_size;         size_t read_size = std::min(chunk_size, file_size - offset);         std::string chunk_data(read_size, '\0');         file.seekg(offset);         file.read(&chunk_data[0], read_size);         // 异步上传(线程池或std::async)         futures.emplace_back(std::async(std::launch::async,              UploadChunk, file_hash, i, chunk_data, "http://server/upload"));     }     // 5. 等待所有分片上传完成     for (auto& f : futures) {         if (!f.get()) return false; // 若任一分片失败,返回错误     }     // 6. 请求服务端合并分片     std::cout << "All chunks uploaded. Requesting merge..." << std::endl;     // 发送合并请求:POST http://server/merge?file_hash=xxx&chunk_count=xxx     return true; }

2. 服务端:接收分片与合并

#include <fstream> #include <filesystem> #include <mutex> namespace fs = std::filesystem; // 接收分片并存储 bool ReceiveChunk(const std::string& file_hash, int chunk_idx,                   const std::string& chunk_data) {     // 创建临时目录(如/tmp/file_uploads/{file_hash})     fs::path tmp_dir = fs::temp_directory_path() / "file_uploads" / file_hash;     fs::create_directories(tmp_dir);     // 存储分片文件(命名为chunk_0, chunk_1...)     std::ofstream chunk_file(tmp_dir / ("chunk_" + std::to_string(chunk_idx)),                               std::ios::binary);     chunk_file.write(chunk_data.data(), chunk_data.size());     return true; } // 合并分片为完整文件 bool MergeChunks(const std::string& file_hash, size_t chunk_count,                  const std::string& output_path) {     fs::path tmp_dir = fs::temp_directory_path() / "file_uploads" / file_hash;     std::ofstream output_file(output_path, std::ios::binary);     // 按顺序拼接所有分片     for (size_t i = 0; i < chunk_count; ++i) {         fs::path chunk_path = tmp_dir / ("chunk_" + std::to_string(i));         if (!fs::exists(chunk_path)) return false;         std::ifstream chunk_file(chunk_path, std::ios::binary | std::ios::ate);         size_t chunk_size = chunk_file.tellg();         chunk_file.seekg(0);         std::string chunk_data(chunk_size, '\0');         chunk_file.read(&chunk_data[0], chunk_size);         output_file.write(chunk_data.data(), chunk_size);     }     // 合并完成后删除临时分片     fs::remove_all(tmp_dir);     return true; }

12. 面试官:C++ 中std::vector与std::list的核心差异是什么?如何选择?

参考答案:

二者是 STL 中最常用的序列容器,底层结构不同导致性能特性差异显著:

维度

std::vector

std::list

底层结构

动态数组(连续内存)

双向链表(非连续节点)

随机访问

O (1)(通过索引直接访问)

O (n)(需从头 / 尾遍历)

插入 / 删除

中间插入 / 删除 O (n)(需移动后续元素)

中间插入 / 删除 O (1)(仅修改指针)

内存开销

低(仅存储数据,无额外指针)

高(每个节点含数据 + 2 个指针)

缓存友好度

高(连续内存,CPU 缓存命中率高)

低(节点分散,易缓存失效)

扩容成本

可能触发扩容(拷贝旧数据,O (n))

无扩容成本(节点动态分配)

迭代器失效

扩容 / 插入时可能失效(指向旧内存)

仅删除当前节点时失效,其他迭代器有效

示例代码与场景选择

#include <vector> #include <list> #include <chrono> #include <iostream> // 测试随机访问性能 template <typename Container> void TestRandomAccess(Container& c, const std::string& name) {     auto start = std::chrono::high_resolution_clock::now();     for (size_t i = 0; i < c.size(); i += 100) {         auto val = c[i]; // vector支持[],list不支持(需用advance)     }     auto end = std::chrono::high_resolution_clock::now();     std::cout << name << " random access: "                << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count()                << " us" << std::endl; } // 测试中间插入性能 template <typename Container> void TestInsertMiddle(Container& c, const std::string& name) {     auto it = c.begin();     std::advance(it, c.size() / 2); // 定位到中间位置     auto start = std::chrono::high_resolution_clock::now();     for (int i = 0; i < 1000; ++i) {         it = c.insert(it, i); // 插入后返回新迭代器(list高效)     }     auto end = std::chrono::high_resolution_clock::now();     std::cout << name << " insert middle: "                << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count()                << " us" << std::endl; } int main() {     const size_t N = 100000;     std::vector<int> vec;     std::list<int> lst;     // 初始化数据     for (size_t i = 0; i < N; ++i) {         vec.push_back(i);         lst.push_back(i);     }     // 测试随机访问(vector远快于list)     TestRandomAccess(vec, "vector"); // 约10us     // TestRandomAccess(lst, "list"); // 不支持[],需用迭代器遍历(约10000us)     // 测试中间插入(list远快于vector)     TestInsertMiddle(vec, "vector"); // 约50000us(需移动大量元素)     TestInsertMiddle(lst, "list");   // 约100us(仅修改指针)     return 0; }

选择原则

1. 优先用std::vector的场景:

需要随机访问(如[]操作、排序算法);数据量不大且插入 / 删除主要在末尾(push_back/pop_back);追求空间效率和缓存友好性(如数值计算、高频访问场景)。

2. 用std::list的场景:

频繁在中间插入 / 删除(如链表式数据结构、实现队列 / 栈的中间操作);数据量动态变化大,且无法预估容量(避免 vector 频繁扩容)。

3. 折中方案:

std::deque:支持两端 O (1) 插入 / 删除,随机访问 O (1),适合实现队列;boost::container::stable_vector:迭代器稳定(类似 list),内存连续(类似 vector)。

13. 面试官:C++ 中如何实现类似 Go 的 Context 功能,用于控制并发任务的超时和取消?

参考答案:

Go 的context.Context用于传递 “取消信号、超时时间、请求域数据”,C++ 可通过回调 + 条件变量 + 共享状态实现类似功能,核心是 “信号传递机制”。

核心设计思路:

共享状态:存储取消状态(是否已取消)、超时时间、附加键值对数据;信号触发:支持手动取消、超时自动取消;回调注册:允许任务注册取消回调(被取消时执行清理逻辑);派生机制:支持从父 Context 创建子 Context(取消信号级联传播)。

C++ 实现代码:

#include <iostream> #include <mutex> #include <condition_variable> #include <chrono> #include <unordered_map> #include <functional> #include <memory> #include <thread> #include <any> // 前向声明 class Context; using ContextPtr = std::shared_ptr<Context>; class Context { public:     // 创建根Context(无超时,需手动取消)     static ContextPtr Background() {         return std::shared_ptr<Context>(new Context());     }     // 从父Context创建带超时的子Context     static ContextPtr WithTimeout(ContextPtr parent, std::chrono::milliseconds timeout) {         auto ctx = std::shared_ptr<Context>(new Context(parent));         // 启动超时定时器         ctx->timeout_thread_ = std::thread([ctx, timeout]() {             std::this_thread::sleep_for(timeout);             ctx->Cancel(); // 超时后取消         });         ctx->timeout_thread_.detach();         return ctx;     }     // 从父Context创建可手动取消的子Context     static ContextPtr WithCancel(ContextPtr parent) {         return std::shared_ptr<Context>(new Context(parent));     }     ~Context() {         Cancel(); // 析构时取消,确保资源释放     }     // 手动取消     void Cancel() {         std::lock_guard<std::mutex> lock(mtx_);         if (canceled_) return;         canceled_ = true;         cv_.notify_all(); // 唤醒所有等待的任务         // 执行注册的回调         for (auto& cb : callbacks_) {             cb();         }         // 级联取消子Context         for (auto& child : children_) {             child->Cancel();         }     }     // 检查是否已取消     bool IsCanceled() {         std::lock_guard<std::mutex> lock(mtx_);         return canceled_;     }     // 等待取消信号(阻塞直到取消)     void Wait() {         std::unique_lock<std::mutex> lock(mtx_);         cv_.wait(lock, [this]() { return canceled_; });     }     // 注册取消回调(任务被取消时执行)     void OnCancel(std::function<void()> callback) {         std::lock_guard<std::mutex> lock(mtx_);         if (canceled_) {             callback(); // 已取消则立即执行         } else {             callbacks_.push_back(callback);         }     }     // 设置键值对(请求域数据)     void Set(const std::string& key, std::any value) {         std::lock_guard<std::mutex> lock(mtx_);         data_[key] = std::move(value);     }     // 获取键值对     std::any Get(const std::string& key) {         std::lock_guard<std::mutex> lock(mtx_);         if (data_.count(key)) {             return data_[key];         }         // 从父Context查找         if (parent_) {             return parent_->Get(key);         }         return std::any();     } private:     explicit Context(ContextPtr parent = nullptr) : parent_(std::move(parent)) {         // 父Context取消时,子Context也取消         if (parent_) {             parent_->OnCancel([this]() { this->Cancel(); });             parent_->children_.push_back(shared_from_this());         }     }     bool canceled_ = false;     std::mutex mtx_;     std::condition_variable cv_;     std::vector<std::function<void()>> callbacks_;     std::unordered_map<std::string, std::any> data_;     ContextPtr parent_;     std::vector<ContextPtr> children_; // 子Context,用于级联取消     std::thread timeout_thread_; // 超时线程 }; // 测试:模拟并发任务受Context控制 void Task(ContextPtr ctx, int id) {     std::cout << "Task " << id << " started" << std::endl;     // 注册取消回调(清理资源)     ctx->OnCancel([id]() {         std::cout << "Task " << id << " canceled, cleaning up..." << std::endl;     });     // 模拟任务执行(循环检查是否取消)     while (!ctx->IsCanceled()) {         std::this_thread::sleep_for(std::chrono::milliseconds(100));     }     std::cout << "Task " << id << " exited" << std::endl; } int main() {     // 创建根Context     auto root = Context::Background();     // 创建5秒超时的子Context     auto ctx = Context::WithTimeout(root, std::chrono::seconds(5));     // 设置请求域数据     ctx->Set("trace_id", "123456");     std::cout << "Trace ID: " << std::any_cast<std::string>(ctx->Get("trace_id")) << std::endl;     // 启动3个任务     std::thread t1(Task, ctx, 1);     std::thread t2(Task, ctx, 2);     std::thread t3(Task, ctx, 3);     // 等待任务结束(5秒后超时自动取消)     t1.join();     t2.join();     t3.join();     return 0; }

核心功能:

取消传播:父 Context 取消时,所有子 Context 自动取消(级联机制);超时控制:WithTimeout创建的 Context 会在指定时间后自动取消;资源清理:任务可通过OnCancel注册回调,确保取消时释放资源(如关闭文件、释放连接);请求域数据:通过Set/Get传递上下文信息(如日志 ID、用户认证信息)。

此实现可用于 C++ 并发编程中控制多线程 / 协程的生命周期,尤其适合分布式系统中的超时控制和请求跟踪。

总结

以上是腾讯 C++ 面试的核心问题及参考答案,涵盖了网络编程、内存管理、并发控制、数据结构等高频考点。

实际面试中,除了理论知识,还可能涉及代码手写(如 LRU、线程安全队列)和项目经验深挖,建议结合具体场景理解原理,而非死记硬背。