小红书 c++后端开发最新面试题
1. Redis是怎么做到高性能的?
Redis 就是靠 “内存快”+“单线程省麻烦”“+“多路复用提效率”+“数据结构和协议够精简”,把性能做到了极致。
-
纯内存操作:数据直接存在内存里,而不是像数据库那样存在硬盘。内存的读写速度比硬盘快成百上千倍(比如内存读写毫秒级,硬盘可能是秒级),这是 Redis 快的 “底子”。
-
单线程模型:Redis 用一个主线程处理所有请求,避免了多线程切换的开销(比如线程间的锁竞争、上下文切换)。虽然单线程听起来 “笨”,但因为内存操作足够快,而且 Redis 把耗时的操作(比如持久化)交给了后台线程,主线程不会被拖累。
-
丰富的数据结构:Redis 的核心数据结构(比如字符串、哈希、列表)都是基于 C 语言自己实现的 “精简版”,没有多余功能,操作效率极高。例如:字符串用 “简单动态字符串”(SDS),比 C 语言原生字符串更省内存、更方便修改;哈希表用链地址法解决冲突,还会自动扩容缩容,保证查询效率稳定。
-
I/O多路复用:虽然是单线程,但 Redis 用了 “IO 多路复用” 技术(比如 Linux 的 epoll),能同时监听多个客户端的网络请求,不用等一个请求处理完再处理下一个,相当于 “单线程干多线程的活”,还没多线程的麻烦。
2. HTTP和RPC的区别是什么?
HTTP 是通用的数据传输协议,格式固定,用途广;RPC 是专门用于远程函数调用的工具,调用方式简单,性能更好,场景更专一。
我觉得主要有下面4点区别:
1、用途不一样
-
HTTP 是一种通用的网络传输协议,能传各种数据(文本、图片、JSON 等),不管两边是什么程序(浏览器、App、服务器都行),只要按它的格式发数据就能通信。比如浏览器打开网页、手机 App 刷新闻,基本都用 HTTP。
-
RPC 是专门用来让程序调用远程函数的工具,比如你的代码里,A 服务器的函数要调用 B 服务器的函数(就像调用本地函数一样),这时候用 RPC 更方便。它只关心 “程序之间的函数调用”,场景更窄。
2、调用方式不一样
-
HTTP 调用时,你得手动拼请求格式(比如 URL、请求头、JSON 数据),接收方也要手动解析这些格式。比如你调一个登录接口,得写
http://xxx/login,然后在请求体里塞{"user":"xxx"}。 -
RPC 调用时,你直接写代码调用函数就行(比如
remoteServer.add(1,2)),不用管格式,底层会自动把参数传给远程,再把结果拿回来。就像调用自己代码里的函数一样自然。
3、数据格式和性能不一样
-
HTTP 协议格式固定且冗余(比如带一堆请求头、状态码),传输时数据量稍大,解析起来也稍慢。
-
RPC 可以用更精简的协议(比如自定义二进制格式),数据传输时 “包袱” 少,解析快,性能通常比 HTTP 好。尤其在系统内部模块频繁调用时,差距更明显。
4、依赖的技术不一样
-
HTTP 基于 TCP 协议,但它自己定义了一套完整的规范(比如请求方法、状态码),是独立的协议。
-
RPC 更像一种 “实现思路”,底层可以用 TCP,也可以用 HTTP,甚至其他协议,核心是 “远程函数调用” 这个功能。
3. 虚拟内存用户态的地址空间怎么分配的
用户态地址空间是一块连续的虚拟地址范围(比如 32 位系统通常是 0~3GB,64 位系统范围更大),按 “从低到高” 分成几块,每块有专门用途,互不干扰。

-
代码段,包括二进制可执行代码;
-
数据段,包括已初始化的静态常量和全局变量;
-
BSS 段,包括未初始化的静态变量和全局变量;
-
堆段,包括动态分配的内存,从低地址开始向上增长;
-
文件映射段,包括动态库、共享内存等
-
栈段,包括局部变量和函数调用的上下文等,比如函数里定义的
int x = 5,就在栈里。栈的大小是固定的,一般是 8MB 。当然系统也提供了参数,以便我们自定义大小;
4. 介绍Go的 GMP模型?
Go 的 GMP 模型是 Go 语言实现并发并发的核心机制,目的是高效地把任务分配给 CPU 执行,由三个核心部分组成:

