腾讯的最新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、线程安全队列)和项目经验深挖,建议结合具体场景理解原理,而非死记硬背。