腾讯 c++后端开发最新面试题
1. TCP和UDP区别是什么?
-
连接:TCP 是面向连接的传输层协议,传输数据前先要建立连接;UDP 是不需要连接,即刻传输数据。
-
服务对象:TCP 是一对一的两点服务,即一条连接只有两个端点。UDP 支持一对一、一对多、多对多的交互通信
-
可靠性:TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。UDP 是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议
-
拥塞控制、流量控制:TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
-
首部开销:TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
-
传输方式:TCP 是流式传输,没有边界,但保证顺序和可靠。UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
2. HTTP用的是哪个传输协议,为什么?
HTTP/1.1 底层基于 TCP 协议(传输控制协议),原因是可靠性需求。HTTP 作为应用层协议,需要确保数据完整、有序地传输(例如网页内容、图片等不能丢失或乱序)。TCP 提供了可靠传输(通过确认机制、重传机制、序号机制),恰好满足这一需求。
HTTP/2 仍基于 TCP 协议,但通过以下优化缓解了 HTTP/1.1 的问题:
-
多路复用:在同一 TCP 连接中,多个请求 / 响应可并行传输(通过帧和流的机制),理论上解决了 “队头阻塞”。
-
二进制帧:将数据分割为二进制帧传输,更高效且易于解析。
-
头部压缩(HPACK):减少重复的请求头数据传输。
但 HTTP/2 仍受限于 TCP 的固有缺陷,比如TCP 层的队头阻塞:多路复用依赖同一 TCP 连接,若 TCP 层出现丢包,所有流(请求)都会被阻塞等待重传,导致 HTTP/2 的多路复用优势被削弱。
HTTP/3 彻底改用 QUIC 协议 作为传输层协议,而 QUIC 基于 UDP 实现,原因是为了解决 TCP 无法克服的问题:
-
解决队头阻塞:QUIC 实现了 “流级别的重传”,某一流(请求)的丢包不会影响其他流,从根本上避免了 TCP 层的队头阻塞。
-
更快的连接建立:QUIC 合并了 TCP 三次握手和 TLS 握手过程,只需 1-2 个 RTT 即可建立加密连接(TCP + TLS 需 3-4 个 RTT)。
-
连接迁移:QUIC 使用 “连接 ID” 标识连接,而非 TCP 的 “IP + 端口”,因此当设备网络切换(如从 WiFi 到 4G)时,连接不会中断。
QUIC 基于 UDP 却能提供类似 TCP 的可靠性:通过序号、确认机制、重传机制等实现可靠传输,同时保留了 UDP 的低延迟优势。
3. HTTP状态码你知道哪些?

HTTP 状态码分为 5 大类
-
1xx 类状态码属于提示信息,是协议处理中的一种中间状态,实际用到的比较少。
-
2xx 类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。
-
3xx 类状态码表示客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向。
-
4xx 类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。
-
5xx 类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。
其中常见的具体状态码有:
-
200:请求成功;
-
301:永久重定向;302:临时重定向;
-
404:无法找到此页面;405:请求的方法类型不支持;
-
500:服务器内部出错。
4. HTTP是长连接还是短连接?
HTTP 协议采用的是「请求-应答」的模式,也就是客户端发起了请求,服务端才会返回响应,一来一回这样子。

由于 HTTP 是基于 TCP 传输协议实现的,客户端与服务端要进行 HTTP 通信前,需要先建立 TCP 连接,然后客户端发送 HTTP 请求,服务端收到后就返回响应,至此「请求-应答」的模式就完成了,随后就会释放 TCP 连接。

如果每次请求都要经历这样的过程:建立 TCP -> 请求资源 -> 响应资源 -> 释放连接,那么此方式就是 HTTP 短连接,如下图:

这样实在太累人了,一次连接只能请求一次资源。
能不能在第一个 HTTP 请求完后,先不断开 TCP 连接,让后续的 HTTP 请求继续使用此连接?
当然可以,HTTP 的 Keep-Alive 就是实现了这个功能,可以使用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,避免了连接建立和释放的开销,这个方法称为 HTTP 长连接。

HTTP 长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。
所以,HTTP/1.1 版本之后,默认就是长连接的模式。
5. .Linux查看进程指令是哪个?
ps 命令
6. linux 命令 ps -aux第二列是什么,和ps -elf区别?
ps -aux第二列是 进程的pid号:

下图是ps -elf的显示:

两者的区别:
-
ps -aux更侧重 用户、资源占用(CPU / 内存) 等信息,适合快速查看进程的资源消耗情况。 -
ps -elf更侧重 进程关系(PPID)、优先级(PRI/NI)、系统级标志(F) 等信息,适合分析进程间的父子关系和系统调度相关属性。
7. mysql的索引如何实现的?
MySQL InnoDB 引擎是用了B+树作为了索引的数据结构。
B+Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放的。每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每一个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表。
主键索引的 B+Tree 如图所示:

