百度C++开发一面凉经
来源:https://www.nowcoder.com/feed/main/detail/038b061de2f34bce81fcd485629512ac
1、你知道网络编程服务端建立连接的流程吗?把用到的api说出来?
- socket():创建一个套接字,指定协议族(如
AF_INET表示IPv4)、套接字类型(如SOCK_STREAM表示TCP流式套接字)以及协议(通常为0,系统会自动选择合适的协议)。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);- bind():将创建的套接字与服务器的本地地址(IP 和端口号)绑定。
sockaddr结构体包含地址信息。
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有本地地址
serv_addr.sin_port = htons(PORT); // 绑定的端口号
bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));- listen():将套接字设为被动监听状态,等待客户端的连接请求。
backlog参数定义可以排队的最大连接数。
listen(sockfd, backlog);- accept():从连接队列中提取下一个连接,创建一个新的套接字用于与客户端通信。这是阻塞调用,直到有连接到达。
int newsockfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);- recv()/send() 或 read()/write():在
accept后,通过新创建的套接字与客户端进行数据通信。
recv(newsockfd, buffer, sizeof(buffer), 0); // 接收数据
send(newsockfd, buffer, sizeof(buffer), 0); // 发送数据- close():关闭套接字,释放资源。
close(sockfd);2、服务端需要设置端口号吗?客户端呢?端口号在哪个api中自动绑定的?端口号有什么作用?如果两个程序使用同一个端口号会怎么样?端口号的范围是多少?哪里限制了端口号的范围?
1. 服务端是否需要设置端口号?客户端呢?
- 服务端:通常需要显式设置端口号,通过
bind()函数将套接字绑定到一个特定的端口号(和IP地址)。端口号告诉操作系统该服务监听来自客户端的连接请求的特定端口。 - 客户端:通常不需要显式设置端口号。当客户端通过
connect()连接到服务器时,操作系统会自动分配一个随机的临时端口号(即源端口)。客户端关心的主要是服务器的地址和端口,而自己的端口可以由系统自动处理。
2. 端口号在哪个 API 中自动绑定的?
- 服务端:端口号在
bind()函数中显式绑定到服务端的套接字。
bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));- 在
serv_addr结构体中,serv_addr.sin_port = htons(PORT)指定了端口号。 - 客户端:客户端如果不手动绑定端口号,系统会在
connect()函数调用时,自动分配一个临时端口号。
3. 端口号的作用
- 端口号的主要作用是区分主机上运行的不同服务。例如,Web 服务器通常使用端口号 80(HTTP)或 443(HTTPS),而SSH 服务使用端口号 22。
- 端口号让一台机器的多个网络应用程序可以通过同一IP地址处理来自不同客户端的网络请求。
4. 如果两个程序使用同一个端口号会怎么样?
- 在同一台主机上,同一时间同一个端口只能由一个程序占用。如果尝试在同一端口上启动两个程序,后启动的程序会遇到
EADDRINUSE错误,即“地址已在使用”。 - 在不同的主机上,使用相同的端口号不会冲突。因为TCP/IP 协议通过IP地址+端口号来区分网络连接,所以两台不同的主机可以使用相同的端口号。
5. 端口号的范围
有效的端口号范围是:0 到 65535。
- 0-1023:这是“知名端口号(well-known ports)”,通常为常见的服务和应用程序保留(如HTTP的80,SSH的22等),非特权用户程序不能绑定这些端口。
- 1024-49151:这是“注册端口号”,可用于特定服务,但没有专属分配,可以在注册机构注册使用。
- 49152-65535:这是“动态或私有端口号”,主要用于客户端程序,通常是系统自动分配给临时连接使用。
6. 哪里限制了端口号的范围?
端口号的范围是由TCP/IP 协议栈在操作系统中定义的。这个限制来自于协议本身,因为端口号使用 16 位整数来表示,允许的最大值是 65535(2^16 - 1)。
3、你知道linux用什么命令查找端口号吗?
1. netstat
netstat 是一个老牌的网络工具,可以列出所有网络连接及端口的使用情况。常用选项如下:
netstat -tuln-t:显示 TCP 端口。-u:显示 UDP 端口。-l:显示监听状态的端口。-n:以数字形式显示地址和端口号。
2. ss
ss 是 netstat 的现代替代品,速度更快,功能也更强大。
ss -tuln与 netstat 类似,ss -tuln 显示所有正在监听的 TCP 和 UDP 端口。
3. lsof
lsof 可以列出打开的文件(包括网络端口也是文件),你可以用它来查找哪个进程在使用某个端口。
lsof -i :PORT_NUMBER例如,查找哪个进程在使用端口 8080:
lsof -i :80804. fuser
fuser 可以显示使用某个端口的进程 ID(PID)。它主要用于查找某个端口的占用情况。
fuser -n tcp PORT_NUMBER例如,查找使用 TCP 端口 80 的进程:
fuser -n tcp 805. nmap
nmap 是一个网络扫描工具,主要用于扫描指定主机上开放的端口。
nmap localhost这会列出本地主机上所有开放的端口。
4、UDP和TCP?TCP头部有哪些内容?TCP怎么保证可靠连接的?
1. UDP 和 TCP 的区别
UDP(User Datagram Protocol,用户数据报协议) 和 TCP(Transmission Control Protocol,传输控制协议) 是两种常见的传输层协议,主要区别:
| 特点 | TCP | UDP |
|---|---|---|
| 是否连接 | 面向连接(建立连接,三次握手) | 无连接(不建立连接,直接发送数据) |
| 可靠性 | 提供可靠的数据传输(确认、重传、顺序保证) | 不保证可靠传输(无确认、不重传) |
| 传输方式 | 数据流(面向字节流,按顺序传输) | 数据报(独立的数据包,可能乱序或丢失) |
| 拥塞控制 | 有拥塞控制、流量控制 | 无拥塞控制 |
| 传输效率 | 相对较低,因需要建立连接、确认和重传数据 | 相对较高,开销小 |
| 应用场景 | 需要可靠性、高延迟容忍的场景,如文件传输、HTTP请求 | 实时要求高但可靠性要求不高的场景,如视频直播、DNS请求 |
| 头部大小 | 20 字节固定长度 | 8 字节固定长度 |
2. TCP 头部内容
| 字段 | 大小(位) | 描述 |
|---|---|---|
| 源端口号 | 16 | 发送方的端口号,用来标识源应用程序。 |
| 目的端口号 | 16 | 接收方的端口号,用来标识目标应用程序。 |
| 序列号 | 32 | 表示该段数据在整个数据流中的位置,用于数据重组和丢包重传。 |
| 确认号 | 32 | 确认接收到的数据段的下一个序列号,用于实现可靠数据传输(ACK)。 |
| 数据偏移 | 4 | 表示TCP头部的长度,以32位字为单位。 |
| 保留位 | 3 | 保留未使用。 |
| 控制位 | 9 | 包括重要的控制标志,例如:SYN、ACK、FIN、RST 等。 |
| 窗口大小 | 16 | 用于流量控制,表示接收方能够接收的最大字节数。 |
| 校验和 | 16 | 用于检查头部和数据是否出错,确保数据完整性。 |
| 紧急指针 | 16 | 指示紧急数据的位置。 |
| 选项 | 可变 | 可选字段,用于扩展功能,如窗口大小调整、时间戳等。 |
3. TCP 如何保证可靠连接
- 三次握手(Three-way Handshake):在建立连接时,TCP 使用三次握手确保双方确认彼此的存在,并同步初始序列号,避免早期的旧数据干扰。
- 客户端发送 SYN 包(同步序列号)请求连接。
- 服务器收到 SYN 后返回 SYN-ACK 包确认连接。
- 客户端收到 SYN-ACK 后再发送 ACK 包,完成连接建立。
- 确认应答机制(ACK):每个数据包在发送后,接收方会发送一个确认应答(ACK)包,告知发送方已成功接收。发送方只有在收到 ACK 后,才认为数据成功传送。
- 重传机制:如果发送方没有在预期时间内收到 ACK,TCP 会自动重传数据,确保丢失的数据段被重新发送。
- 序列号与重排序:每个数据包都有唯一的序列号,接收方根据序列号来重组数据,确保数据按正确顺序到达。
- 流量控制:通过窗口大小(Window Size)字段,TCP 控制数据传输速度,避免发送方发送过快导致接收方处理不过来。
- 拥塞控制:TCP 使用如慢启动、拥塞避免等算法来检测网络拥塞,并根据网络情况动态调整传输速度,确保不会因过载导致网络崩溃。
- 四次挥手(Four-way Teardown):TCP 连接结束时通过四次挥手过程来保证连接的可靠关闭,确保双方都有机会传输剩余的数据。
5、说一说网络的TCP/IP四层模型的作用?
TCP/IP 四层模型的具体层次
- 应用层(Application Layer) 作用:应用层是面向用户的网络层次,它为应用程序提供接口,支持各种网络服务,如文件传输、电子邮件、远程登录、网页浏览等。 常见协议:HTTP(网页访问)、FTP(文件传输)、SMTP(邮件传输)、DNS(域名解析)、SSH(远程登录)等。 功能:
- 定义应用程序的通信方式。
- 提供网络服务,支持数据格式转换、加密、解密等。
- 传输层(Transport Layer) 作用:传输层负责在应用程序之间进行可靠的数据传输和端到端的通信。它为应用层提供进程间的数据传输,并保证数据传输的可靠性和顺序性。 常见协议:TCP(传输控制协议)、UDP(用户数据报协议)。 功能:
- TCP 提供可靠的数据传输,使用三次握手、流量控制、重传机制等保证数据正确传递。
- UDP 提供快速但不可靠的传输,适用于实时应用。
- 端口号用于标识源和目标应用程序。
- 网络层(Internet Layer) 作用:网络层负责将数据包从源主机传递到目标主机,提供跨网络的寻址和路由选择。它决定数据如何通过多个网络跳转,直至到达目的地。 常见协议:IP(互联网协议)、ICMP(互联网控制报文协议)、ARP(地址解析协议)。 功能:
- IP 协议负责分配 IP 地址,并决定路由选择(从源到目的地)。
- 数据包的分组和重组:将数据划分为数据包并重新组装。
- 提供无连接、不可靠的传输。
- 链路层(Link Layer) 作用:链路层负责在同一物理网络内进行数据帧的传输,它处理设备之间的通信以及帧的发送和接收。链路层还包括硬件寻址(如 MAC 地址),处理局域网(LAN)或广域网(WAN)内的通信。 常见协议:以太网、Wi-Fi、PPP(点对点协议)、帧中继。 功能:
- 封装数据包为数据帧,并通过物理介质传输。
- 硬件寻址(MAC 地址)用于在局域网内识别设备。
- 检错和校验帧的传输是否正确(如 CRC 校验)。
作用
- 分层抽象:将网络通信过程分为多个抽象层,每层处理特定的任务。这种设计减少了复杂性,使得开发和维护网络系统更加容易。
- 模块化设计:每一层只处理特定的功能,彼此独立,可以互换和改进。例如,应用层的改变不会直接影响到传输层和网络层。
- 标准化协议:TCP/IP 协议栈为每一层定义了标准化的协议,确保不同厂商和系统之间能够互相通信。例如,传输层的 TCP 和网络层的 IP 都是标准化的协议。
- 互操作性:不同网络设备和操作系统可以通过遵循相同的协议规范,确保互相通信和数据传输,这也是互联网能够全球互联的重要原因。
- 故障隔离:分层设计有助于隔离问题,当某一层出现故障时,可以专注于该层进行排查,不会影响其他层。
6、TCP是那一层的,IP呢?说一说DNS的工作流程是怎么样的?
TCP 和 IP 分别属于哪一层?
TCP(Transmission Control Protocol,传输控制协议) 属于 传输层(Transport Layer)。
传输层负责在两个主机之间提供可靠的数据传输服务,TCP通过三次握手、序列号、重传机制等手段确保数据的可靠传输。
IP(Internet Protocol,互联网协议) 属于 网络层(Internet Layer)。
网络层的主要功能是处理数据包的路由选择和转发,确保数据能够跨越不同的网络传输到目标地址。IP协议负责为数据包分配IP地址,并决定数据包从源主机到目标主机的路径。
DNS 的工作流程
DNS(Domain Name System,域名系统) 是互联网的“电话簿”,用于将人类可读的域名(如 www.xxx.com)解析为计算机可处理的 IP 地址(如 192.0.2.1)。
DNS 的工作流程主要分为以下几个步骤:
1. 用户发起 DNS 请求
- 当用户在浏览器中输入一个域名(如
www.example.com),客户端应用程序(例如浏览器)会首先检查本地缓存是否有对应的 IP 地址。 - 如果在缓存中找不到结果,客户端将发起 DNS 查询请求,通常是由操作系统的 DNS 解析器(resolver)完成的。
2. 查询本地 DNS 缓存
DNS 解析器首先检查操作系统的缓存,看是否有这个域名的解析记录。如果有,则直接返回 IP 地址,跳过后续步骤。
3. 向递归 DNS 服务器查询
- 如果本地没有缓存,DNS 解析器会向配置的递归 DNS 服务器发起查询请求。递归服务器通常由互联网服务提供商(ISP)或公共 DNS 提供商(如 Google DNS、Cloudflare DNS)提供。
- 递归 DNS 服务器的作用是代表客户端递归地查找域名的 IP 地址。
4. 递归查询过程
如果递归 DNS 服务器自身的缓存中也没有该域名的 IP 地址,它会按照以下顺序进行递归查询:
- 根域名服务器(Root DNS Server):递归服务器首先向根域名服务器发起请求。根服务器不直接提供域名的 IP 地址,但会告诉递归服务器该请求需要查询的顶级域(TLD)服务器。
- 顶级域名服务器(TLD DNS Server):递归服务器根据根服务器的回应,向负责域名的 TLD 服务器发起请求。例如,针对
www.xxx.com,TLD 服务器为.com服务器。TLD 服务器会回应递归服务器,指向负责该域名的权威 DNS 服务器。 - 权威 DNS 服务器(Authoritative DNS Server):权威服务器是最终负责存储域名和对应 IP 地址的服务器。递归服务器向该服务器发起请求,权威服务器返回域名的对应 IP 地址(例如
192.0.2.1)。
5. 返回结果
- 递归 DNS 服务器将从权威 DNS 服务器处获得的 IP 地址返回给客户端的 DNS 解析器。
- 同时,递归 DNS 服务器会将结果缓存一定时间(由权威服务器的 TTL 值决定),以便下次查询时更快响应。
6. 客户端与目标主机通信
客户端收到解析后的 IP 地址后,就可以使用该 IP 地址与目标主机建立 TCP/IP 连接,完成数据传输。
7、你能介绍一下什么是重载吗?你知道重载和重写的区别吗?
1. 什么是重载(Overloading)?
**重载(Overloading)**是面向对象编程中的一个重要概念,允许在同一个作用域中定义多个同名的函数或方法,但这些函数具有不同的参数列表。编译器通过参数的个数、类型或顺序来区分这些函数调用。这种机制使得函数可以根据不同的参数类型或数量实现不同的功能。
在 C++ 中,重载不仅适用于普通函数,还适用于构造函数和运算符。
重载的特点:
- 函数名相同,但参数类型、数量或顺序不同。
- 返回值类型可以不同,但不能仅靠返回值类型区分重载函数。
- 重载发生在编译时,编译器根据传入的参数选择调用哪个函数。
#include <iostream>
class Calculator {
public:
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
};
int main() {
Calculator calc;
std::cout << calc.add(5, 3) << std::endl; // 输出 8
std::cout << calc.add(5.2, 3.1) << std::endl; // 输出 8.3
std::cout << calc.add(1, 2, 3) << std::endl; // 输出 6
}函数 add 被重载了三次,分别处理不同数量或类型的参数。
2. 什么是重写(Overriding)?
**重写(Overriding)**是在继承关系中,子类重新定义从父类继承的方法。子类通过提供与父类方法完全相同的函数签名(包括函数名、参数类型、参数个数和返回类型)来覆盖父类的方法,实现特定于子类的行为。
在 C++ 中,重写主要通过 虚函数(virtual functions) 实现,并且重写的函数需要在父类中声明为 virtual,以支持多态性。
重写的特点:
- 函数名、参数列表和返回类型必须与父类的虚函数一致。
- 重写必须在继承体系中进行。
- 使用虚函数表(vtable)来实现动态绑定(多态)。
- 发生在运行时,根据对象的实际类型调用对应的子类方法。
#include <iostream>
class Animal {
public:
virtual void makeSound() {
std::cout << "Animal makes sound" << std::endl;
}
};
class Dog : public Animal {
public:
void makeSound() override {
std::cout << "Dog barks" << std::endl;
}
};
int main() {
Animal* animal = new Dog();
animal->makeSound(); // 输出 "Dog barks"
delete animal;
}Dog 类重写了 Animal 类的虚函数 makeSound,并且通过多态性,在运行时调用了 Dog 类的 makeSound 函数。
区别
| 特性 | 重载(Overloading) | 重写(Overriding) |
|---|---|---|
| 定义位置 | 同一类中 | 子类对父类的虚函数进行重写 |
| 函数名 | 相同 | 相同 |
| 参数列表 | 不同(参数类型、个数、顺序等) | 完全相同(包括返回值类型) |
| 返回值类型 | 可以不同,但不能仅靠返回值区分 | 必须相同 |
| 继承关系 | 无需继承 | 必须在继承体系中,子类重写父类的虚函数 |
| 发生时机 | 编译时确定,编译器通过参数列表选择调用的函数 | 运行时确定,通过虚函数表实现动态绑定,体现多态性 |
| 关键字 | 无特定关键字,直接通过参数列表重载 | 父类方法需要使用 virtual,子类重写时可以用 override |
| 用途 | 提供同名函数的多个实现,以支持不同类型或数量的参数 | 子类提供父类方法的新实现,体现对象的多态行为 |
8、你了解虚拟内存吗?进程间的通信方式有哪些你了解吗?
1. 虚拟内存(Virtual Memory)
虚拟内存 是现代操作系统中用于管理内存的一项重要技术,它通过将物理内存与磁盘存储相结合,为每个进程提供一个独立、连续的地址空间。这样,进程不必知道物理内存的实际布局,操作系统负责将虚拟地址映射到物理地址,从而提高内存的利用率和系统的稳定性。
虚拟内存的作用:
- 内存扩展:即使物理内存不足,操作系统也可以通过使用磁盘(如交换空间,swap)来扩展可用的内存容量。
- 进程隔离:每个进程拥有独立的虚拟地址空间,保证一个进程不能直接访问另一个进程的内存,增强了安全性和稳定性。
- 简化内存管理:开发人员可以在虚拟地址空间中编写代码,而不必关心底层的物理内存布局。
虚拟内存的工作原理:
- 当进程请求访问内存时,操作系统通过页表(page table)将进程的虚拟地址转换为物理地址。
- 如果所需数据不在物理内存中,发生 页面置换(Page Swap),操作系统会将不常用的页面写入磁盘,并将所需页面从磁盘加载到内存。
- 通过 页表 和 TLB(转换后备缓冲区) 来加速虚拟地址到物理地址的转换。
页面置换算法:
- FIFO(First In First Out):最早进入内存的页面最先被置换。
- LRU(Least Recently Used):最近最少使用的页面最先被置换。
- LFU(Least Frequently Used):访问频率最少的页面被置换。
2. 进程间的通信方式(Inter-process Communication, IPC)
在操作系统中,不同进程之间通常需要交换数据或协作完成某些任务。由于进程的虚拟内存空间是隔离的,直接访问彼此的内存是不允许的,因此需要使用 进程间通信(IPC)机制。
管道(Pipe):
一种单向通信的机制,通常用于具有父子关系的进程之间。父进程和子进程通过管道传递数据。
命名管道(Named Pipe) 支持无关的进程之间通信,且可以双向通信。
ls | grep .cpp管道将 ls 的输出传递给 grep。
int pipefd[2];
pipe(pipefd);
if (fork() == 0) {
// 子进程,写入管道
write(pipefd[1], "Hello", 5);
} else {
// 父进程,读取管道
char buffer[10];
read(pipefd[0], buffer, 5);
printf("Message: %s\n", buffer);
}消息队列(Message Queue):
- 通过消息队列将数据结构化的消息从一个进程传递给另一个进程,支持有序、异步的通信方式。
- 操作系统提供接口来管理消息队列(如消息发送、接收、删除)。
共享内存(Shared Memory):
- 允许多个进程共享一块内存区域,这是最快的进程间通信方式,因为数据不需要通过内核缓冲区传递。
- 为了避免多个进程同时读写共享内存引发问题,通常需要搭配 信号量 或 互斥锁 来进行同步控制。
信号量(Semaphore):
- 信号量是一种用于控制进程间同步的机制,常用于解决共享资源访问中的竞争问题。
- 信号量维护了一个计数器,可以保证多个进程对同一资源的访问是安全的。
消息传递接口(Socket):
- Socket 是用于在不同主机或同一主机不同进程之间进行通信的通用机制,特别适用于网络编程。
- 支持跨网络、跨主机的进程通信,常见的应用如客户端-服务器模型。
信号(Signal):
- 信号是一种异步通信机制,允许进程通过发送信号的方式来通知另一个进程发生了某些事件(如
SIGKILL用于终止进程)。 - 信号主要用于进程控制而非大数据传输。
文件(File):
- 不同进程可以通过读写同一个文件来交换数据,这种方式比较慢,但简单且易于实现。
- 适合进程之间共享持久化数据。
套接字(Socket):
套接字不仅仅用于网络编程,也可以用于本地进程之间的通信。它支持双向通信,提供了极大的灵活性。
9、用户态和内核态的区别是什么,用户态怎么转到内核态,是自动转换的吗?
区别
在操作系统中,用户态(User Mode) 和 内核态(Kernel Mode) 是两种不同的 CPU 执行模式,它们的主要区别在于对系统资源的访问权限不同。
用户态(User Mode):
- 权限:受限,不能直接访问硬件或操作系统内核的核心数据结构和资源。
- 执行的任务:应用程序在用户态下运行,处理用户发起的请求或任务。
- 内存访问:只能访问用户空间的内存,无法访问内核空间。
- 安全性:通过限制用户态对系统资源的访问,保证系统的安全和稳定,防止用户程序随意操作硬件或系统资源。
- 故障处理:如果用户态程序出现错误,通常只会影响该程序,而不会导致整个系统崩溃。
内核态(Kernel Mode):
- 权限:最高权限,可以直接访问硬件设备、控制内存管理、处理中断等,拥有操作系统的全部控制权。
- 执行的任务:操作系统的内核在内核态运行,负责管理硬件资源(如 CPU、内存、I/O 设备)和提供系统服务(如文件系统、网络协议栈等)。
- 内存访问:可以访问内核空间和用户空间的所有内存。
- 安全性:由于内核态具有最高的权限,一旦出现错误(如内存越界或不正确的资源管理),可能会导致整个系统崩溃或严重的安全漏洞。
用户态如何转到内核态
从 用户态 转到 内核态 主要发生在进程需要操作系统的核心功能(如 I/O 操作、进程调度、内存管理)时。这个转换并不是自动的,而是通过特定的机制来实现的。
常见的用户态到内核态的转换机制:
- 系统调用(System Call):
- 用户进程通过系统调用(如
read,write,open,fork等)请求内核提供的服务。系统调用是操作系统提供给用户程序访问内核功能的接口。 - 当用户进程执行系统调用时,会触发 CPU 中的 陷入指令(trap instruction),该指令将 CPU 从用户态切换到内核态,并跳转到内核中的相应服务程序来处理该请求。
- 用户进程通过系统调用(如
用户程序调用 open() 函数打开文件,进入内核态后,内核通过文件系统打开该文件,并返回文件描述符,随后切换回用户态继续执行。
中断(Interrupts):
当硬件设备(如键盘、鼠标、网络卡等)发出中断请求时,系统会暂停当前的用户进程,转而处理设备的中断服务程序。处理中断的操作是在内核态完成的。
中断处理完成后,系统会恢复到用户态,继续执行被中断的进程。
用户正在输入键盘,键盘控制器发出中断请求,系统进入内核态处理按键,最后返回用户态继续程序执行。
- 异常(Exceptions):当用户程序发生异常(如除零错误、非法内存访问)时,系统会切换到内核态处理这些异常。异常通常是错误条件导致的,需要内核进行纠正或终止相关的进程。
用户程序试图访问非法内存地址,系统会捕获异常,进入内核态处理这个错误,可能会终止进程或抛出错误信息。
用户态与内核态的转换是否自动完成?
从用户态到内核态的转换不是自动发生的,需要通过明确的操作来触发(如系统调用、硬件中断、异常等)。一旦发生转换,CPU 会通过特殊的机制切换到内核态执行高权限代码。
转换的流程:
- 当用户进程触发了某种事件(如系统调用、中断、异常)时,CPU 会通过中断向量表或系统调用表找到对应的内核态处理函数。
- CPU 切换到内核态,保存当前的用户态上下文信息(如寄存器、程序计数器等),并执行内核态的处理程序。
- 内核完成处理后,再切换回用户态,恢复用户进程的上下文,继续执行用户进程。
自动恢复用户态:
虽然转换到内核态需要明确的触发机制,但从内核态回到用户态的过程通常是自动完成的。例如,当系统调用或中断处理结束后,操作系统会自动将 CPU 状态恢复为用户态,并继续执行之前的用户进程。
10、C++与C相比有什么好处?
1. 面向对象编程
- 类与对象:C++ 支持类的定义,允许将数据和操作封装在一起,促进代码的模块化和复用。
- 继承:C++ 支持类的继承,允许创建新类以扩展或修改现有类的功能,提高代码复用性。
- 多态性:通过虚函数,C++ 支持运行时多态,允许通过基类指针调用派生类的方法,使得代码更加灵活。
2. 标准模板库(STL)
- 数据结构和算法:C++ 提供了丰富的标准模板库,包括常用数据结构(如向量、链表、集合、映射等)和算法(如排序、查找等),大大提高了开发效率。
- 泛型编程:使用模板,开发者可以编写与类型无关的代码,提高代码的重用性和灵活性。
3. 更强的类型安全
C++ 提供了更严格的类型检查,避免了一些潜在的错误,增强了代码的安全性。
4. 资源管理
- 构造函数和析构函数:C++ 提供了构造函数和析构函数,可以在对象创建和销毁时自动管理资源,减少内存泄漏的风险。
- 智能指针:C++11 引入的智能指针(如
std::shared_ptr,std::unique_ptr)帮助管理动态分配的内存,自动释放资源,进一步提高了安全性。
5. 功能扩展
- 函数重载:C++ 支持函数重载,允许同名的函数根据参数类型和数量的不同实现不同的功能,提高了函数的灵活性。
- 运算符重载:C++ 允许重载运算符,使得用户定义的类型能够使用常见的运算符(如 +, -, *, 等),增强了代码的可读性。
6. 命名空间
避免命名冲突:C++ 引入了命名空间,帮助组织代码并避免名称冲突,尤其是在大型项目中非常有用。
11、C++11有哪些新特性?
- 自动类型推导(Auto): 允许编译器推导变量的类型,使代码更加简洁。
auto x = 5; // x的类型将被推导为int- 范围-based for 循环: 简化了对容器元素的遍历。
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (const auto& num : numbers) {
// 使用num
}- 智能指针: 引入了
std::shared_ptr和std::unique_ptr等智能指针,用于管理动态分配的内存,帮助防止内存泄漏。
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);- Lambda 表达式: 允许在函数内部定义匿名函数,提高代码可读性和灵活性。
auto add = [](int a, int b) { return a + b; };- nullptr: 引入了空指针常量
nullptr,用于替代传统的空指针NULL。
int* ptr = nullptr;- 强制类型转换(Type Casting): 引入了
static_cast、dynamic_cast、const_cast、reinterpret_cast等更安全和灵活的类型转换操作符。
double x = 3.14;
int y = static_cast<int>(x);- 右值引用和移动语义: 支持通过右值引用实现移动语义,提高了对临时对象的处理效率。
std::vector<int> getVector() {
// 返回一个临时vector
return std::vector<int>{1, 2, 3};
}
std::vector<int> numbers = getVector(); // 使用移动语义- 新的容器和算法: 引入了新的容器,如
std::unordered_map、std::unordered_set,以及一些新的算法。
std::unordered_map<int, std::string> myMap = {{1, "one"}, {2, "two"}};- 线程支持(std::thread): 提供了原生的多线程支持,使得并发编程更加方便。
#include <thread>
void myFunction() {
// 线程执行的代码
}
int main() {
std::thread t(myFunction);
t.join(); // 等待线程结束
return 0;
}12、你了解智能指针吗?
std::shared_ptr:- 原理:
std::shared_ptr是基于引用计数的智能指针,用于管理动态分配的对象。它维护一个引用计数,当计数为零时,释放对象的内存。 - 使用场景:适用于多个智能指针需要共享同一块内存的情况。例如,在多个对象之间共享某个资源或数据。
- 原理:
std::shared_ptr<int> sharedInt = std::make_shared<int>(42);
std::shared_ptr<int> anotherSharedInt = sharedInt; // 共享同一块内存std::unique_ptr:原理:
std::unique_ptr是独占式智能指针,意味着它独占拥有所管理的对象,当其生命周期结束时,对象会被自动销毁。使用场景:适用于不需要多个指针共享同一块内存的情况,即单一所有权。通常用于资源管理,例如动态分配的对象或文件句柄。
std::unique_ptr<int> uniqueInt = std::make_unique<int>(42);
// uniqueInt 的所有权是唯一的std::weak_ptr:原理:
std::weak_ptr是一种弱引用指针,它不增加引用计数。它通常用于协助std::shared_ptr,以避免循环引用问题。使用场景:适用于协助解决
std::shared_ptr的循环引用问题,其中多个shared_ptr互相引用,导致内存泄漏。
std::shared_ptr<int> sharedInt = std::make_shared<int>(42);
std::weak_ptr<int> weakInt = sharedInt;std::auto_ptr(已废弃):原理:
std::auto_ptr是C++98标准引入的智能指针,用于独占地管理对象。但由于其存在潜在的问题,已在C++11中被废弃。使用场景:在C++98标准中,可用于独占性地管理动态分配的对象。不推荐在现代C++中使用。
std::auto_ptr<int> autoInt(new int(42)); // 已废弃13、类的缺省函数有哪些?
1. 缺省构造函数
- 作用:无参数构造函数,用于创建类的实例时不传递参数。
- 特征:如果类中没有定义任何构造函数,编译器会自动提供一个缺省构造函数。
2. 拷贝构造函数
- 作用:用于通过另一个同类型对象来初始化一个对象,即复制对象。
- 特征:如果类中没有定义拷贝构造函数,编译器会自动提供一个。默认的拷贝构造函数执行逐成员复制。
3. 移动构造函数
- 作用:用于将一个对象的资源“移动”到另一个对象,避免不必要的复制。
- 特征:当类中没有定义移动构造函数时,编译器会提供一个默认实现,通常会将资源的所有权转移给新对象。
4. 赋值运算符
- 作用:用于将一个对象的值赋给另一个已存在的同类型对象。
- 特征:如果类中没有定义赋值运算符,编译器会提供一个默认实现。默认赋值运算符执行逐成员复制。
5. 移动赋值运算符
- 作用:用于将一个对象的资源“移动”到另一个已存在的对象,避免不必要的复制。
- 特征:当类中没有定义移动赋值运算符时,编译器会提供一个默认实现。
6. 析构函数
- 作用:在对象生命周期结束时自动调用,用于清理资源(如动态内存、文件句柄等)。
- 特征:如果类中没有定义析构函数,编译器会提供一个默认的析构函数,该函数会自动释放所有成员的资源。
14、构造函数是先执行基类构造还是子类构造?析构函数呢?
1. 构造函数的执行顺序
基类构造函数先执行:当创建一个子类对象时,基类的构造函数会在子类的构造函数之前被调用。这确保了基类的部分在子类的特定实现之前被正确初始化。
class Base {
public:
Base() { std::cout << "Base constructor" << std::endl; }
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived constructor" << std::endl; }
};
int main() {
Derived d; // 输出:Base constructor
// Derived constructor
return 0;
}2. 析构函数的执行顺序
子类析构函数先执行:在销毁一个子类对象时,子类的析构函数会在基类的析构函数之后被调用。这确保了子类的资源被释放后,基类仍然有效,以便可以安全地清理基类的资源。
class Base {
public:
~Base() { std::cout << "Base destructor" << std::endl; }
};
class Derived : public Base {
public:
~Derived() { std::cout << "Derived destructor" << std::endl; }
};
int main() {
Derived d; // 当 d 的生命周期结束时,输出顺序为:
// Derived destructor
// Base destructor
return 0;
}15、Windows创建线程的函数是什么,创建进程的呢?pthread_create有哪些参数,分别有哪些作用?父线程的pthread_create返回值是什么,pthread_detach()函数的作用?
1. Windows 创建线程和进程的函数
- 创建线程:
- 函数:
CreateThread - 原型:
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);
- 函数:
- 创建进程:
- 函数:
CreateProcess - 原型:
BOOL CreateProcess(LPCSTR lpApplicationName, LPCSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation);
- 函数:
2. pthread_create 的参数
函数原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);参数说明:
pthread_t \*thread:用于存储新线程的 ID 的指针。const pthread_attr_t \*attr:指向线程属性对象的指针,可以为NULL,表示使用默认属性。这个参数可以用于设置线程的栈大小、调度策略等。void \*(\*start_routine)(void \*):线程的起始例程,线程运行时将调用这个函数。该函数必须返回void *类型的值,并接收一个void *类型的参数。void \*arg:传递给线程起始例程的参数,可以是任意类型的指针,在线程内部可以通过强制转换来访问。
3. pthread_create 返回值
- 返回值:成功时返回
0,失败时返回错误代码。父线程通过检查返回值来判断线程是否成功创建。
4. pthread_detach() 函数的作用
作用:
pthread_detach(pthread_t thread)用于将线程设置为“分离状态”,使得线程在结束时自动释放其资源,而不需要被其他线程调用pthread_join来回收。- 这样做的好处是可以避免资源泄漏,适用于那些不需要等待线程结束结果的情况。
#include <pthread.h>
#include <stdio.h>
void *threadFunction(void *arg) {
printf("Thread is running\n");
return NULL;
}
int main() {
pthread_t thread;
if (pthread_create(&thread, NULL, threadFunction, NULL) != 0) {
perror("Failed to create thread");
}
// 设置线程为分离状态
pthread_detach(thread);
// 主线程继续工作
printf("Main thread is running\n");
// 需要等待一段时间以保证子线程有机会执行
sleep(1);
return 0;
}例子中,子线程在运行后会自动释放资源,而主线程则继续执行。
16、如果子线程退出,父线程没有捕捉子线程的信息会有什么影响?那如果父线程先退出呢,子线程会怎么样?
1. 子线程退出,父线程没有捕捉子线程的信息
影响:
- 如果子线程退出而父线程没有调用
pthread_join()来获取子线程的返回值,子线程的资源将不会被释放,导致资源泄漏。 - 子线程会处于“僵尸状态”,直到父线程调用
pthread_join(),这时系统才会回收子线程的资源。 - 如果多个子线程都处于僵尸状态,会占用系统资源,可能导致资源耗尽。
2. 父线程先退出
如果父线程在子线程之前退出,子线程的行为取决于系统和线程的创建方式:
- 如果子线程是独立的:在大多数操作系统中,子线程通常会继续执行,即使父线程已经结束。这是因为线程的存在不依赖于其父线程的存在。
- 如果父线程创建了子线程并且没有进行适当的同步或等待:如果父线程直接退出,子线程仍会在后台运行,但可能会导致一些不确定的状态,尤其是如果子线程依赖于父线程的某些资源(如打开的文件描述符或内存)。
17、算法题,给你一个3* 3的正方形,数字1-9,要求每行每列以及每个对角线的和都一样,打印出这个正方形?拓展:给你一个n * n的正方形,数字1-n*n,打印出这个正方形?
这个是类似于魔方阵的题目,给段代码帮助大家理解。
#include <iostream>
#include <vector>
void generateMagicSquare(int n) {
// 创建一个 n x n 的二维向量并初始化为 0
std::vector<std::vector<int>> magicSquare(n, std::vector<int>(n, 0));
int num = 1; // 从 1 开始填充
int i = 0; // 行索引
int j = n / 2; // 列索引,初始位置在第一行中间
// 填充魔方阵
while (num <= n * n) {
magicSquare[i][j] = num; // 填充当前数字
// 计算下一个位置
int newi = (i - 1 + n) % n; // 上一行
int newj = (j + 1) % n; // 下一列
if (magicSquare[newi][newj] != 0) { // 如果目标位置已填充
i++; // 下移到下一行
} else {
i = newi; // 移动到新位置
j = newj; // 更新列索引
}
num++; // 更新数字
}
// 输出魔方阵
std::cout << n << "x" << n << " 魔方阵如下:" << std::endl;
for (int row = 0; row < n; ++row) { // 遍历每一行
for (int col = 0; col < n; ++col) { // 遍历每一列
std::cout << magicSquare[row][col] << "\t"; // 打印元素
}
std::cout << std::endl; // 换行
}
// 输出行、列和对角线的和
std::cout << "每行、每列和对角线的和都为:" << (n * (n * n + 1) / 2) << std::endl;
}
int main() {
int n;
std::cout << "请输入正方形的阶数 n (n 为奇数): ";
std::cin >> n;
if (n % 2 == 1) { // 确保 n 是奇数
generateMagicSquare(n);
} else {
std::cout << "请确保输入的 n 为奇数!" << std::endl;
}
return 0; // 结束程序
}