基础
1.变量的声明和定义有什么区别
分配内存大小
2.sizeof 和 strlen 的主要区别是什么?
1.运算符,库函数
2.计算内存大小,计算字符串大小
3.编译时计算,运行时计算
3.简述 C/C++ 程序编译时的内存分配情况
- 内存分配主要涉及 程序的内存布局。
- 编译时:规划内存布局,确定所属的段(Text/Data/BSS),但不分配实际内存
- 运行时:操作系统根据编译/链接结果加载程序,动态分配堆和栈内存
- 内存分为静态内存和动态内存,静态内存在程序启动时分配,生命周期持续到程序结束,动态内存在运行时分配,栈自动管理,堆需手动释放。
编译时的关键行为:
- 1.符号表的生成
- 编译器收集变量/函数地址的信息(如全局变量Data/BSS),但尚未分配实际内存地址。如int global_val =10;会被标记为“已初始化全局变量”,放入DATA段
- 2.内存对齐计算
- 编译器根据平台规则计算在内存中布局,优化访问速度
- 3.链接阶段的内存分配
- 地址绑定:连接器将多个目标文件的段合并,例如合并所有目标文件的DATA段,并确定全局变量的最终地址。
1.c++程序内存布局:
Text(代码段):程序指令
Data 数据段:初始化的全局变量和static变量
BSS段 :未初始化的全局变量和static变量
堆:动态分配的内存(new/malloc)
栈: 局部变量、函数参数、函数返回值
- 请谈谈你对拷贝构造函数和赋值运算符的理解
- 拷贝构造函数:在对象创建时通过另一个对象来初始化新对象。默认执行浅拷贝。
- 赋值运算符:用于将已存在对象的状态赋值给另一个已存在对象
- 如果对象的成员是指针类型,那么仅复制指针的值(即地址),而不是指针指向的内容,这样会导致 浅拷贝
- 拷贝构造函数和赋值运算符都需要特别处理深拷贝,避免浅拷贝导致的内存问题
5.左值、右值、左值引用、右值引用、std::move(移动语义)、完美转发
左值:表示一个由明确地址的对象,可以用它修改对象的值。生命周期持久性
右值:临时对象、字面量、函数返回值。生命周期:通常在表达式结束后销毁
左值引用:
右值引用:指向右值的引用。用于支持移动语义和完美转发
std::move:用于将左值转换成右值引用。本身并不移动对象,移动操作由移动构造函数或移动赋值函数实现。避免了拷贝,提升性能。
完美转发:完美转发是一种在模板中保持传递参数的左值/右值性质的技术
templete<typename T>
void test(T &&arg) {
func(std::farword<T>(arg));
}
网络
1.TCP三次握手
三次握手通过 SYN、SYN+ACK、ACK 三次交互,确保双方收发能力和序列号同步,防止历史连接干扰,可靠建立双向连接。
- 第一次握手(SYN): 客户端向服务器发送一个SYN(同步)包,表示请求建立连接。此时,客户端进入SYN_SEND状态,等待服务器确认。
- 第二次握手(SYN-ACK): 服务器收到客户端的SYN包后,向客户端发送一个SYN-ACK包,表示同意建立连接,并同步自己的序列号。此时,服务器进入SYN_RECV状态。
- 第三次握手(ACK): 客户端收到服务器的SYN-ACK包后,发送一个ACK(确认)包,确认服务器的同步号。此时,客户端进入ESTABLISHED状态,服务器收到后也进入ESTABLISHED状态,双方建立起连接。
三次握手的目的是确保双方都能同步并确认对方的序列号,从而在数据传输过程中确保可靠性和数据完整性。如果某个环节未能完成,连接无法成功建立。
-
为什么三次?
- 两次无法防止失效的SYN请求(网络延迟导致重复连接)。
- 三次确保双方收发能力正常,且序列号同步。
2.TCP 四次挥手
目的:安全关闭双向连接,确保双方数据发送完毕,避免数据丢失或资源泄漏。
过程(假设客户端主动关闭):
-
FIN(客户端→服务端)
- 客户端发送
FIN=1(结束标志)、seq=u,进入FIN_WAIT_1状态,表示客户端不再发送数据。
- 客户端发送
-
ACK(服务端→客户端)
- 服务端收到
FIN后,回复ACK=1、ack=u+1,进入CLOSE_WAIT状态,表示已收到关闭请求,但可能仍有数据要发送。 - 客户端收到
ACK后进入FIN_WAIT_2状态。
- 服务端收到
-
FIN(服务端→客户端)
- 服务端处理完剩余数据后,发送
FIN=1、seq=v,进入LAST_ACK状态,表示服务端也准备关闭。
- 服务端处理完剩余数据后,发送
-
ACK(客户端→服务端)
- 客户端收到
FIN后,回复ACK=1、ack=v+1,进入TIME_WAIT状态(等待2MSL确保服务端收到ACK),服务端收到后直接关闭连接。
- 客户端收到
-
为什么四次?
-
TCP是全双工的,双方需独立关闭:
- 第一次挥手(
FIN)表示客户端不再发数据,但还能收数据。 - 第二次挥手(
ACK)仅确认客户端的FIN,服务端可能仍有数据要发。 - 第三次挥手(
FIN)表示服务端数据发完,也准备关闭。 - 第四次挥手(
ACK)确保服务端能安全关闭。
- 第一次挥手(
-
-
TIME_WAIT的作用(等待
2MSL):TCP 连接中,主动关闭连接的一方出现的状态, 收到对方的 FIN 请求后,发送 ACK 响应,会处于 time_wait 状态
- 确保最后一个
ACK到达服务端(若丢失,服务端会重发FIN)。 - 让网络中残留的旧数据包失效,避免影响新连接。
TCP 连接占用的端口,无法被再次使用,会导致新建 TCP 连接会出错,address already in use : connect 异常 TCP 四次挥手关闭连接机制中,为了保证 ACK 重发和丢弃延迟数据,设置 time_wait 为 2 倍的 MSL
解决办法,服务器端 允许 time_wait 状态的 socket 被重用,so_reuseaddr选项 缩减 time_wait 时间,设置为 1 MSL
- 确保最后一个
MSL,Maximum Segment Lifetime,“报文最大生存时间” 1.任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。(IP 报文)
一句话总结:
“四次挥手通过 FIN 和 ACK 分阶段关闭双向连接,确保数据完整传输,并通过 TIME_WAIT 防止最后一次 ACK 丢失或旧数据干扰新连接。”
3.TCP 和 UDP对比
1.可靠性 tcp面向连接,保证数据可靠传输(3次握手,重传机制) 发送数据不需要先建立连接,直接发送无连接,数据可能丢失,乱序,重复,不可靠
2.流量控制 tcp有流量控制
3.速度 tcp因为有可靠性保障,相对udp较慢
3.头部开销 TCP有序列号,确认号,20字节以上,UDP头部8字节
4.RPC
RPC(远程过程调用,Remote Procedure Call)是一种允许程序在不同计算机或进程间进行通信的协议,使得客户端能够像调用本地函数一样调用远程服务器上的函数。RPC 的核心思想是抽象了网络通信的细节,使得分布式系统中的不同组件可以透明地进行交互。具体工作流程如下:
- 客户端调用本地代理函数:客户端发起一个远程调用请求,实际上是调用一个本地的代理函数(也叫存根),这个函数和远程函数接口完全一致,客户端程序认为它在调用本地函数。
- 代理函数封装请求:代理函数将调用信息(参数、方法名等)打包成一个请求,发送到网络上的远程服务器。
- 服务器接收请求并执行:远程服务器上的相应函数接收到客户端请求后,解包并执行实际的远程函数。执行结果返回给服务器端的代理函数。
- 代理函数返回结果:服务器端的代理函数将执行结果封装后,通过网络返回给客户端的代理函数。
- 客户端接收结果:客户端接收到返回结果后,继续执行后续操作,用户不需要关心远程调用的具体实现。
RPC 的优势在于其简化了分布式系统的通信开发,程序员无需处理底层的网络通信、数据序列化与反序列化等复杂过程。常见的 RPC 框架包括 gRPC、Apache Thrift、Dubbo 等。通过 RPC,开发者能够轻松实现跨机器、跨语言的服务调用,从而提高系统的可扩展性与灵活性。
5.大端和小端
计算机存储多字节数据(如 int, float)时,有两种排列方式:大端序(Big-Endian) 和 小端序(Little-Endian)。它们的出现主要源于硬件设计的历史差异:
大端序(Big-Endian):
高字节在前(类似人类书写习惯,如 0x1234 存储为 12 34)。
早期网络协议(如 TCP/IP)和部分处理器(如 PowerPC、早期的 ARM)使用。
小端序(Little-Endian):
低字节在前(如 0x1234 存储为 34 12)。
现代主流 CPU(x86、ARM)默认使用,因其硬件设计更高效。
如何存储?
以 32 位整数 0x12345678 为例:
字节序 内存地址增长方向 →
大端序 12 34 56 78(高字节在前)
小端序 78 56 34 12(低字节在前)
- 大端序:像大人物一样高调(高字节在前)。
- 小端序:像小人物一样低调(低字节在前)。
网络字节序(大端序),是网络协议的标准字节序,确保数据在不同平台之间传输时,字节序的统一性。
网络协议(如TCP/IP)在设计时采用了大端序作为数据传输的标准,Python 的 struct 模块默认使用本机的字节顺序来编码和解码数据,通常是小端序。
发送:如果是小端序,转网络字节序(大端序),接收端转换回主机字节序,再解包 int main() {
unsigned int num = 1;
unsigned char *ptr = reinterpret_cast<unsigned char*>(&num);
//使用指针 ptr 指向 num 的地址,并将其强制转换为 unsigned char*,这样我们可以逐字节访问变量 num。
if (*ptr == 1) {
std::cout << "小端模式" << std::endl;
} else {
std::cout << "大端模式" << std::endl;
}
//----------
union test
{
char c;
int i;
}
test t;
t.i = 1;
if(t.c == 0)
{
//0000 0001 =》 小端0100 0000,大端0000 0001
return 小端。
}
// 网络上传输的数据都是字节流,UDP/TCP/IP协议规定:把接收到的第一个字节当作高位字节看待,所以说,网络字节序是大端字节序
return 0;
}
6.IO
IO:输入/输出,IO操作
1.磁盘IO:数据读取,数据写入
2.网络IO:数据请求,数据发送
IO操作->2个阶段:
1.用户进程空间->内核空间
2.内核空间->用户进程空间
LINUX中进程无法直接操作I/O设备,其必须通过系统调用请求kernel来协助完成I/O动作;内核会为每个I/O设备维护一个缓冲区
Linux 中的 I/O 设备分类 在 Linux 中,"每个 I/O 设备"主要指的是所有能够进行输入/输出操作的硬件设备及其抽象,包括但不限于以下类型:
设备类型 示例设备文件 典型用途
块设备 /dev/sda, /dev/nvme0n1 硬盘、SSD、USB存储等
字符设备 /dev/tty, /dev/input/mice 键盘、鼠标、串口等
网络设备 eth0, wlan0 (无设备文件) 网卡
虚拟设备 /dev/null, /dev/zero 特殊用途
内核为不同设备维护的缓冲区:
- 块设备缓冲区
- 字符设备缓冲区
- 网络设备缓冲区(发送缓冲区:sk_write_queue,接收缓冲区:sk_receive_queue)
对于一个输入操作来说,进程IO系统调用后,内核会先看缓冲区中有没有相应的缓存数据,没有的话再到设备中读取,因为设备IO一般速度较慢,需要等待;内核缓冲区有数据则直接复制到进程空间。
缓存数据指的是内核将最近访问过的磁盘数据保留在内存中的副本,主要包括: 文件内容数据 文件系统元数据(如inode、目录结构等) 磁盘块数据
7.5种IO模型
1. 阻塞IO
- 特点:调用
read/recv时线程全程挂起,直到数据就绪并拷贝完成。 - 缺点:并发能力差(一请求一线程)。
2. 非阻塞IO
- 特点:调用立即返回
EWOULDBLOCK,线程需轮询检查就绪状态,就绪后仍需同步拷贝数据。 - 缺点:CPU空转消耗资源。
3. IO多路复用(如select/poll/epoll)
- 特点:单线程监听多个fd,就绪后再调用
read同步拷贝(仍是同步IO)。 - 优势:高并发场景核心方案(如Redis/Nginx)。
4. 信号驱动IO(如SIGIO)
- 特点:内核通过信号通知数据就绪,但拷贝阶段仍需线程同步处理。
- 缺点:信号处理复杂,实际应用少。
5. 异步IO(如io_uring/IOCP)
- 特点:内核全权处理IO,数据就绪并拷贝完成后回调通知,全程无阻塞。
- 优势:真正异步,性能天花板(如Windows IOCP、Linux io_uring)。
关键对比:
- 同步IO:前4种(最终需线程主动参与数据拷贝)。
- 异步IO:仅第5种(内核全自动处理)。
一句话总结:
“五种IO模型按阻塞程度递进,从完全阻塞(阻塞IO)到完全异步(AIO),其中IO多路复用是同步高并发的核心方案,而异步IO是性能终极形态。”
阻塞IO和非阻塞IO区别?
a.阻塞IO:在阻塞IO模型中,当应用程序发起一个IO操作(例如读取文件或网络数据),如果数据没有准备好,该操作会一直阻塞线程的执行。线程会等待,直到数据准备好后才能继续执行后续的代码。这意味着在进行阻塞IO操作期间,线程无法执行其他任务。
b.非阻塞IO:相比之下,非阻塞IO模型允许应用程序在发起一个IO操作后立即返回,并且无论数据是否准备好都可以继续执行后续的代码。如果数据还没有准备好,非阻塞IO模型会立即返回一个错误码或者空数据。应用程序需要通过轮询或者重复尝试来检查数据是否已经准备好。
同步IO和异步IO区别?
同步IO和异步IO区别?
-
同步IO(Synchronous IO):在同步IO模型中,应用程序发起一个IO操作后会被阻塞等待操作完成。这意味着应用程序必须等待IO操作完成后才能继续执行后续代码。当数据就绪时,系统将数据传输给应用程序并解除阻塞状态,应用程序可以读取或写入数据。同步IO通常以函数调用的形式进行,例如读取文件、网络请求等。
-
异步IO(Asynchronous IO):在异步IO模型中,应用程序发起一个IO操作后立即返回,并不会阻塞等待结果返回。应用程序可以继续执行后续代码而无需等待。当数据就绪时,系统会通知应用程序进行读取或写入操作,并提供相应的回调函数或事件处理机制来处理完成通知。异步IO常见的实现方式包括回调函数、Promise/Future对象、协程/生成器等。
-
关键区别:
- 同步IO需要阻塞等待结果返回,而异步IO则不需要。
- 同步IO只能进行一次性的单个操作,而异步IO可以同时处理多个任务。
- 同步IO适合于简单和可预测的IO操作,而异步IO适合于需要同时处理大量任务和高并发的场景
同步IO和阻塞IO区别?
阻塞IO:是同步IO的一种特例,调用时会一直等待数据就绪(全程阻塞线程)
同步IO:包含阻塞IO和非阻塞IO+轮询(如select/poll),最终仍需线程主动等待数据拷贝完成
关键对比:
- 阻塞IO:调用recv()时,从开始等待到数据拷贝完成全程挂起线程
- 非阻塞IO+轮询(同步非阻塞) :调用recv()立即返回EWOULDBLOCK,线程需轮询检查就绪状态
- 共同点:数据从内核到用户空间的拷贝阶段,线程都必须同步等待
典型场景:
- 阻塞IO:传统Socket默认模式
- 同步非阻塞IO:Nginx使用的epoll边缘触发模式
一句话总结:
"阻塞IO是同步IO的子集,区别在于是否立即返回。所有阻塞IO都是同步的,但同步IO不一定全程阻塞(如非阻塞IO+轮询)。"
异步IO和非阻塞IO区别?
- 非阻塞IO需要应用程序自行查询和处理状态变化, 而异步IO则由操作系统负责监测和通知状态变化。
- 非阻塞IO(同步非阻塞):调用后立即返回(成功或
EWOULDBLOCK),但数据就绪后仍需线程主动拷贝数据(同步操作)。
- 异步IO(如
io_uring/IOCP):内核完全接管IO操作,数据就绪后通过回调/信号通知,全程无需线程参与等待(真正的异步)。
关键对比:
| 特性 | 非阻塞IO | 异步IO |
|---|---|---|
| 调用返回 | 立即返回(需轮询) | 立即返回(无需轮询) |
| 数据就绪 | 线程主动检查/拷贝 | 内核自动回调通知 |
| 线程阻塞 | 拷贝阶段仍可能阻塞 | 全程无阻塞 |
典型场景:
- 非阻塞IO:
epoll边缘触发(仍需recv同步拷贝) - 异步IO:
io_uring(Linux)、IOCP(Windows)
一句话总结:
“非阻塞IO仍需线程主动处理数据拷贝(同步),而异步IO由内核全权处理并通过回调通知,实现真正的无阻塞。”
epoll
epoll通过红黑树管理fd、就绪链表通知事件、回调机制触发更新,实现了高效的事件驱动模型,成为Linux高并发网络编程的核心机制。
1. 性能维度
select:采用1024位固定fd_set,O(n)轮询,适合低并发(<1000连接)poll:动态fd数组,突破select数量限制,但仍是O(n)轮询epoll:红黑树管理fd+就绪链表,O(1)事件通知,支持百万级并发
2. 触发机制
select/poll:仅支持水平触发(LT),未处理事件会持续通知epoll:支持LT和边缘触发(ET),ET模式减少无效事件(如Nginx使用ET)
3. 内存效率
select:每次调用需重置fd_set,全量拷贝到内核poll:需传递整个pollfd数组epoll:内核维护数据结构,仅返回就绪事件
4. 使用复杂度
select:需处理FD_ISSET等宏,代码冗余poll:统一pollfd结构,接口更简洁epoll:分离epoll_ctl(注册)和epoll_wait(等待),扩展性强
5. 跨平台性
select:所有平台支持poll:多数Unix-like系统支持epoll:Linux特有(其他系统有类似kqueue)
一句话总结:
"select受限于fd数量和性能,poll改进数量限制但仍是轮询,epoll通过红黑树和就绪链表实现O(1)事件通知,成为Linux高并发首选方案,ET模式进一步提升性能。"