C++知识总结
作者:爱写代码的刚子
时间:2024.10.6
前言:这是对C++知识的总结
认识volatile吗?
由于访问寄存器要比访问内存单元快的多,编译器在存取变量时,为提高存取速度,编译器优化有时会先把变量读取到一个寄存器中;以后再取变量值时就直接从寄存器中取值。但在很多情况下会读取到脏数据,严重影响程序的运行效果。
volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
- 一个参数既可以是const还可以是volatile吗?
可以的,例如只读的状态寄存器。它是volatile因为它可能被意想不到地改变(多线程、硬件)。它是const因为程序不应该试图去修改它。
- 一个指针可以是volatile 吗?
可以,当一个中服务子程序修改一个指向buffer的指针时。
-
volatile int* ptr; 表示指针指向的值是 volatile 的,值可能会被外部修改。
-
int* volatile ptr; 表示指针本身是 volatile 的,指针的值(地址)可能会被外部修改。
讲一下虚函数和多态
虚函数
虚函数是基类中声明的函数,旨在允许派生类对其进行重写(或覆盖)。通过将函数声明为虚函数,可以确保在使用基类指针或引用时,调用的是派生类中实际实现的函数,而不是基类中的函数。
这种行为称为动态绑定 或运行时多态。
动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定,编译时),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定,运行时),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
多态
多态性使得相同的接口可以针对不同类型的对象执行不同的行为,从而实现更灵活和可扩展的代码设计。
在 C++ 中,多态分为编译时多态 和运行时多态两种形式。
1. 静态多态
静态多态:是在编译过程中确定的多态形式,也叫做早期绑定 。它通过函数重载 和模板实现。
-
函数重载是指同一个函数名可以有多个不同的实现,依据参数的数量或类型来区分调用哪个函数。
-
模板提供了一种机制,可以编写类型无关的通用代码,在编译时确定具体类型,从而实现多态性。
2. 动态多态
运行时多态是在程序运行时确定的多态形式,也叫做晚期绑定。它是通过继承和虚函数实现的。运行时多态允许基类的指针或引用根据指向的派生类对象,调用派生类中的重写方法。
- **虚函数:**虚函数是基类中声明的,允许在派生类中被重写的函数。它确保在使用基类指针或引用时,调用的是派生类的实际实现,而不是基类的实现。
- 纯虚函数与抽象类:如果基类中的某个函数是纯虚函数 (= 0),那么基类称为抽象类,不能创建实例,派生类必须实现纯虚函数。抽象类通常用于定义接口或通用行为。
实现多态的条件
• 必须有继承:派生类继承自基类。
• 基类中的方法必须声明为虚函数(virtual)。
• 使用基类指针 或引用来指向派生类对象。
相关博客(重要)
构造函数可以是虚函数吗?
原因:
1. 对象初始化顺序:在构造派生类对象时,首先调用基类的构造函数,接着才是派生类的构造函数。在基类构造函数执行时,派生类部分还没有被初始化,也没有构建虚函数表。如果允许构造函数为虚函数,那么调用虚函数时可能访问到未初始化的派生类部分,这会导致未定义行为。
2. 虚函数表的创建:虚函数表是在构造过程中创建的,只有在基类构造函数完全执行后,才会初始化派生类的虚函数表。因此,构造函数无法使用虚函数机制。
虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它
但析构却不一定,我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
介绍一下锁?以及死锁
锁是一种用于管理并发访问共享资源的机制。在多线程编程中,多个线程可能会同时访问和修改共享资源,这会引发竞争条件,导致不一致的结果。锁通过协调线程对资源的访问,确保一次只有一个线程可以访问或修改共享资源,从而避免数据冲突和不一致。
死锁的概念
死锁(Deadlock)是指在多线程或多进程环境中,多个线程或进程因相互等待对方释放资源,导致所有线程或进程都无法继续执行的一种状态。简单来说,死锁是一种"僵局",在这种状态下,每个线程都在等待其他线程释放资源,而这些资源又被其他线程占有,导致所有相关线程都无法前进。
死锁的四个必要条件
死锁的发生通常需要满足以下四个条件,称为死锁的必要条件:
1. 互斥条件(Mutual Exclusion):
资源是以互斥的方式使用的,即每次只有一个线程可以使用某个资源。如果一个线程获得了资源,其他线程必须等待该资源被释放。
2. 持有并等待条件(Hold and Wait):
一个线程持有一个资源的同时,还在等待其他线程持有的资源。即线程在持有了某些资源的情况下,又去请求其他资源,而这些资源当前正被其他线程占用。
3. 不可剥夺条件(No Preemption):
资源不能被强行从持有它的线程中剥夺,只有持有该资源的线程能主动释放它。
4. 循环等待条件(Circular Wait):
存在一个线程等待链,链中的每个线程都在等待下一个线程所持有的资源。即形成了一个闭环,导致线程之间相互等待,无法继续执行。
5种I/O模型
在网络编程和操作系统中,I/O模型决定了应用程序如何与内核进行交互以执行输入输出操作。主要有5种I/O模型,它们的区别在于应用程序处理I/O的方式以及如何管理阻塞状态。以下是5种常见的I/O模型:
1. 阻塞 I/O(Blocking I/O)
工作原理:
• 应用程序发起一个 I/O 操作(如 recvfrom),在系统完成该操作之前,应用程序会一直被阻塞,不能执行其他操作。
• 一旦数据到达或操作完成,I/O 函数返回结果,应用程序才会继续执行后续代码。
特点:
• 简单,易于理解和实现。
• 线程在等待数据期间会被阻塞,资源利用效率较低。
示例:
cpp
char buffer[1024];
int n = recvfrom(sock, buffer, sizeof(buffer), 0, NULL, NULL);
这里的 recvfrom 会阻塞,直到接收到数据。
适用场景:
• 适用于对实时性要求不高的场景。
• 但如果大量 I/O 操作会使程序效率低下。
2. 非阻塞 I/O(Non-blocking I/O)
工作原理:
• 应用程序发起 I/O 操作(如 recvfrom),如果数据没有准备好,系统立即返回一个错误(如 EWOULDBLOCK),而不会阻塞。
• 应用程序可以在等待数据到达期间执行其他操作,需要多次调用 I/O 函数来轮询数据是否可用。
特点:
• 应用程序不会阻塞,可以做其他工作。
• 需要不断检查数据是否准备好,导致 CPU 消耗增加(轮询)。
示例:
cpp
fcntl(sock, F_SETFL, O_NONBLOCK); // 设置为非阻塞模式
char buffer[1024];
int n;
while ((n = recvfrom(sock, buffer, sizeof(buffer), 0, NULL, NULL)) < 0) {
if (errno == EWOULDBLOCK) {
// 数据还没准备好,执行其他任务
} else {
// 其他错误
}
}
适用场景:
• 适用于不希望线程被阻塞的场景,但会引入轮询的问题。
3. I/O 多路复用(I/O Multiplexing)
工作原理:
• I/O 多路复用允许程序同时监视多个文件描述符(如 select 或 poll 函数)。
• 程序可以等待多个套接字或文件描述符上的 I/O 操作准备好(如可读、可写),而不是仅等待一个。
• 当有任何一个描述符准备好时,select 或 poll 返回,程序再处理对应的 I/O。
特点:
• 能够同时管理多个 I/O 操作,避免了阻塞问题。
• 相对于阻塞 I/O 更高效,但 select 和 poll 的实现可能会对大量描述符造成性能瓶颈。
示例(使用 select**)**:
cpp
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sock1, &readfds);
FD_SET(sock2, &readfds);
int maxfd = sock2 + 1;
select(maxfd, &readfds, NULL, NULL, NULL);
if (FD_ISSET(sock1, &readfds)) {
// sock1 可读
}
if (FD_ISSET(sock2, &readfds)) {
// sock2 可读
}
适用场景:
• 适合需要管理多个 I/O 操作的场景,如服务器程序。
4. 信号驱动 I/O(Signal-driven I/O)
工作原理:
• 应用程序首先通过系统调用(如 fcntl)开启文件描述符的信号驱动模式,并为描述符注册一个信号处理函数。
• 当 I/O 操作可以继续时,操作系统会发送一个信号(如 SIGIO)通知应用程序。应用程序在信号处理函数中执行 I/O 操作。
特点:
• 不会阻塞,应用程序可以继续执行其他操作。
• 一旦 I/O 准备好,系统发送信号通知,避免了轮询。
• 但信号处理的编写较为复杂,信号处理的时机也较难控制。
示例:
cpp
void sigio_handler(int signo) {
char buffer[1024];
recvfrom(sock, buffer, sizeof(buffer), 0, NULL, NULL);
// 处理 I/O
}
int main() {
fcntl(sock, F_SETFL, O_ASYNC); // 开启信号驱动 I/O
signal(SIGIO, sigio_handler); // 设置信号处理函数
// 继续其他工作
}
适用场景:
• 信号驱动适合某些实时响应需求的场景,但通常较少使用,因为它复杂且难以调试。
5. 异步 I/O
工作原理:
• 应用程序发起 I/O 请求后,立即返回,不会阻塞。操作系统在后台完成 I/O 操作,当操作完成时,内核通知应用程序(通过回调、信号或其他机制)。
• 与信号驱动 I/O 的区别是,异步 I/O 不仅在 I/O 可进行时通知,还负责完成整个操作。
特点:
• I/O 完全由内核管理,程序无需关注操作何时完成。
• 应用程序在发起 I/O 操作后可以继续处理其他任务,I/O 完成后内核通知应用程序。
• 实现更复杂,且在不同操作系统上的支持有所差异。
适用场景:
• 适用于对性能要求高、希望最大限度提高 I/O 并发的场景,例如高性能服务器或实时系统。
总结
I/O 模型 | 是否阻塞 | 是否轮询 | 适用场景 |
---|---|---|---|
阻塞 I/O | 是 | 否 | 简单场景,低并发需求 |
非阻塞 I/O | 否 | 是 | 程序控制力较强,避免阻塞 |
I/O 多路复用 | 是 | 否(通过 select/poll 等实现) | 处理多个 I/O,较高并发需求 |
信号驱动 I/O | 否 | 否 | 实时响应需求,但实现复杂 |
异步 I/O | 否 | 否 | 高并发需求,最大化并发性能 |
epoll的底层特性
epoll 是 Linux 提供的一种高效 I/O 多路复用机制,适用于处理大量文件描述符。它在性能和可扩展性方面优于传统的 select 和 poll。以下是 epoll 的底层特性:
1. 事件驱动(Event-Driven)模型:
• epoll 基于事件通知机制工作。当文件描述符状态发生变化时(如可读、可写等),内核会通知用户程序。epoll_wait 只会返回那些发生了事件的文件描述符,而无需每次遍历整个描述符列表。
2. 高效的 O(1) 复杂度:
• epoll 的性能与监视的文件描述符数量无关。相比于 select 和 poll 需要遍历所有文件描述符(O(n)),epoll 只处理发生事件的文件描述符,其复杂度接近 O(1),在大量并发连接场景下表现优异。
3. 内核事件表(Kernel Event Table):
• epoll 在内核中维护一个事件表,用户进程通过系统调用 epoll_ctl() 操作该表,向其中添加、修改或删除文件描述符。事件表驻留在内核中,减少了系统调用的开销,提升了性能。
4. Edge-Triggered (ET) 和 Level-Triggered (LT) 模式:
• Level-Triggered (LT):默认模式,文件描述符处于就绪状态时会反复通知应用程序,直到事件被处理完毕。
• Edge-Triggered (ET):更高效的模式,只有在文件描述符从未就绪变为就绪时才通知应用程序。应用程序需要一次性处理所有数据,否则可能错过后续事件。
5. 使用红黑树和链表管理事件:
• epoll 在内核中使用红黑树(red-black tree)来存储和管理文件描述符,使得插入、删除、修改操作的复杂度为 O(log n)。
• 同时,epoll 使用一个双向链表来存储已就绪的文件描述符,便于快速遍历和获取事件。
6. 无文件描述符数量限制:
• epoll 没有 select 固有的文件描述符数量限制(如 1024 个文件描述符)。它可以处理的文件描述符数量只受系统的最大文件描述符数量限制(ulimit -n),因此更适合大规模并发网络服务。
7. 文件描述符可重复利用:
• epoll 不需要每次调用都重新设置监视的文件描述符集合。文件描述符一旦被添加到 epoll 实例中,只有当它们被显式删除时才会被移除,减少了调用 epoll_ctl 的频率。
8. 低资源消耗:
• 由于 epoll 在内核中维护事件表,并且只返回有状态变化的文件描述符,它相对节省资源。相比 select 和 poll 需要每次从用户空间传递整个文件描述符集合的做法,epoll 通过减少系统调用和内存拷贝的开销,提高了效率。
9. 适用于长连接场景:
• epoll 非常适合高并发、长连接的场景,如 Web 服务器、聊天室等。它可以高效地处理成千上万个并发连接,而不会像 select 一样性能急剧下降。
总结:
epoll 的底层特性让它非常适合处理大规模并发 I/O 操作。它通过事件驱动机制、内核事件表、红黑树和链表结构,提供了高效的文件描述符管理和事件通知机制。与 select 和 poll 相比,epoll 更加适用于高并发和长连接的场景。
TCP传输协议
TCP 协议:TCP是一个面向连接的,安全的,流式传输协议,这个协议是一个传输层协议。
面向连接:是一个双向连接,通过三次握手完成,断开连接需要通过四次挥手完成。
安全:tcp通信过程中,会对发送的每一数据包都会进行校验, 如果发现数据丢失, 会自动重传
流式传输:发送端和接收端处理数据的速度,数据的量都可以不一致
TCP握手问题
网络编程的接口
1. Socket 编程概述
套接字(Socket)是网络编程的基本单位。它可以看作是网络通信的端点,用于发送和接收数据。常用的套接字类型包括:
• TCP 套接字:面向连接的可靠通信(使用 SOCK_STREAM)。
• UDP 套接字:无连接的、不可靠的通信(使用 SOCK_DGRAM)。
2. Socket 编程步骤
以下是基于 TCP 的套接字编程基本流程(客户端和服务器):
服务器端流程:
1. 创建套接字(socket):
• 使用 socket() 函数创建一个套接字。
• 常用语法:
cpp
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
• AF_INET: 使用 IPv4 地址族。
• SOCK_STREAM: 使用 TCP 协议。
• 返回一个套接字文件描述符 sockfd。
2. 绑定地址(bind):
• 将套接字绑定到特定的 IP 地址和端口。
• 使用 bind() 函数,绑定套接字和 sockaddr_in 结构的地址。
cpp
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用接口
serv_addr.sin_port = htons(8080); // 绑定端口 8080
bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
3. 监听(listen):
• 将套接字设置为监听状态,准备接受客户端连接。
• 使用 listen() 函数,指定监听队列长度。
cpp
listen(sockfd, 5);
4. 接受连接(accept):
• 使用 accept() 函数,阻塞等待客户端连接。成功时返回一个新的套接字文件描述符,用于与客户端通信。
cpp
int client_sockfd = accept(sockfd, (struct sockaddr*)&client_addr, &addrlen);
5. 处理客户端请求:
• 使用 read() 和 write()(或 recv() 和 send())函数进行数据传输。
cpp
char buffer[1024];
read(client_sockfd, buffer, sizeof(buffer)); // 读取客户端数据
write(client_sockfd, "Hello, Client!", 14); // 发送数据给客户端
6. 关闭连接(close):
• 关闭套接字,释放资源。
cpp
close(client_sockfd);
close(sockfd);
客户端流程:
1. 创建套接字(socket):
• 与服务器端相同,使用 socket() 创建一个套接字。
cpp
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
2. 连接服务器(connect):
• 使用 connect() 函数与服务器建立连接。
cpp
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr); // 指定服务器 IP
connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
3. 发送和接收数据:
• 使用 write() 和 read() 函数进行数据传输。
cpp
write(sockfd, "Hello, Server!", 14);
read(sockfd, buffer, sizeof(buffer));
4. 关闭连接:
• 通信结束后,关闭套接字。
cpp
close(sockfd);
3. UDP 套接字编程
与 TCP 不同,UDP 套接字是无连接的,以下是使用 UDP 的基本步骤:
服务器端流程:
1. 创建套接字:
cpp
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
2. 绑定地址:
cpp
bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
3. 接收和发送数据:
• 使用 recvfrom() 接收数据,sendto() 发送数据。
cpp
recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&client_addr, &addrlen);
sendto(sockfd, "Hello, UDP Client!", 18, 0, (struct sockaddr*)&client_addr, addrlen);
4. 关闭套接字:
cpp
close(sockfd);
客户端流程:
1. 创建套接字:
cpp
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
2. 发送和接收数据:
cpp
sendto(sockfd, "Hello, UDP Server!", 18, 0, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&serv_addr, &addrlen);
3. 关闭套接字:
C++
close(sockfd);
介绍一下listen的第二个参数
listen() 函数的第二个参数(即 backlog 参数)用于指定 已完成三次握手但尚未被 accept() 处理的连接数 的最大值。简单来说,它控制的是内核为当前监听的套接字维护的 全连接队列 的长度。
backlog 参数的作用:
1. 全连接队列(Accept Queue):
• 当客户端发起连接请求,经过三次握手成功后,连接会进入 全连接队列,等待服务器程序调用 accept() 函数来处理。
• backlog 参数用于限制该队列中未被处理的连接数。如果队列已满,系统会拒绝新的连接请求,客户端可能会收到一个 ECONNREFUSED 错误,表示连接被拒绝。
2. 半连接队列(SYN Queue):
• 在 TCP 三次握手过程中,客户端发送 SYN 请求到服务器,此时服务器会将该连接放入 半连接队列 ,直到三次握手成功后,再进入 全连接队列。
• backlog 参数并不直接控制半连接队列的大小,不过在某些操作系统(例如 Linux),backlog 的大小可能会间接影响半连接队列的行为。
操作系统的处理方式:
不同操作系统可能会对 backlog 参数有所调整。它的值有时候会被限制为系统的最大允许值:
• 在 Linux 系统中,backlog 实际上可以被系统配置项 /proc/sys/net/core/somaxconn 的值所限制。如果 listen 中的 backlog 大于 somaxconn,那么实际的队列大小将被限制为 somaxconn 的值。
• Windows 操作系统在某些情况下也会对 backlog 做出自己的优化和限制。
backlog 的典型使用:
例如,listen(sockfd, 5); 中的 5 作为 backlog,意思是允许最多 5 个完成三次握手的连接进入全连接队列,如果第 6 个连接到来且队列满了,新的连接请求将会被拒绝。
特别说明:
• 过小的 backlog 值:如果设置得太小,服务器在处理高并发请求时可能无法及时处理所有连接请求,导致连接被拒绝。
• 过大的 backlog 值:如果设置得太大,而服务器的能力(例如处理器、内存)无法跟上,可能会导致系统资源不足。
实际建议:
通常情况下,可以根据服务器的并发处理能力和操作系统的推荐值来设置 backlog。在高并发环境下,可以通过增加 backlog 来提高系统对并发连接请求的容忍度。
UDP和TCP在使用上有什么区别吗?
UDP(用户数据报协议)和 TCP(传输控制协议)是两种常用的传输层协议,它们在使用上有明显的区别,主要体现在 可靠性 、连接性 、速度 和 数据传输模式 等方面。下面详细介绍它们在使用上的区别:
1. 连接性
• TCP :TCP 是面向连接的协议。在使用 TCP 进行通信之前,客户端和服务器需要经过 三次握手 建立连接。在完成数据传输后,连接需要通过 四次挥手 进行释放。这种机制保证了连接的可靠性。
• UDP:UDP 是无连接的协议。使用 UDP 时,客户端和服务器之间没有连接建立的过程,直接就可以发送数据。UDP 是一种"尽力而为"的协议,发送的数据包没有保证一定能到达。
2. 可靠性
• TCP :TCP 提供可靠的数据传输。它保证了数据包的到达顺序,并且在数据传输过程中会有 确认机制 和 重传机制,确保数据不会丢失。TCP 还包括流量控制、拥塞控制等机制,确保在高流量时数据传输仍然可靠。
• UDP:UDP 不提供可靠的传输。它不保证数据包的顺序,也不提供重传机制。如果数据包在传输过程中丢失,UDP 不会尝试重传。因此,UDP 适用于对数据完整性要求不高,但对速度有较高要求的应用。
3. 传输速度
• TCP:由于 TCP 需要经过三次握手建立连接,并在数据传输过程中进行确认、流量控制和重传等操作,这些机制虽然提高了可靠性,但会增加开销和延迟。因此,TCP 的传输速度通常会比 UDP 慢一些。
• UDP:UDP 是无连接、无状态的协议,直接发送数据包,没有确认和重传机制,因此传输速度快、延迟低。UDP 更适合需要快速传输的场景,如实时视频、语音通话、游戏等。
4. 数据传输模式
• TCP :TCP 是 面向字节流 的协议,数据以字节流的形式进行传输。这意味着 TCP 会将数据拆分为多个小数据包发送,接收方会将数据包重新组装为完整的数据。由于 TCP 是流式传输,发送方和接收方的数据可以连续读取。
• UDP :UDP 是 面向数据报 的协议,数据以独立的报文形式进行传输。每个 UDP 数据包都是独立的,发送方发送多少个数据包,接收方就收到多少个数据包。每个数据包的边界是明确的,接收方一次只能处理一个完整的数据包。
5. 应用场景
• TCP:适用于需要高可靠性、完整性且对速度要求不高的场景。例如:
• HTTP/HTTPS:网页浏览、文件下载等场景。
• FTP:文件传输协议。
• SMTP/POP3/IMAP:电子邮件协议。
• UDP:适用于需要快速传输且能容忍一定丢包的场景。例如:
• 实时通信:视频会议、VoIP(语音通信)等。
• 在线游戏:对延迟敏感的实时游戏。
• DNS:域名解析协议。
• 直播流媒体:实时音视频流传输。
6. 流量控制与拥塞控制
• TCP :TCP 有 流量控制 和 拥塞控制 机制,可以根据网络的状态调整数据传输的速度,防止发送方过快发送数据而导致网络拥堵或接收方处理不过来。TCP 会通过滑动窗口机制来实现流量控制,防止网络拥塞。
• UDP:UDP 没有流量控制和拥塞控制机制。发送方会以固定的速度发送数据,接收方必须自行处理。如果接收方处理不过来,数据包可能会被丢弃,无法重传。
7. 头部开销
• TCP:TCP 的头部较大,通常为 20 字节,包含用于数据重传、流量控制、连接管理等各种信息。因为有更多控制信息,头部开销较大。
• UDP:UDP 的头部非常简单,通常为 8 字节,包含源端口号、目标端口号、数据长度和校验和。由于没有复杂的控制信息,头部开销小,适合快速传输。
8. 有序性
• TCP:TCP 确保数据包按顺序到达。如果某个数据包在传输过程中丢失,TCP 会进行重传,直到接收方收到完整且按顺序排列的所有数据包。
• UDP:UDP 不保证数据包的顺序。如果数据包在传输过程中丢失或者顺序颠倒,UDP 不会进行纠正,因此接收方可能收到乱序的数据包。
总结
特性 TCP UDP
连接性 面向连接,需要建立连接 无连接,直接传输数据
可靠性 提供可靠传输,保证数据到达 不提供可靠传输,可能丢包
传输速度 较慢,因有握手、重传等机制 快,延迟低
传输模式 面向字节流 面向数据报
流量控制 有流量控制和拥塞控制 无流量控制和拥塞控制
头部开销 较大(20 字节) 较小(8 字节)
有序性 保证有序传输 不保证顺序
适用场景 文件传输、网页浏览、电子邮件 视频会议、实时游戏、DNS查询
总结:
• 如果应用场景需要 高可靠性、顺序性 ,并且能够容忍较高的开销和延迟,那么选择 TCP。
• 如果应用场景对 速度要求高 ,能够容忍一定的数据丢失和乱序,且不需要建立连接,那么选择 UDP。
UDP最大报文长度
UDP 报文的总长度由 UDP 头部 中的 长度字段(Length) 指定。该字段是 16 位的无符号整数,表示 UDP 报文的总长度(包含 UDP 头部和数据),因此理论上最大值为 2^16 - 1 = 65535 字节(约 64 KB)。
尽管理论上 UDP 支持 64 KB 的报文,但实际传输中,受 IP 层 的限制,尤其是 IP 层的 MTU(Maximum Transmission Unit,最大传输单元) 限制,大部分网络设备和链路对单个数据包的最大尺寸(MTU)通常是 1500 字节 (以太网环境中),超过这个长度的 UDP 报文会在 IP 层进行 分片。
遇到过栈溢出吗?
栈溢出的常见原因:
• 递归调用过深:例如递归函数没有合适的退出条件,导致无限递归,函数调用栈不断增加,最终栈溢出。
• 局部变量过多或过大:函数中的局部变量(如大型数组)如果占用的内存超过了栈的容量限制,也可能导致栈溢出。
• 死循环中递归调用:递归调用未能正确跳出循环。
• 内存泄漏或错误内存管理:某些编程错误可能会导致栈空间耗尽。
如何避免栈溢出?
• 避免深递归 :递归深度过大时,考虑改用 迭代算法 或采用 尾递归优化。
• 适当控制局部变量的大小 :如果局部变量过大,可以使用 动态内存分配(如 malloc()、new),而不是在栈上分配大量内存。
• 增加栈的大小:有时候可以通过修改编译器选项或系统设置来增加栈的大小,尤其是在 Linux 系统中,使用 ulimit 命令可以查看和调整栈大小。
cpp
ulimit -s 8192 # 将栈大小设置为 8 MB
• 启用栈保护机制
• 栈保护(Stack Guard/Canary) :某些编译器提供了栈保护机制(如 Canary 值),可以在栈溢出发生时检测并防止恶意攻击。虽然这不能直接解决栈溢出问题,但可以提高程序的安全性,防止缓冲区溢出攻击。
• 在 GCC 中,可以启用栈保护:
cpp
gcc -fstack-protector-all program.c -o program
一个进程间的多个线程都共享哪些资源?
1. 内存空间
• 代码段:所有线程共享同一个进程的代码段,即可执行程序的指令。
• 数据段:所有线程共享进程的全局变量和静态变量。
• 堆:所有线程共享动态分配的内存,使用 malloc、new 等分配的内存区域。
2. 文件描述符
• 所有线程共享打开的文件描述符,包括文件、套接字等。这意味着一个线程可以打开文件并且其他线程可以使用该文件描述符进行读写。
3. 信号
• 进程内的所有线程共享信号处理。进程的信号处理函数对所有线程有效,接收到的信号可以被任何线程处理。
4. 资源限制
• 线程共享与进程相关的资源限制,比如 CPU 使用时间、内存限制等。
5. 进程ID和其他进程属性
• 所有线程共享同一个进程ID(PID),同时进程的用户ID(UID)、组ID(GID)等属性也是共享的。
6. 线程间同步机制
• 线程可以通过共享的同步机制(如互斥锁、条件变量、信号量)进行通信和同步。
进程地址空间除了栈还有什么
1. 代码段(Text Segment)
• 存储程序的可执行指令。这一部分是只读的,以防止程序意外修改自身的指令。
2. 数据段(Data Segment)
• 存储全局变量和静态变量。数据段分为以下两部分:
• 初始化数据段:包含已初始化的全局和静态变量。
• 未初始化数据段(BSS Segment):包含未初始化的全局和静态变量,这部分在程序运行时会被自动初始化为零。
3. 堆(Heap)
• 用于动态分配内存。程序可以在运行时通过函数(如 malloc、new 等)申请和释放内存。堆的大小可以动态变化,且堆内存需要手动管理。
4. 共享库段(Shared Libraries Segment)
• 存储程序使用的共享库(动态链接库)的代码和数据。多个进程可以共享这些库,以节省内存。
5. 映射区域(Memory-Mapped Segment)
• 通过 mmap 函数创建的内存映射区域。这些区域可以映射文件到内存,或者用于进程间通信。
6. 线程局部存储(Thread Local Storage, TLS)
• 尽管是属于线程的概念,但每个线程在进程的地址空间中可能会有一些线程局部变量。这些变量对每个线程是独立的。
网络传输数据包要经过哪些层?
1. OSI 模型(开放系统互联模型)
OSI 模型共有 7 层,数据包在网络传输时经过以下层次:
1. 应用层(Layer 7)
• 处理应用程序之间的通信,负责数据的生成和应用层协议的处理(如 HTTP、FTP、SMTP 等)。
2. 表示层(Layer 6)
• 负责数据的表示和转换,包括数据编码、加密和解密等。
3. 会话层(Layer 5)
• 负责建立、管理和终止会话,维护会话的状态。
4. 传输层(Layer 4)
• 提供端到端的通信,负责数据的分段、传输和重组。主要协议有 TCP 和 UDP。
5. 网络层(Layer 3)
• 负责数据包的路由选择和转发,主要协议有 IP(互联网协议)。
6. 数据链路层(Layer 2)
• 负责在同一局域网内的帧传输,处理物理地址(如 MAC 地址),常用协议有 Ethernet。
7. 物理层(Layer 1)
• 负责物理媒介的传输,包括电缆、光纤和无线信号等,确保数据以比特流的形式通过物理媒介传输。
2. TCP/IP 模型
TCP/IP 模型通常简化为 4 层,数据包在传输时经过以下层次:
1. 应用层
• 与 OSI 模型的应用层、表示层和会话层相对应,处理应用程序之间的通信,协议包括 HTTP、FTP、DNS 等。
2. 传输层
• 对应于 OSI 模型的传输层,负责端到端的通信,主要使用的协议有 TCP 和 UDP。
3. 网络层
• 对应于 OSI 模型的网络层,负责数据包的路由和转发,主要使用的协议有 IP。
4. 链路层(数据链路层和物理层)
• 包括数据链路层和物理层,负责在物理媒介上发送和接收数据帧,处理物理地址和媒介的传输。
数据包传输过程
1. 应用层将数据传递给传输层,使用 TCP 或 UDP 封装成段或数据报。
2. 传输层将数据封装成数据包,并将其传递给网络层。
3. 网络层将数据包封装成 IP 数据报,并进行路由选择。
4. 数据包经过链路层,被封装成帧,并通过物理层发送。
5. 数据到达目标主机时,逆向过程进行解封装,最终传递给应用层。
ping命令是在第几层
ping 命令主要使用 ICMP (Internet Control Message Protocol,互联网控制消息协议)协议进行网络诊断,因此它通常被归类于 网络层(Layer 3)的一部分。
详细说明:
• 协议:ping 使用 ICMP 协议来发送回声请求(Echo Request)和接收回声应答(Echo Reply)消息。
• 功能:ping 用于测试网络连接的可达性,确定目标主机是否在线以及测量数据包往返延迟时间。
OSI 模型层级分析:
• 应用层(Layer 7):用户输入 ping 命令时,它实际上是应用层的一个操作,但 ping 的具体实现和数据包的发送过程涉及到更低的层次。
• 网络层(Layer 3):ICMP 是一个网络层协议,因此 ping 命令的核心功能在网络层处理,它负责生成 ICMP 数据包并处理路由。
总结
虽然用户在使用 ping 命令时直接与应用层交互,但它所依赖的 ICMP 协议工作在网络层。因此,可以说 ping 命令的主要操作是在 网络层。
什么是野指针,怎么解决野指针?
野指针(Dangling Pointer)是指指向已释放或无效内存地址的指针。当指针所指向的内存已经被释放,但指针本身仍然指向那块内存时,就形成了野指针。这种情况可能会导致未定义行为、程序崩溃、数据损坏等问题。
产生野指针的常见原因:
1. 内存释放后未清空指针:
当使用 free() 或 delete 释放动态分配的内存后,如果没有将指针设为 NULL,该指针就成为野指针。
2. 局部变量的地址被返回:
如果函数返回了一个局部变量的地址,调用者将得到一个野指针,因为局部变量在函数返回后被销毁。
3. 指向已经释放内存的指针:
在某些情况下,指针可能指向已经释放的内存,导致野指针。
解决野指针的方法:
1. 在释放内存后将指针置为 NULL:
在释放动态内存后,将指针置为 NULL,以避免野指针的产生。
2. 避免返回局部变量的地址:
如果需要返回指针,应该确保返回的是动态分配的内存,或者使用静态变量。
3. 使用智能指针(在 C++ 中):
在 C++ 中,可以使用智能指针(如 std::shared_ptr 或 std::unique_ptr),它们可以自动管理内存的分配和释放,避免手动管理指针带来的风险。
4. 谨慎管理内存:
在大型项目中,使用内存管理工具(如 Valgrind)来检测内存泄漏和野指针,可以帮助开发者识别潜在问题。
5. 遵循良好的编程习惯:
在编写代码时,养成良好的编程习惯,例如:
• 避免在指针未初始化的情况下进行操作。
• 定期检查指针的有效性。
• 使用合适的注释来说明指针的生命周期。
总结
野指针是指向无效内存的指针,会导致程序的不稳定和潜在的错误。通过在释放内存后将指针置为 NULL、避免返回局部变量的地址、使用智能指针等方法,可以有效避免和解决野指针的问题。
进程间通信的方式
1. 管道(Pipe)
• 无名管道:用于同一进程组的父子进程之间,数据在管道中以字节流的形式传输,具有先进先出(FIFO)的特性。
• 命名管道(FIFO):可以在不同进程之间通信,具有名称,支持无关的进程。
2. 消息队列(Message Queue)
• 允许进程以消息的形式发送和接收数据。消息队列存储在内核中,支持多个进程间的异步通信,可以按优先级处理消息。
3. 共享内存(Shared Memory)
• 多个进程可以直接访问同一块内存区域以进行数据交换。这是最快的IPC方式之一,因为进程之间无需通过内核进行数据拷贝,但需要使用同步机制(如信号量)来避免数据竞争。
4. 信号量(Semaphore)
• 信号量用于控制对共享资源的访问,通常与共享内存一起使用。它可以用于进程间的同步,确保在访问共享资源时避免冲突。
5. 套接字(Socket)
• 主要用于网络编程,可以在同一台机器上的不同进程之间或在不同机器之间通信。支持流(TCP)和数据报(UDP)两种通信方式。
6. 信号(Signal)
• 信号是异步通知机制,用于通知进程发生了某个事件。信号的主要用途是处理异步事件(如终止、暂停等),但不适合用于传输数据。
共享内存用什么接口?
共享内存是进程间通信(IPC)的一种高效方式,它允许多个进程访问同一块内存区域。使用共享内存通常涉及到以下几个关键的系统调用接口:
1. shmget
• 功能:创建一个共享内存段或获取一个已存在的共享内存段的标识符。
• 参数:
• key_t key: 用于唯一标识共享内存段的键值。
• size_t size: 共享内存段的大小(字节)。
• int shmflg: 控制标志(如权限设置)。
• 返回值:成功时返回共享内存段的标识符,失败时返回 -1。
2. shmat
• 功能:将共享内存段附加到调用进程的地址空间。
• 参数:
• int shmid: 共享内存段的标识符。
• const void *shmaddr: 指定附加的地址(通常为 NULL,表示由系统自动选择)。
• int shmflg: 控制标志。
• 返回值:成功时返回共享内存段的指针,失败时返回 (void *) -1。
3. shmdt
• 功能:从调用进程的地址空间分离共享内存段。
• 参数:
• const void *shmaddr: 指向要分离的共享内存段的指针。
• 返回值:成功时返回 0,失败时返回 -1。
4. shmctl
• 功能:控制共享内存段的操作,如删除、获取状态信息等。
• 参数:
• int shmid: 共享内存段的标识符。
• int cmd: 控制命令(如 IPC_RMID 表示删除共享内存段)。
• struct shmid_ds *buf: 用于获取或设置共享内存段的状态信息。
• 返回值:成功时返回 0,失败时返回 -1。
创建线程的接口
• POSIX 线程:使用 pthread_create 接口创建线程,适用于 C/C++ 环境,需链接 pthread 库(通常为 -lpthread)。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void* thread_function(void* arg) {
printf("Hello from thread! Argument: %d\n", *(int*)arg);
return NULL;
}
int main() {
pthread_t thread;
int arg = 42;
// 创建线程
if (pthread_create(&thread, NULL, thread_function, &arg) != 0) {
perror("Failed to create thread");
return 1;
}
// 等待线程结束
pthread_join(thread, NULL);
return 0;
}
• C++11 及以后的标准库:使用 std::thread 类创建线程,语法更简洁,支持 lambda 表达式和更灵活的可调用对象。
cpp
#include <iostream>
#include <thread>
void thread_function(int arg) {
std::cout << "Hello from thread! Argument: " << arg << std::endl;
}
int main() {
int arg = 42;
// 创建线程
std::thread t(thread_function, arg);
// 等待线程结束
t.join();
return 0;
}
函数在压栈时都有哪些寄存器?
1. 通用寄存器
在调用函数时,以下寄存器通常会受到影响:
• RBP**(Base Pointer,基指针)**:用于指向当前函数的栈帧的基地址。函数入口时,RBP 会被保存并设置为当前 RSP(Stack Pointer,栈指针)值。
• RSP**(Stack Pointer,栈指针)**:指向栈顶。每次压栈(如保存寄存器的值、参数等)时,RSP 会减小。
2. 参数寄存器
在 x86-64 架构中,前几个函数参数通常通过寄存器传递,而不是通过栈。常用的参数寄存器包括:
• RDI:第一个参数。
• RSI:第二个参数。
• RDX:第三个参数。
• RCX:第四个参数。
• R8:第五个参数。
• R9:第六个参数。
超过六个参数的函数会将后续参数压入栈中。
3. 返回值寄存器
• RAX:用于存放函数的返回值。通常情况下,返回值会存放在 RAX 中(如果是指针或整数类型),而浮点值则可能存放在其他寄存器中,如 XMM0。
4. 保存状态的寄存器
在进入函数时,需要保存一些寄存器的状态,以便在函数返回时恢复。常见的寄存器包括:
• RBX:被调用者保存寄存器,调用函数时需要保存其值。
• R12**、R13 、R14、**R15:同样是被调用者保存寄存器,调用函数时也需保存。
5. 其他寄存器
根据调用约定的不同,可能还会涉及到其他特定寄存器的使用。
具体过程示例
在一个简单的 C 函数调用中,以下步骤通常会发生:
1. 函数参数压栈或放入寄存器:将参数放入指定的寄存器(如 RDI, RSI, 等)。
2. 保存返回地址:调用指令将返回地址压入栈中。
3. 创建新的栈帧:保存当前的 RBP,然后将 RSP 的值复制到 RBP,以标识新的栈帧。
4. 分配局部变量:更新 RSP 来为局部变量分配空间。
5. 执行函数体:函数体执行期间,使用寄存器和栈来存储临时数据和返回值。
函数指针的好处
1. 动态函数调用:
函数指针使得程序可以在运行时决定调用哪个函数。这对于实现回调函数和事件驱动编程非常有用。
2. 减少代码重复:
使用函数指针,可以避免重复编写类似的代码,尤其在处理多种相似操作时。可以将通用的操作逻辑放在一个函数中,然后通过不同的函数指针来指定不同的实现。
3. 回调机制:
函数指针是实现回调的基础。在许多库和框架中,用户可以传递自定义的函数给库函数,以便在特定事件发生时调用(例如,排序函数中的比较函数)。
4. 多态性:
函数指针可以用于实现简单的多态性。通过将不同的函数指针赋值给同一个函数指针变量,可以在运行时调用不同的函数,实现不同的行为。
5. 接口和抽象:
函数指针常用于定义接口和抽象,允许不同的实现通过函数指针进行相互替换。可以为模块化编程和设计模式(如策略模式)提供支持。