比如,我们执行了下面这条查询语句:
select * from product where id= 5;
这条语句使用了主键索引查询 id 号为 5 的商品。查询过程是这样的,B+Tree 会自顶向下逐层进行查找:
-
将 5 与根节点的索引数据 (1,10,20) 比较,5 在 1 和 10 之间,所以根据 B+Tree的搜索逻辑,找到第二层的索引数据 (1,4,7);
-
在第二层的索引数据 (1,4,7)中进行查找,因为 5 在 4 和 7 之间,所以找到第三层的索引数据(4,5,6);
-
在叶子节点的索引数据(4,5,6)中进行查找,然后我们找到了索引值为 5 的行数据。
数据库的索引和数据都是存储在硬盘的,我们可以把读取一个节点当作一次磁盘 I/O 操作。那么上面的整个查询过程一共经历了 3 个节点,也就是进行了 3 次 I/O 操作。
B+Tree 存储千万级的数据只需要 3-4 层高度就可以满足,这意味着从千万级的表查询目标数据最多需要 3-4 次磁盘 I/O,所以B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4次。
8. 为什么用B+不用B树?如果把内容存在内存上,用B树会不会快一点?
数据库索引通常存储在磁盘上,而 B+ 树的结构设计对磁盘 IO 更友好:
-
B+ 树的 非叶子节点只存储索引键,不存储实际数据(行记录),因此相同大小的节点可以容纳更多索引项,树的高度更低(通常 3-4 层),查询时需要访问的磁盘页数更少(IO 次数更少)。
-
B+ 树的 所有数据都存储在叶子节点,且叶子节点之间通过双向链表连接,这使得范围查询(如
WHERE id BETWEEN 100 AND 200)可以直接通过叶子节点的链表顺序遍历,效率远高于 B 树(B 树需要回溯父节点)。
另外,更好地支持顺序访问,数据库经常需要全表扫描或大范围查询,B+ 树的叶子节点链表结构天然支持高效的顺序遍历,而 B 树由于数据分散在各个节点,顺序访问效率低。
理论上,在 纯内存场景 下,B 树可能在某些场景下表现更好,但优势并不明显,甚至可能不如 B+ 树:
-
随机查询的微小优势:B 树的非叶子节点可能直接存储数据,若查询命中非叶子节点,可减少一次内存访问(无需到叶子节点)。但实际中,索引树的高度通常很低(内存中 2-3 层),这种差异对性能影响极小。
-
范围查询的劣势依然存在,即使在内存中,B+ 树的叶子节点链表结构对范围查询的支持仍优于 B 树。B 树进行范围查询时,仍需频繁回溯父节点,逻辑更复杂,效率更低。
9. C++ 中 struct和Class区别是什么?
在 C++ 中,struct 和 class 是两种用于定义自定义数据类型的关键字,它们的核心功能相似(都能包含成员变量和成员函数),但存在一些关键区别,主要体现在默认访问权限和默认继承方式上:
-
默认访问权限不同:
struct默认访问权限为public,class默认访问权限为private。比如:
struct A {
int x; // 默认 public,外部可直接访问
void f() {} // 默认 public,外部可调用
};
class B {
int y; // 默认 private,外部无法直接访问
void g() {} // 默认 private,外部无法直接调用
};
int main() {
A a;
a.x = 10; // 合法(struct 成员默认 public)
a.f(); // 合法
B b;
b.y = 20; // 编译错误(class 成员默认 private)
b.g(); // 编译错误
return 0;
}
-
默认继承方式不同:当使用继承时,两者的默认继承权限也不同,
struct默认继承方式为public继承,class默认继承方式为private继承。比如:
struct Base {
int x;
};
// struct 默认 public 继承:Base 的 public 成员在 Derived1 中仍为 public
struct Derived1 : Base {
// 可直接访问 Base::x,外部也可通过 Derived1 对象访问 x
};
// class 默认 private 继承:Base 的 public 成员在 Derived2 中变为 private
class Derived2 : Base {
// 可直接访问 Base::x,但外部无法通过 Derived2 对象访问 x
};
int main() {
Derived1 d1;
d1.x = 10; // 合法(public 继承)
Derived2 d2;
d2.x = 20; // 编译错误(private 继承)
return 0;
}
10. 简单介绍一下C++ STL有哪些容器?
C++ STL(标准模板库)提供了多种容器,用于存储和管理数据,这些容器可分为序列式容器、关联式容器和容器适配器三大类,各自适用于不同场景:

