这是一场关于技术面试的会议。本次会议主要围绕应聘者的技术背景、C++和iOS开发经验以及相关基础知识展开讨论。面试官对应聘者的技术能力进行了多方面的考察和评估。
1、自我介绍与工作经历
介绍:
XX毕业于XX大学,本科自动化专业,毕业后从事一年纯C++开发,后转向iOS应用开发。2015年至2024年主要从事IOS开发,期间涉及音视频业务领域开发,包括播放器业务和底层库封装。
2013年至2015年6月从事C++开发,2015年6月后转向iOS开发,期间混合C++开发。2024年底至2025年有三个月鸿蒙系统开发经验,编程语言ArkUI 和C++ 混合开发。
2、C++中定义常量的方式
(1)、使用 const 关键字 (推荐用于运行时常量)
(2)、使用 constexpr 关键字 (C++11 引入,推荐用于编译时常量)
(3)、 使用枚举 (enum 或 enum class)
(4)、使用 #define 预处理器指令 (传统方式,不推荐)
语法对比:
//1. 使用 const 关键字
const data_type CONSTANT_NAME = value;
//2.使用 constexpr 关键字
constexpr data_type CONSTANT_NAME = value;
//3. 使用枚举 (enum 或 enum class)
// 传统枚举 (plain enum)
enum EnumName {
CONSTANT1 = value1,
CONSTANT2 = value2,
// ...
};
// 强类型枚举 (enum class, C++11引入)
enum class EnumName {
CONSTANT1 = value1,
CONSTANT2 = value2,
// ...
};
4. 使用 #define 预处理器指令
#define CONSTANT_NAME value
使用案例:
//1、const
const double PI = 3.1415926535;
const int MAX_BUFFER_SIZE = 1024;
const std::string GREETING = "Hello, World!";
//2、constexpr
double PI = 3.1415926535; // 字面量是编译期可知的
constexpr int MAX_BUFFER_SIZE = 1024;
constexpr int ARRAY_SIZE = MAX_BUFFER_SIZE / sizeof(int); // 表达式可在编译期计算
// constexpr 函数,可在编译期计算
constexpr int square(int x) {
return x * x;
}
constexpr int SQUARED_VALUE = square(5); // 编译期调用函数,结果为 25
3、enum
// 传统枚举
enum Colors {
RED = 0xFF0000,
GREEN = 0x00FF00,
BLUE = 0x0000FF
};
// 强类型枚举
enum class FileMode {
READ = 1,
WRITE = 2,
READ_WRITE = READ | WRITE
};
//4. 使用 #define 预处理器指令
#define PI2 3.1415926535
#define MAX_BUFFER_SIZE 1024
#define GREETING "Hello, World!"
int main() {
// PI = 3.14; // 错误:无法修改 const 变量
double radius = 5.0;
double area = PI * radius * radius; // 使用常量
int my_array[ARRAY_SIZE]; // OK:因为 ARRAY_SIZE 是编译期常量,可用于指定数组大小
// constexpr int y = some_runtime_function(); // 错误:返回值不能在编译期确定
int background = BLUE; // 传统枚举常量会泄漏到外围作用域
FileMode mode = FileMode::READ_WRITE; // 强类型枚举需要使用作用域运算符
double area = PI2 * 5.0 * 5.0;
// 预处理器会将其替换为: double area = 3.1415926535 * 5.0 * 5.0;
char buffer[MAX_BUFFER_SIZE];
std::cout << GREETING << std::endl;
return 0;
}
最佳实践建议:
-
首选
constexpr:如果你需要一个编译时常量(用于数组大小、模板参数等),毫不犹豫地使用constexpr。 -
其次选
const:如果你需要一个在运行时初始化后就不再改变的值,使用const。 -
使用
enum class:定义一组相关的整数常量。 -
避免使用
#define来定义常量。
为什么不推荐#define:
无作用域: #define 定义的常量是全局的,不受命名空间、类等作用域的限制,容易造成命名冲突。
无类型: 只是简单的文本替换,没有数据类型,编译器无法进行类型检查,容易出错。
难以调试: 在调试器中看到的是替换后的值,而不是常量名,调试困难。
容易出错: 由于是文本替换,在复杂表达式中有可能产生意想不到的后果(例如 #define DOUBLE(x) x*x 在调用 DOUBLE(1+1) 时会产生 1+1*1+1 = 3 的错误结果)。
仅在极少数情况下使用: 需要与大量遗留代码交互,或者需要条件编译(#ifdef / #if defined)时,因为 #ifdef 不能检查 const 或 constexpr 变量。
3、c++中,静态多态和动态多态的区别
静态多态在编译阶段就确定了要调用哪个具体的函数或代码。编译器根据传入的参数类型或模板参数来生成不同的代码。
主要实现方式:
a) 函数重载 (Function Overloading)
b) 模板 (Templates)
动态多态在程序运行时才确定要调用哪个函数。它通过继承和虚函数机制实现。
实现方式:虚函数 (Virtual Functions)
底层机制分析:
每个包含虚函数的对象内部都有一个 vptr (虚函数表指针),指向该类的 vtable (虚函数表)。vtable 中存储了所有虚函数的实际地址。
使用案例:
#include <iostream>
//a) 函数重载
// 同一个函数名 print,多种实现
void print(int i) {
std::cout << "Integer: " << i << std::endl;
}
void print(double f) {
std::cout << "Double: " << f << std::endl;
}
void print(const std::string& s) {
std::cout << "String: " << s << std::endl;
}
// 函数模板
template <typename T>
T add(T a, T b) {
return a + b;
}
// 类模板
template <typename T>
class Calculator {
public:
T multiply(T a, T b) {
return a * b;
}
};
int main() {
print(10); // 调用 print(int)
print(3.14); // 调用 print(double)
print("Hello"); // 调用 print(const std::string&)
// 编译器生成 add<int> 和 add<double> 两个不同的函数
std::cout << add(5, 3) << std::endl; // 调用 add<int>(5, 3)
std::cout << add(2.5, 3.7) << std::endl; // 调用 add<double>(2.5, 3.7)
Calculator<int> intCalc;
std::cout << intCalc.multiply(4, 5) << std::endl; // 20
return 0;
}
#include <iostream>
#include <vector>
// 基类 with virtual function
class Animal {
public:
// 虚函数
virtual void speak() const {
std::cout << "Some animal sound..." << std::endl;
}
virtual ~Animal() {} // 虚析构函数,必不可少!
};
// 派生类
class Dog : public Animal {
public:
// 重写 (override) 基类的虚函数
void speak() const override {
std::cout << "Woof! Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void speak() const override {
std::cout << "Meow! Meow!" << std::endl;
}
};
int main() {
Dog myDog;
Cat myCat;
// 1. 通过基类指针调用
Animal* animalPtr = &myDog;
animalPtr->speak(); // 输出: Woof! Woof! (运行时决定)
animalPtr = &myCat;
animalPtr->speak(); // 输出: Meow! Meow! (运行时决定)
// 2. 通过基类引用调用
Animal& animalRef = myDog;
animalRef.speak(); // 输出: Woof! Woof!
// 3. 多态的强大体现:统一处理不同对象
std::vector<Animal*> animals = {&myDog, &myCat};
for (const auto& animal : animals) {
animal->speak(); // 运行时根据具体对象类型调用正确的 speak()
}
return 0;
}
-
当
animalPtr->speak()被调用时,程序会:-
通过
animalPtr找到对象的vptr。 -
通过
vptr找到类的vtable。 -
在
vtable中找到speak函数的位置。 -
调用该位置存储的函数地址。
-
现代C++更倾向于使用静态多态(模板、泛型编程)来实现策略模式和编译期多态,因为它效率更高。而动态多态(虚函数)则用于实现真正的运行时子类型多态,当你需要处理未知的具体类型时,它是不可或缺的工具。
4、TCP/IP协议
TCP/IP五层协议模型包括应用层、传输层、网络层、数据链路层和物理层。
数据封装与分用过程
发送方(封装过程 - 从上到下):
-
应用层:你的应用程序(如浏览器)产生原始数据(HTTP请求)。
-
传输层:将数据分成段,加上TCP头(包含源端口、目的端口等)。
-
网络层:将TCP段封装成IP包,加上IP头(包含源IP、目的IP等)。
-
数据链路层:将IP包封装成帧,加上帧头(包含源MAC、目的MAC等)和帧尾(校验和)。
-
物理层:将帧转换成比特流,通过网线/Wi-Fi发送出去。
接收方(分用过程 - 从下到上):
-
物理层:接收到比特流,将其组合成帧。
-
数据链路层:检查帧的MAC地址和校验和。如果无误,剥去帧头和帧尾,将IP包上交网络层。
-
网络层:检查IP包的目的IP地址。如果是本机,剥去IP头,将TCP段上交传输层。
-
传输层:检查TCP头的目的端口号,将数据交给正在监听该端口的应用程序。
-
应用层:应用程序(如Web服务器)接收并解析数据,生成回复,然后整个过程反向再来一遍。
总结:五层协议就像一个分工明确的快递系统,每一层只负责自己特定的任务,通过封装和协作,最终实现了复杂而强大的全球网络通信。
TCP的连接过程:通过 “三次握手” 来建立连接,通过 “四次挥手” 来终止连接。这个过程确保了连接的可靠性和双方状态的同步。
三次握手的目的是同步序列号(SEQ和ACK)、交换TCP参数(如窗口大小),并确认双方都具有收发能力。
四次挥手的目的是双方都确认要关闭连接。因为TCP是全双工的,每个方向必须单独关闭。
第一部分:TCP三次握手 (建立连接)
第一步:SYN (Synchronize) - 客户端发送连接请求
-
客户端 -> 服务器:发送一个TCP数据包。
-
设置 SYN标志位 = 1。
-
客户端随机选择一个初始序列号(ISN),比如
seq = x,并放在序列号字段中。
第二步:SYN-ACK (Synchronize-Acknowledge) - 服务器确认并回应
-
服务器 -> 客户端:回复一个TCP数据包。
-
SYN标志位 = 1和ACK标志位 = 1。
-
服务器也随机选择自己的初始序列号,比如
seq = y。 -
将确认号(Acknowledgment Number) 字段设置为
ack = x + 1(客户端的序列号+1)。
第三步:ACK (Acknowledge) - 客户端最终确认
-
客户端 -> 服务器:发送最后一个TCP数据包
-
ACK标志位 = 1。
-
将序列号字段设置为
seq = x + 1(第一步的seq+1)。 -
将确认号字段设置为
ack = y + 1(服务器的序列号+1)。
至此,双向连接建立成功,双方可以开始全双工地传输数据。
第二部分:TCP四次挥手 (终止连接)
第一步:FIN (Finish) - 客户端发起关闭请求
-
客户端 -> 服务器:发送一个TCP数据包。
-
设置 FIN标志位 = 1。
-
携带一个序列号,比如
seq = u(u是之前传输的最后一个字节的序列号+1)。
第二步:ACK - 服务器确认收到关闭请求
-
服务器 -> 客户端:回复一个ACK包。
-
设置 ACK标志位 = 1。
-
将确认号设置为
ack = u + 1。
第三步:FIN - 服务器发送关闭请求
-
服务器 -> 客户端:发送一个FIN包。
-
设置 FIN标志位 = 1(通常这个FIN包会和第二步的ACK包合并发送,但如果服务器还有数据要传,就会分开)。
-
携带一个序列号,比如
seq = w。
第四步:ACK - 客户端最终确认
-
客户端 -> 服务器:发送最后一个ACK包。
-
设置 ACK标志位 = 1。
-
将确认号设置为
ack = w + 1。
为什么握手是三次,挥手是四次?
因为建立连接时,服务器的SYN和ACK可以被合并成一个包发送(SYN-ACK)。
而关闭连接时,第二步的ACK是内核立即回复的,但第三步的FIN需要等应用程序调用close()函数后才会发送,所以通常无法合并,需要分两次发送,因此是四次。
5、进程和线程
进程是资源分配的基本单位
线程是CPU调度和执行的基本单位
进程和线程的区别
| 特性 | 进程 (Process) | 线程 (Thread) |
|---|---|---|
| 基本定义 | 资源分配的独立单位 | CPU调度的最小单位,是进程中的一个执行流 |
| 资源拥有 | 拥有独立的地址空间和系统资源 (代码、数据、堆栈、文件句柄、I/O设备等) | 共享所属进程的全部资源 (拥有自己独立的栈和寄存器) |
| 独立性 | 独立性高 一个进程崩溃后,在保护模式下不会影响其他进程。 | 独立性低 一个线程崩溃可能导致整个进程崩溃,从而影响同进程下的其他线程。 |
| 开销 | 大 创建、销毁、切换进程需要分配和回收资源,开销大。 | 小 创建、销毁、切换线程只需很少开销,因为它们共享资源。 |
| 通信机制 | 复杂 需要进程间通信(IPC)机制,如: 管道、消息队列、共享内存、信号量、Socket等。 | 简单 可以直接读写共享的进程数据段(全局变量、堆内存)进行通信。 但需要同步机制(如互斥锁、信号量)来避免冲突。 |
| 性能 | 上下文切换开销大,速度慢。 | 上下文切换开销小,速度快。 |
| 安全性 | 高。一个进程无法直接访问另一个进程的地址空间。 | 低。一个线程可以修改另一个线程的数据,容易导致错误。 |
| 归属关系 | 父进程可以创建子进程 | 线程隶属于进程,一个进程可包含多个线程 |
6、进程间的通信方式
在 C++ 中实现进程间通信(IPC)主要依赖于操作系统提供的 API。由于 C++ 标准库本身没有直接提供 IPC 机制,我们通常需要使用 POSIX API(Linux/Unix-like 系统) 或 Windows API。
(1)、 共享内存 (Shared Memory)
这是速度最快的 IPC 方式,适用于需要高频、大数据量交换数据的场景。
(2)、命名管道 (Named Pipes / FIFO)
适用于需要结构化通信的场景,支持双向通信。
(3) 消息队列 (Message Queue)
(4) Socket 通信 (本地套接字)
最适合网络通信,也可用于本地进程间通信。
(5)使用信号 (Signals) 进行简单通信
案例实现:
// 1、共享内存 (Shared Memory)
//Linux/POSIX 实现
#include <iostream>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
// 写入进程 (writer.cpp)
int main() {
// 1. 创建或打开一个共享内存对象
const char* name = "/my_shared_memory";
int shm_fd = shm_open(name, O_CREAT | O_RDWR, 0666);
// 2. 配置共享内存对象的大小
ftruncate(shm_fd, 4096); // 调整大小为 4KB
// 3. 将共享内存映射到进程的地址空间
void* ptr = mmap(0, 4096, PROT_WRITE, MAP_SHARED, shm_fd, 0);
// 4. 向共享内存写入数据
const char* message = "Hello from Writer!";
std::strcpy(static_cast<char*>(ptr), message);
std::cout << "Writer: Data written to shared memory." << std::endl;
// 5. 等待,让读取进程有时间读取
sleep(2);
// 6. 解除映射并关闭
munmap(ptr, 4096);
shm_unlink(name); // 移除共享内存对象
return 0;
}
// 读取进程 (reader.cpp)
int main() {
// 1. 打开已存在的共享内存对象
const char* name = "/my_shared_memory";
int shm_fd = shm_open(name, O_RDONLY, 0666);
// 2. 将共享内存映射到进程的地址空间
void* ptr = mmap(0, 4096, PROT_READ, MAP_SHARED, shm_fd, 0);
// 3. 从共享内存读取数据
std::cout << "Reader: Read from shared memory: "
<< static_cast<char*>(ptr) << std::endl;
// 4. 解除映射并关闭
munmap(ptr, 4096);
close(shm_fd);
return 0;
}
//Windows 实现
#include <iostream>
#include <windows.h>
#include <cstring>
int main() {
// 1. 创建共享内存文件映射
HANDLE hMapFile = CreateFileMapping(
INVALID_HANDLE_VALUE, // 使用物理内存
NULL, // 默认安全属性
PAGE_READWRITE, // 读写权限
0, // 高32位大小
4096, // 低32位大小
L"MySharedMemory"); // 共享内存名称
// 2. 映射到进程地址空间
LPVOID pBuf = MapViewOfFile(
hMapFile, // 映射对象句柄
FILE_MAP_ALL_ACCESS, // 读写权限
0, 0, 4096);
// 3. 写入数据
const char* message = "Hello from Windows!";
strcpy_s(static_cast<char*>(pBuf), 4096, message);
std::cout << "Data written to shared memory. Press Enter to exit..." << std::endl;
std::cin.get();
// 4. 清理
UnmapViewOfFile(pBuf);
CloseHandle(hMapFile);
return 0;
}
//2、命名管道 (Named Pipes / FIFO)
#include <iostream>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>
// 服务器端 (创建管道并读取)
int main() {
const char* fifo_path = "/tmp/my_fifo";
// 创建命名管道
mkfifo(fifo_path, 0666);
std::cout << "Server: Waiting for client..." << std::endl;
// 打开管道进行读取
int fd = open(fifo_path, O_RDONLY);
char buffer[100];
read(fd, buffer, sizeof(buffer));
std::cout << "Server received: " << buffer << std::endl;
close(fd);
unlink(fifo_path); // 删除管道文件
return 0;
}
// 客户端 (写入管道)
int main() {
const char* fifo_path = "/tmp/my_fifo";
// 打开管道进行写入
int fd = open(fifo_path, O_WRONLY);
const char* message = "Hello from Client!";
write(fd, message, strlen(message) + 1);
close(fd);
return 0;
}
//3、 消息队列 (Message Queue)
#include <iostream>
#include <mqueue.h>
#include <cstring>
int main() {
struct mq_attr attr;
attr.mq_flags = 0;
attr.mq_maxmsg = 10;
attr.mq_msgsize = 1024;
attr.mq_curmsgs = 0;
// 创建消息队列
mqd_t mq = mq_open("/my_queue", O_CREAT | O_RDWR, 0666, &attr);
const char* message = "Hello Message Queue!";
mq_send(mq, message, strlen(message) + 1, 0);
char buffer[1024];
mq_receive(mq, buffer, 1024, NULL);
std::cout << "Received: " << buffer << std::endl;
mq_close(mq);
mq_unlink("/my_queue");
return 0;
}
//4、Socket 通信 (本地套接字)
// 服务器端
#include <iostream>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <cstring>
int main() {
int server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
sockaddr_un server_addr{};
server_addr.sun_family = AF_UNIX;
strcpy(server_addr.sun_path, "/tmp/my_socket");
unlink("/tmp/my_socket"); // 确保路径可用
bind(server_fd, (sockaddr*)&server_addr, sizeof(server_addr));
listen(server_fd, 5);
std::cout << "Server waiting for connection..." << std::endl;
int client_fd = accept(server_fd, nullptr, nullptr);
char buffer[100];
read(client_fd, buffer, sizeof(buffer));
std::cout << "Server received: " << buffer << std::endl;
close(client_fd);
close(server_fd);
unlink("/tmp/my_socket");
return 0;
}
//使用信号 (Signals) 进行简单通信
#include <iostream>
#include <csignal>
#include <unistd.h>
void signal_handler(int signal) {
std::cout << "Received signal: " << signal << std::endl;
}
int main() {
// 注册信号处理函数
signal(SIGUSR1, signal_handler);
std::cout << "My PID: " << getpid() << std::endl;
std::cout << "Waiting for signal... (Send SIGUSR1 to this PID)" << std::endl;
// 等待信号
pause();
return 0;
}
//使用方式
# 编译运行
g++ signal_example.cpp -o signal_example
./signal_example
# 从另一个终端发送信号
kill -SIGUSR1 <pid>
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 高性能、大数据量 | 共享内存 | 速度最快,无需数据拷贝 |
| 结构化通信 | 命名管道 | 支持双向通信,有结构 |
| 跨网络通信 | Socket | 最通用,支持本地和远程 |
| 简单事件通知 | 信号 | 轻量级,适合简单通知 |
| 异步消息传递 | 消息队列 | 支持异步通信和消息优先级 |
7、僵尸进程
僵尸进程是已经执行完毕、但其退出状态尚未被父进程回收的进程。
技术原理:进程的生死轮回
在 Unix/Linux 系统中,一个进程终止时,它并不会立刻从系统里完全消失。内核会做两件事:
-
释放它占用的几乎所有资源(内存、文件描述符等)。
-
在系统的进程表中保留一个极小的条目(Zombie 条目),这个条目里保存了该进程的退出状态和一些计时信息。
这个条目会一直保留,直到父进程通过 wait() 或 waitpid() 系统调用来读取这个退出状态。一旦父进程读取了状态,这个僵尸条目就会被系统回收,进程才算是被彻底销毁。
如果父进程一直不调用 wait(),那么这个僵尸进程就会一直存在。
如何处理僵尸进程
(1) 杀死父进程(最常用)
僵尸进程的存在是因为父进程没有 wait()。如果杀死父进程,内核会将这些僵尸进程的“抚养权”移交給 init 进程(PID = 1)。init 进程会定期调用 wait() 来清理这些僵尸进程。这是清理僵尸进程最简单有效的方法。
(2) 编写代码时预防(最佳实践)
在父进程代码中,显式地调用 wait() 或 waitpid() 来回收子进程。
(3)使用信号处理(异步处理)
父进程可以捕获 SIGCHLD 信号(当子进程状态改变时,内核会向父进程发送此信号),并在信号处理函数中调用 wait()
//(1). 杀死父进程(最常用)
# 1. 找到僵尸进程的父进程ID (PPID)
ps -eo pid,ppid,state,comm | grep Z (//Z 就代表 Zombie)
# 2. 杀死父进程
kill -9 <parent_pid>
(2). 编写代码时预防(最佳实践)
#include <iostream>
#include <unistd.h>
#include <sys/wait.h> // for wait()
int main() {
pid_t child_pid = fork();
if (child_pid == 0) {
// 子进程
std::cout << "Child is running." << std::endl;
_exit(0);
} else {
// 父进程
std::cout << "Parent is waiting for child..." << std::endl;
wait(nullptr); // 等待并回收任何一个子进程
// 或者用 waitpid(child_pid, nullptr, 0); 等待指定子进程
std::cout << "Parent has reaped the child. No zombie!" << std::endl;
sleep(5);
}
return 0;
}
(3). 使用信号处理(异步处理)
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
void reap_child(int sig) {
int saved_errno = errno; // 保存errno以防被wait修改
while (waitpid(-1, NULL, WNOHANG) > 0) {} // 非阻塞地回收所有已终止的子进程
errno = saved_errno;
}
int main() {
// 设置 SIGCHLD 的信号处理函数
signal(SIGCHLD, reap_child);
pid_t child_pid = fork();
if (child_pid == 0) {
// 子进程
_exit(0);
} else {
// 父进程可以做自己的事,子退出时会触发信号处理函数自动回收
sleep(10);
}
return 0;
}
8、孤儿进程
孤儿进程是指其父进程已经终止或退出,但自身仍在运行的子进程。
技术原理:操作系统如何管理孤儿进程?
在 Linux/Unix 系统中,内核的设计机制确保了不会有进程被“遗弃”而失去管理。其过程如下:
-
父进程终止:无论出于何种原因(正常退出、被杀死等),一个进程终止了。
-
内核检查:内核会遍历进程列表,检查是否有任何进程的父进程ID(PPID)是这个刚刚终止的进程的PID。这些进程就是“即将成为孤儿”的进程。
-
重新父化(Reparenting):内核会将所有这些孤儿进程的父进程ID(PPID)重新设置为 1。
-
init 进程接管:init 进程(或现代系统中的 systemd) 的PID永远是1。它会定期调用
wait()系统调用来回收其任何终止的子进程。因此,当这些孤儿进程最终结束时,init 进程会负责回收它们,确保它们不会变成僵尸进程。
//创建一个孤儿进程
#include <iostream>
#include <unistd.h> // for fork(), getpid(), getppid(), sleep()
int main() {
std::cout << "Parent PID: " << getpid() << std::endl;
pid_t child_pid = fork(); // 创建子进程
if (child_pid == 0) {
// 这是子进程
std::cout << "Child PID: " << getpid() << " started." << std::endl;
std::cout << "Child's original PPID: " << getppid() << std::endl;
std::cout << "Child is going to sleep for 5 seconds. waiting parent exit..." << std::endl;
sleep(5); // 子进程睡眠,等待父进程先退出
// 醒来后,再次检查父进程ID
std::cout << "Child woke up. Now its PPID is: " << getppid() << " (should be 1 - init)" << std::endl;
std::cout << "Child continues its work and will exit normally." << std::endl;
sleep(5); // 模拟子进程继续工作
std::cout << "Child exiting." << std::endl;
} else {
// 这是父进程
std::cout << "Parent created a child with PID: " << child_pid << std::endl;
std::cout << "Parent will exit NOW, making the child an ORPHAN." << std::endl;
// 父进程立即退出,不等待子进程
_exit(0);
}
return 0;
}
9、poll和epoll
poll 和 epoll 都是 Linux 系统中用于 I/O 多路复用 的技术。它们允许一个进程同时监视多个文件描述符(例如网络套接字),来检查是否有可读、可写或异常等事件发生。这样,单个进程/线程就可以高效地管理大量的网络连接,而不需要为每个连接创建一个线程,极大地节省了系统资源。
poll是早期的一种 I/O 多路复用技术,它改进了更古老的 select 的最大文件描述符数量限制。
epoll 是 Linux 2.6 内核引入的,专门为解决 poll/select 在处理大量连接时性能低下的问题而设计。
poll和epoll的区别:
poll是主动轮询socket是否有变化,epoll可以监听多个socket的变化并阻塞等待事件。
介绍 poll 和epoll的工作流程和C++代码实现
好的,我们来详细讲解一下 poll 和 epoll 的工作流程,并用 C++ 代码实现它们。
核心概念:I/O 多路复用
poll 和 epoll 都是 Linux 系统中用于 I/O 多路复用 的技术。它们允许一个进程同时监视多个文件描述符(例如网络套接字),来检查是否有可读、可写或异常等事件发生。这样,单个进程/线程就可以高效地管理大量的网络连接,而不需要为每个连接创建一个线程,极大地节省了系统资源。
poll工作流程
-
准备文件描述符集合: 应用程序创建一个
struct pollfd类型的数组,并为每个需要监视的文件描述符设置一个数组元素。需要指定要监视的fd和关心的事件(如POLLIN表示可读)。 -
调用 poll: 应用程序调用
poll()函数,并将这个数组传递给它。poll()会阻塞(除非设置超时),直到以下情况之一发生:-
一个或多个被监视的文件描述符就绪(发生了关心的事件)。
-
调用被信号中断。
-
设置的超时时间已到。
-
-
内核检查与阻塞: 内核遍历应用程序传递的所有文件描述符,检查其状态。如果没有文件描述符就绪,内核会将进程阻塞,直到有事件发生。
-
返回结果: 当
poll()返回时,它会返回就绪的文件描述符数量。同时,内核会在每个pollfd结构的revents字段中设置该文件描述符上实际发生的事件。 -
应用程序处理: 应用程序遍历整个数组,检查每个
pollfd的revents字段,来判断是哪个文件描述符就绪以及是什么事件,然后进行相应的读写操作。
epoll工作流程
epoll 被设计用来解决 poll 和 select 的性能瓶颈。它包含三个核心系统调用:epoll_create, epoll_ctl, epoll_wait。
-
创建 epoll 实例 (
epoll_create): 在内核中创建一个上下文,返回一个文件描述符(epfd)来代表这个实例。 -
管理监视列表 (
epoll_ctl): 使用EPOLL_CTL_ADD,EPOLL_CTL_MOD,EPOLL_CTL_DEL操作来向epoll实例(epfd)动态地添加、修改或删除需要监视的文件描述符和事件。这个操作只需执行一次,而不是每次调用时都传递整个列表。 -
等待事件 (
epoll_wait): 调用epoll_wait()来等待事件发生。与poll不同,它返回的是一个就绪事件的数组,而不是需要遍历所有被监视的描述符。内核通过一个事件就绪列表直接告诉应用程序哪些文件描述符准备好了,避免了无效的遍历。
工作模式:
-
水平触发 (LT, Level-Triggered): 默认模式。只要文件描述符处于就绪状态(例如,套接字缓冲区中有未读数据),
epoll_wait()就会一直通知你。 -
边缘触发 (ET, Edge-Triggered): 只有当文件描述符状态发生变化时(例如,从不可读变为可读),才会通知一次。在这种模式下,应用程序必须一次性读完所有数据(循环
read直到EAGAIN),否则如果数据没读完,除非再有新数据到来,否则不会收到通知。ET 模式效率更高,但编程更复杂。
案例
//poll
#include <iostream>
#include <poll.h>
#include <unistd.h>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
// 1. 创建监听套接字
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置 SO_REUSEADDR 选项
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 2. 绑定地址和端口
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8080);
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
close(listen_fd);
exit(EXIT_FAILURE);
}
// 3. 开始监听
if (listen(listen_fd, 128) < 0) {
perror("listen");
close(listen_fd);
exit(EXIT_FAILURE);
}
std::cout << "Server listening on port 8080..." << std::endl;
// 4. 初始化 pollfd 数组
const int MAX_CLIENTS = 1024;
struct pollfd fds[MAX_CLIENTS];
int nfds = 0; // 当前监视的文件描述符数量
// 清空数组
for (int i = 0; i < MAX_CLIENTS; ++i) {
fds[i].fd = -1; // 用 -1 表示无效项
fds[i].events = 0;
}
// 5. 将监听套接字加入数组
fds[0].fd = listen_fd;
fds[0].events = POLLIN; // 关心可读事件(即有新连接)
nfds = 1;
while (true) {
// 6. 调用 poll,阻塞等待事件发生
// timeout = -1 表示无限阻塞
int num_ready = poll(fds, nfds, -1);
if (num_ready < 0) {
perror("poll");
exit(EXIT_FAILURE);
}
// 7. 检查所有文件描述符(注意是 nfds,不是 MAX_CLIENTS)
for (int i = 0; i < nfds && num_ready > 0; ++i) {
if (fds[i].fd == -1 || !(fds[i].revents & POLLIN)) {
continue; // 跳过无效项或未就绪项
}
// 7.1 处理监听套接字上的事件(新连接)
if (fds[i].fd == listen_fd) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept");
continue;
}
std::cout << "New connection accepted. FD: " << client_fd << std::endl;
// 将新连接加入到 pollfd 数组中
if (nfds < MAX_CLIENTS) {
fds[nfds].fd = client_fd;
fds[nfds].events = POLLIN; // 监视新套接字的可读事件
nfds++;
std::cout << "Added new client to poll set. Total: " << nfds-1 << std::endl;
} else {
std::cerr << "Too many clients." << std::endl;
close(client_fd);
}
num_ready--;
}
// 7.2 处理客户端套接字上的事件(数据到达)
else {
char buffer[1024];
ssize_t bytes_read = read(fds[i].fd, buffer, sizeof(buffer) - 1);
if (bytes_read <= 0) {
// 连接关闭或出错
if (bytes_read == 0) {
std::cout << "Client (FD: " << fds[i].fd << ") closed connection." << std::endl;
} else {
perror("read");
}
close(fds[i].fd);
// 从数组中移除:用最后一个有效项覆盖当前项,并减少 nfds
fds[i].fd = fds[nfds-1].fd;
fds[i].events = fds[nfds-1].events;
fds[nfds-1].fd = -1;
nfds--;
i--; // 重要:因为当前位置被新项覆盖,需要重新检查
} else {
// 处理收到的数据
buffer[bytes_read] = '\0';
std::cout << "Received from FD " << fds[i].fd << ": " << buffer;
// 回显给客户端
write(fds[i].fd, buffer, bytes_read);
}
num_ready--;
}
}
}
// 关闭监听套接字(实际不会执行到这里)
close(listen_fd);
return 0;
}
//(epoll, LT 模式)
#include <iostream>
#include <sys/epoll.h>
#include <unistd.h>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
const int MAX_EVENTS = 64;
int main() {
// 1. 创建监听套接字 (同上)
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
// ... (绑定和监听代码与 poll 示例完全相同,此处省略)
// 假设 listen_fd 已创建、绑定并开始监听
// 2. 创建 epoll 实例
int epoll_fd = epoll_create1(0);
if (epoll_fd < 0) {
perror("epoll_create1");
close(listen_fd);
exit(EXIT_FAILURE);
}
// 3. 将监听套接字添加到 epoll 实例中
struct epoll_event event;
event.events = EPOLLIN; // 监视可读事件
event.data.fd = listen_fd; // 携带的数据是文件描述符
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event) < 0) {
perror("epoll_ctl: listen_fd");
close(listen_fd);
close(epoll_fd);
exit(EXIT_FAILURE);
}
// 4. 创建用于接收就绪事件的数组
struct epoll_event events[MAX_EVENTS];
std::cout << "Server (epoll) listening on port 8080..." << std::endl;
while (true) {
// 5. 等待事件发生
int num_ready = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (num_ready < 0) {
perror("epoll_wait");
break;
}
// 6. 处理就绪事件 (注意:这里只需遍历就绪的 num_ready 个,而不是所有连接)
for (int i = 0; i < num_ready; ++i) {
int current_fd = events[i].data.fd;
// 6.1 处理新连接
if (current_fd == listen_fd) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept");
continue;
}
std::cout << "New connection accepted. FD: " << client_fd << std::endl;
// 将新客户端套接字设置为非阻塞(为ET模式做准备,LT模式非必须)
// int flags = fcntl(client_fd, F_GETFL, 0);
// fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
// 将新连接添加到 epoll 实例
event.events = EPOLLIN; // LT 模式
// event.events = EPOLLIN | EPOLLET; // ET 模式
event.data.fd = client_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) < 0) {
perror("epoll_ctl: client_fd");
close(client_fd);
}
}
// 6.2 处理客户端数据
else if (events[i].events & EPOLLIN) {
char buffer[1024];
ssize_t bytes_read = read(current_fd, buffer, sizeof(buffer) - 1);
if (bytes_read <= 0) {
// 连接关闭或出错
if (bytes_read == 0) {
std::cout << "Client (FD: " << current_fd << ") closed connection." << std::endl;
} else {
perror("read");
}
close(current_fd);
// 从 epoll 实例中移除
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, current_fd, NULL);
} else {
// 处理数据
buffer[bytes_read] = '\0';
std::cout << "Received from FD " << current_fd << ": " << buffer;
// 回显
write(current_fd, buffer, bytes_read);
// 如果是 ET 模式,可能需要循环 read 直到 EAGAIN
// while ((bytes_read = read(current_fd, buffer, sizeof(buffer))) > 0) {
// // ... 处理所有数据
// }
}
}
// 还可以处理 EPOLLOUT 等其它事件
}
}
// 清理
close(listen_fd);
close(epoll_fd);
return 0;
}
9、算法题:最长公共子串
题目要求编写一个函数来找出多个字符串的最长公共子串。
示例中给出了flower、flow、fight的公共子串是FL。
#include <iostream>
#include <vector>
#include <string>
#include <sstream>
using namespace std;
string longestCommonSubstring(const vector<string>& strs) {
if (strs.empty()) return "";
// 找到最短的字符串作为基准
string base = strs[0];
for (const auto& s : strs) {
if (s.length() < base.length()) {
base = s;
}
}
int n = base.size();
string result = "";
for (int len = n; len > 0; len--) {
for (int start = 0; start <= n - len; start++) {
string substring = base.substr(start, len);
bool found = true;
for (int i = 0; i < strs.size(); i++) {
if (strs[i].find(substring) == string::npos) {
found = false;
break;
}
}
if (found) {
// 找到后继续检查,确保找到最长的
if (substring.length() > result.length()) {
result = substring;
}
}
}
// 如果找到当前长度的公共子串,继续检查更长的可能
if (!result.empty() && result.length() == len) {
break;
}
}
return result;
}
int main() {
vector<string> strs;
string input;
cout << "请输入多个字符串(用空格分隔,按回车结束):" << endl;
cout << "或者输入 'quit' 退出程序" << endl;
while (true) {
cout << "> ";
getline(cin, input);
if (input == "quit" || input == "exit") {
break;
}
if (input.empty()) {
continue;
}
// 使用字符串流分割输入
stringstream ss(input);
string word;
strs.clear();
while (ss >> word) {
strs.push_back(word);
}
if (strs.size() < 2) {
cout << "请至少输入2个字符串" << endl;
continue;
}
string result = longestCommonSubstring(strs);
cout << "输入的字符串: ";
for (const auto& s : strs) {
cout << "\"" << s << "\" ";
}
cout << endl;
if (result.empty()) {
cout << "没有找到公共子串" << endl;
} else {
cout << "最长公共子串: \"" << result << "\"" << endl;
cout << "长度: " << result.length() << endl;
}
cout << "------------------------" << endl;
}
return 0;
}
10、widows上如何编写cpp 代码运行显示
使用 Visual Studio (IDE)
步骤 1:下载和安装
-
访问 Visual Studio 官网下载页面:https://visualstudio.microsoft.com/zh-hans/downloads/
-
下载 Community 2022 版本,这是免费的。
-
运行安装程序。在“工作负载”选择界面,必须勾选【使用 C++ 的桌面开发】。
-
右侧的安装详细信息中,可以默认全选。然后点击“安装”即可。这个过程会下载大约 5-10 GB 的文件,请耐心等待。
步骤 2:创建并运行你的第一个 C++ 程序
-
打开 Visual Studio,选择“创建新项目”。
-
在项目模板中,选择 “控制台应用”,然后点击“下一步”。
-
为你的项目命名(例如
HelloWorld),选择保存位置,然后点击“创建”。 -
Visual Studio 会自动为你生成一个简单的 C++ 代码框架。你会看到类似下面的代码:
#include <iostream> int main() { std::cout << "Hello World!\n"; } -
运行程序:点击顶部菜单栏的 “本地 Windows 调试器” 按钮(或按快捷键
F5)。 -
此时,Visual Studio 会先编译(Compile)和链接(Link)你的代码,生成可执行文件(
.exe),然后自动运行它。 -
一个黑色的命令行窗口会弹出,并显示
Hello World!。程序运行完后窗口会立即关闭,如果想让窗口暂停,可以在return 0;前加上system("pause");(不推荐用于正式项目)或更好的方法是在 Visual Studio 中按Ctrl + F5(“开始执行(不调试)”),它会自动暂停。

被折叠的 条评论
为什么被折叠?