-
G(Goroutine):就是 Go 里的协程,是实际执行的任务单元(比如一段代码、一个函数)。每个 G 都很轻量,占用内存少(KB 级别),创建和销毁成本低。它需要被调度到 CPU 上才能运行,自身会记录执行状态(比如当前执行到哪行代码、局部变量等)。
-
M(Machine):对应操作系统的线程(OS Thread),是真正执行代码的 “物理载体”。M 会绑定一个 CPU 核心,负责执行被分配给它的 G。但 M 不能直接跑 G,必须通过 P 来关联。
-
P(Processor):可以理解为 “调度器” 或 “资源管理者”,是连接 G 和 M 的中间层。每个 P 都持有一个 “本地队列”,里面放着等待执行的 G。P 还会管理一些资源(比如内存缓存),只有和 P 绑定的 M,才能从 P 的本地队列里取 G 来执行。系统里 P 的数量默认等于 CPU 核心数(可通过环境变量
GOMAXPROCS调整),避免过多线程导致的 CPU 切换开销。
核心工作流程:当你用 go func() 创建一个 G 时,它会被放到某个 P 的本地队列里,每个 P 会绑定一个 M,M 不断从 P 的本地队列里取 G 执行。如果某个 P 的本地队列空了,它会去其他 P 的队列 “偷” G 来执行(负载均衡)。当 G 执行到阻塞操作(比如读写文件、网络请求)时,M 会暂时和 P 解绑,去执行其他就绪的 G;等阻塞操作完成,这个 G 会重新回到队列等待执行。
简单说,GMP 模型通过 P 协调 G 和 M,让大量轻量的 G(协程)高效地利用系统线程(M)和 CPU 核心,既保证了并发的灵活性,又减少了线程切换的开销,让 Go 的并发性能很好。
5. 协程为什么比线程快?
协程比线程快,核心原因是协程的切换和管理成本远低于线程,具体体现在这几点:
-
切换不涉及内核:线程切换需要操作系统内核介入(保存 / 恢复线程的寄存器、栈指针等核心状态),这个过程要陷入内核态,开销很大。而协程切换是在用户态完成的,由程序自己(或协程库)控制,只需要保存少量局部状态(比如当前执行位置、局部变量),不需要内核参与,速度快得多。
-
资源占用少:线程需要操作系统分配独立的栈空间(通常几 MB)和其他内核资源(如线程控制块)。协程的栈可以很小(甚至动态调整,KB 级别),且不需要内核资源,同样的内存能容纳更多协程,管理起来更轻量。
-
调度更高效:线程调度由操作系统内核完成,要考虑全局资源分配、优先级等,调度策略复杂,开销高。协程调度由用户程序自己控制(比如在 IO 操作时主动让出 CPU),调度逻辑更简单直接,不需要复杂的内核决策,耗时更少。
协程是 “用户自己管的轻量单元”,线程是 “操作系统管的重量单元”,前者的切换和管理少了内核介入的开销,所以更快。
6. C++空类的大小是多少?
C++ 空类的大小是 1 字节。
原因很简单:C++ 规定,任何对象都必须有一个唯一的内存地址。如果空类大小为 0,那么当创建这个类的多个对象时,它们会共享同一个地址,这违反了 “每个对象地址唯一” 的规则。所以编译器会给空类隐式分配 1 字节的空间,目的就是为了让这个类的每个实例都能拥有独一无二的内存地址。
class Empty {};
Empty e1, e2; // e1和e2的地址不同,因为每个占1字节
但要注意,如果空类被继承,或者包含成员变量 / 虚函数,大小就会根据实际内容变化,不再是 1 字节了。
7. C++菱形继承会导致什么问题?
C++ 菱形继承会导致两个主要问题
-
数据冗余:菱形继承是指 "子类同时继承两个父类,而这两个父类又继承自同一个基类" 的情况(像个菱形)。这时最顶层的基类数据会在最底层的子类中存在两份(分别来自两个父类的继承路径),造成内存浪费。比如下面代码,这时
D对象里会有两个x,既多余又容易混乱。
class A { int x; };
class B : public A {}; // B继承A的x
class C : public A {}; // C继承A的x
class D : public B, public C {}; // D里会有两个x(来自B和C)
-
二义性问题:当子类访问最顶层基类的成员时,编译器不知道该用哪条继承路径来的成员,会直接报错。比如上面的
D类,如果写d.x,编译器会困惑:"你要访问的是从 B 继承的 x,还是从 C 继承的 x?",导致编译失败。
解决办法是用虚继承(在继承时加virtual关键字),让最顶层基类在子类中只保留一份数据,消除冗余和二义性。
8. C++ 静态变量的使用场景是什么?未初始化的全局静态变量呢?
静态变量(包括全局静态、局部静态、类静态成员)的核心特点是生命周期贯穿程序运行始终,且作用域受限定,常见使用场景如下:
全局静态变量(static 修饰的全局变量)
-
作用:限制变量仅在当前文件内可见(避免不同文件中同名变量冲突),但生命周期是整个程序运行期间。
-
场景:当多个文件需要独立使用同名变量(如统计各模块的内部计数),但不希望被其他文件访问或修改时。例:
static int count = 0;(仅当前.cpp文件可访问,其他文件即使声明extern int count也无法使用)。
局部静态变量(函数内的 static 变量)
-
作用:变量在函数第一次调用时初始化,后续调用不再重新初始化,值会被保留(生命周期全局,作用域仅限函数内)。
-
场景:记录函数被调用的次数(如
static int call_count = 0; call_count++;);单例模式中,确保全局只存在一个实例(如函数内返回静态对象的指针);避免频繁创建销毁临时对象(如工具函数中复用的缓冲区)。
类静态成员变量(static 修饰的类成员)
-
作用:属于整个类而非某个对象,所有对象共享该变量,生命周期全局,需在类外单独初始化。
-
场景:统计类的实例数量(如
static int total;,在构造函数中total++,析构函数中total--);存储类级别的常量或共享配置(如static const int MAX_SIZE = 100;)。
未初始化的全局静态变量
未初始化的全局静态变量(如 static int a;)有两个关键特性:
-
自动初始化:编译器会将其默认初始化为 0(包括数值类型为 0,指针类型为
nullptr等)。 这是因为全局静态变量存放在内存的 BSS 段(未初始化数据段),程序启动时系统会自动将该段所有数据清零。 -
作用域限制:和初始化的全局静态变量一样,仅在当前文件内可见,不影响其他文件的同名变量。
例:
// file1.cpp
static int uninit; // 未初始化,默认值为0,仅file1可见
// file2.cpp
static int uninit; // 与file1的uninit无关,各自为0
未初始化的全局静态变量本质上是 “带文件作用域的零初始化全局变量”,适合需要跨函数(但仅限当前文件)共享、且初始值为 0 的场景。
9. C++代码异常core dump会生成一个栈,里面内容是什么?
栈内容就是程序崩溃前的 “现场录像”,能帮你顺着调用链找到哪里出了问题,以及当时的变量状态。当时的变量状态。主要包含这些信息:
-
函数调用链:从崩溃的那个函数开始,一层层往上列所有调用它的函数。比如 A 调用 B,B 调用 C,C 崩溃了,栈里就会显示 C → B → A 的顺序,能看出 “崩溃是从哪个入口一步步走到这里的”。
-
每个函数的地址:记录每个函数在内存中的具体地址(比如
0x400520),结合编译时的符号表(如果没被 strip 掉),就能对应到具体是哪个函数(比如main()、func())。 -
函数的参数和局部变量:每个函数调用时传入的参数值、以及函数内部定义的临时变量(比如
int a=5中的a),会按栈的顺序保存。这能帮你排查 “是不是参数传错了”“变量值是不是异常了”。 -
栈指针位置:记录崩溃时栈顶的位置,告诉你当前栈用到了哪里,有没有溢出(比如栈被写满了)。
10. 手撕算法
-
两个单调递增序列的中位数,时间复杂度要求O(log(m+n))
更多推荐



所有评论(0)