序列式容器:按元素插入顺序存储,元素位置与插入顺序相关,不自动排序。
-
vector:动态数组,内存连续,支持随机访问([]或at()),优点尾插 / 尾删效率高(O (1)),适合频繁访问元素的场景。缺点是中间插入 / 删除效率低(O (n)),扩容时可能重新分配内存。 -
deque:双端队列,内存分段连续,支持首尾高效操作。优点头插 / 头删、尾插 / 尾删效率均为 O (1),可随机访问,适合场景需要在两端频繁操作的场景(如实现队列、栈)。 -
list:双向链表,元素通过指针连接,内存不连续。优点任意位置插入 / 删除效率高(O (1),只需修改指针)。缺点不支持随机访问(访问元素需遍历,O (n)),内存开销较大。 -
array:固定大小数组(C++11 新增),编译时确定大小,内存连续,比原生数组更安全(支持边界检查),但大小不可变。
关联式容器:元素按键(key) 排序存储,支持快速查找(通常 O (log n)),分为有序和无序两类。
有序关联容器(基于红黑树实现)
-
set:存储唯一键值,元素自动按键升序排序,键即值(key=value)。适合场景需要去重且有序的数据集合(如存储不重复的 ID 并排序)。 -
multiset:与set类似,但允许键值重复,其他特性相同。 -
map:存储键值对(key-value),键唯一且自动排序,通过键快速查找值。适合场景键值映射场景(如字典、配置表)。 -
multimap:与map类似,但允许键重复(一个键可对应多个值)。
无序关联容器(C++11 新增,基于哈希表实现)
-
unordered_set/unordered_multiset:**功能同set/multiset,但元素无序,通过哈希表实现,查找、插入、删除平均效率更高(O (1))。适合场景对顺序无要求,但需要快速增删查的场景。 -
unordered_map/unordered_multimap:**功能同map/multimap,无序,基于哈希表,平均操作效率 O (1)。
容器适配器:基于其他容器实现,封装特定接口,提供受限功能。
-
stack:栈,遵循 “后进先出(LIFO)”,仅支持在顶部插入 / 删除 / 访问元素,默认基于deque实现,也可指定vector或list作为底层容器。 -
queue:队列,遵循 “先进先出(FIFO)”,仅支持在尾部插入、头部删除。默认基于deque实现,也可指定list作为底层容器。 -
priority_queue:优先队列,元素按优先级自动排序(默认最大元素在顶部),插入 / 删除效率 O (log n)。默认基于vector实现,底层用堆结构维护优先级。
11. C++ 的map是线程安全的么?
不是线程安全的。如果多个线程同时对 std::map 进行操作(尤其是写操作,如插入、删除、修改元素),可能会导致未定义行为(例如数据损坏、迭代器失效、程序崩溃等)。
具体来说,线程不安全主要体现在:
-
并发写操作:多个线程同时执行插入(
insert)、删除(erase)或修改(如operator[]赋值)时,会破坏std::map内部的红黑树结构(有序关联容器的底层实现),导致数据错乱。 -
读写并发:即使一个线程读、另一个线程写,也可能出现问题。例如,读线程正在遍历
map时,写线程修改了结构,可能导致读线程的迭代器失效,触发不可预知的错误。 -
无内置同步机制:
std::map没有提供任何锁(如互斥量)或原子操作来保证多线程安全,所有同步逻辑需要开发者手动实现。
如果要多线程安全使用map,需通过外部同步机制保证线程安全,常见方式:
-
加锁保护:使用
std::mutex或std::lock_guard等同步工具,确保同一时间只有一个线程能访问或修改map。例如:
#include <map>
#include <mutex>
std::map<int, int> my_map;
std::mutex mtx; // 互斥锁
// 线程安全的插入操作
void safe_insert(int key, int value) {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁/解锁
my_map[key] = value;
}
// 线程安全的查找操作
int safe_find(int key) {
std::lock_guard<std::mutex> lock(mtx);
auto it = my_map.find(key);
return (it != my_map.end()) ? it->second : -1;
}
-
使用线程安全的替代容器:某些库(如 C++17 后的
concurrent_unordered_map非标准扩展,或第三方库如 Intel TBB)提供了线程安全的哈希表 / 映射容器,可减少手动加锁的开销。
12. 队列和栈有什么区别?
主要区别在于元素的插入和删除方式以及元素的访问顺序。
插入和删除方式:
-
队列:队列采用先进先出(FIFO)的方式,即新元素插入队尾,删除操作发生在队首。
-
栈:栈采用后进先出(LIFO)的方式,即新元素插入栈顶,删除操作也发生在栈顶。
元素的访问顺序:
-
队列:队列的元素按照插入的顺序进行访问,先插入的元素先被访问到。
-
栈:栈的元素按照插入的顺序进行访问,但是最后插入的元素先被访问到。
队列适用于需要按照插入顺序进行处理的场景,例如任务调度;

而栈适用于需要维护最近操作状态的场景,例如函数调用。

更多推荐


所有评论(0)