目录
🌼前言
另一家面试的不足
补充:2025/6/12,面了另一家测开,某大中厂游戏测试开发(偏向开发,各种测试工具),一面二面,4 个不足
① 算法太久没刷了,easy 和简单的 medium 都做的磕磕绊绊
② 必须要针对当前开发的项目,结合当前各大公司,常用的压测工具,进行全方位的针对性测试,并由测试结果推导,去修改代码逻辑,产生性能提升,稳定性提升等等
③ 测试用例的场景题,没有准备,比如《王者荣耀》登录的测试用例,能不能快速说出三四十条,比如猎魂觉醒,新出的武器,能不能在 5 分钟内讲出 40 条可行的测试点
④ 遇到自己不熟悉,不会的部分,不要说自己最近没用过,没玩过,要说类似的,自己会的,去引导面试官,比如说,“我知道另一个类似的 xxx 机制 / 框架 / 技术 / 逻辑,可以讲这个吗”
真正的前言
- 内心 os:最近一周还通宵 3 天打游戏,对,就是那个《英勇之地》,steam 同款手游
- 突然来了面试,一面还过了,那就好好准备吧
影石一面过了,纯属运气,投的测试岗,分到了开发(拿 offer,运气占 50%)
刚好之前接触了一点 gdb 调试,vs 调试,以及C语言内存泄露等等
二面业务面,应该会针对项目,C语言基础,更深入,难度也更大
不管了,尽力就好(指的是,写这一篇博客来复盘)
半吊子水平,啥也不会,只能靠写博客安慰下自己的样子
PS:面试官真好,这不得尽力,就算二面失之交臂,2 天时间,留下了这篇博客,也不亏
因为昨天问了两个C语言的知识
比如函数指针,就是函数回调(理解成函数传参的二级指针了)都没答上来
然后项目的模块,昨天自我介绍,没有提到,只是说了 25 分钟实习产出
面试官对你这个项目做了什么,还不是很清楚,加上是一年半前做的项目了
回答时也没回答的很透彻,比如问你在哪一层上做的注册登录,或者状态机
还有蓝牙协议栈之类的,和项目中的 TCP/IP 协议栈的相似点
以及C语言中,怎么定位内存泄露等等
🗡🗡大二,面了 4 次,0 offer🗡🗡
🗡🗡25年2月,面了 6 次,1 offer,一面 20 分钟,项目吹了 15 分钟,直接发 offer 了😂🗡🗡
🗡🗡25年6月,影石就是第一次面试,要是第 1 次就能有 offer,后面就不面了🗡🗡
① 其实可以作个量化,同等学历 / 技术,你面第 1 次拿不到 offer,面第 20 次能拿到大厂测试或者小厂开发的 offer,面第 50 次,大厂开发二面三面随便进了....
② 面的越多,复盘越多,你对面试的了解就越多,甚至面试官问了上句,你要能预判了他后面想问的东西(他会对什么东西感兴趣呢,你准备好自己的稿子了吗),进一步引导面试的
节奏
(不同岗位问的东西,其实都差不多;
同一家公司,今年问的,和去年问的,也差不多😂
甚至我对比过某些大厂,5年前,也就是 2020 年的面经,
和今年,2025 年的面经,小部分甚至就是原题,大部分雷同,换汤不换药😂)
③
a. 面试官关心的是,你的C语言,C++,项目,算法能力,是否适配他们的要求
b. 到了公司,能不能快速上手
c. 或者有没有工作相关的技能,以及你的学习能力和对语言基础 / 业务的思考,能不能替他们解决业务中的痛点,分担压力(所以,从这个方向出发,去准备你的面试)
d. 能不能写出不内存泄露,没有逻辑 bug,代码风格良好,项目上线后不会出问题的代码,甚至协助同事去优化代码
结合 TinyWebServer 专栏食用更佳:TinyWebServer_千帐灯无此声的博客-CSDN博客
后记
拿 offer 了....真的是运气....同时感谢第一段实习,没有第一段实习打底,肯定面不过的
这几天找房子,然后,另外出一篇博客,复习下 C 语言,保证入职能快速上手项目
第一段实习,中望,是2025年2月,第一次面试,拿的 offer
第二段实习,影石,是2025年6月,第一次面试,拿的 offer
就是,拿 offer 了,不管测开还是开发,都好好干,可能比较缺人吧,加上运气使然
自身的技术其实真的很烂,所以更得多点加班,赶上进度,还有借助 cursor 和 公司文档
没事,多问问题,问过的问题都记录下来,防止后面遇到了,又去打扰别人
而且,不要逮着 mentor 或者某个同事薅羊毛,要雨露均沾,都问问
就像在中望一样,因为自己菜嘛哈哈,实习生 5 点半可以下班,自己前期经常呆到 7 点
那么影石,如果实习生,一般 8 点下班的话,自己呆到 9 点,也不是不行哈哈
🌹一,TinyWebServer && C语言
对于下列基础,或者面试要点,可以结合 cursor 进行进一步了解和背诵,以便面试可以正确回答
1,和蓝牙 WiFi 的联系
尽管 Linux 服务器框架,是网络服务器,但是和蓝牙 WiFi 嵌入式开发有很多相似点
① 网络编程基础
- Socket 编程 --> 蓝牙/WiFi 都基于 Socket 通信
- TCP/IP 协议栈 --> 无线通信协议栈
- 数据包解析 --> 蓝牙/WiFi 帧解析
② 多线程并发
- 线程池 --> 蓝牙多连接
- 线程竞争(锁,信号量) --> 硬件资源竞争
- 异步事件处理 --> 中断处理
③ 底层系统编程
- 文件描述符 --> 硬件设备句柄
- epoll 事件驱动 --> 硬件中断驱动
- 内存管理 --> 嵌入式内存优化
2,基础概念
Ⅰ Socket, epoll, TCP 的联系
🌐 网络通信基础概念关系图
================================================================================
【客户端】 【服务器端】
┌─────────────────┐ ┌─────────────────┐
│ Web浏览器 │ │ TinyWebServer │
│ │ │ │
│ ┌───────────┐ │ │ ┌───────────┐ │
│ │ Socket │ │ │ │ Socket │ │
│ │ (连接) │ │◄─────────►│ │ (监听) │ │
│ └───────────┘ │ │ └───────────┘ │
└─────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ TCP │ │ TCP │
│ (传输层) │◄─────────►│ (传输层) │
│ 三次握手建连 │ │ listen/accept │
└─────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ IP │ │ IP │
│ (网络层) │◄─────────►│ (网络层) │
└─────────────────┘ └─────────────────┘
🔍 服务器端详细机制:
┌─────────────────────────────────────────────────────────────────┐
│ TinyWebServer 内部机制 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ listenfd │ │ epollfd │ │ clientfd │ │
│ │ (监听socket) │ │ (事件管理器)│ │ (客户连接) │ │
│ │ │ │ │ │ │ │
│ │ 负责监听 │───→│ 统一管理 │◄──│ 数据传输 │ │
│ │ 新连接请求 │ │ 所有事件 │ │ 读写操作 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 内核空间 │ │
│ │ │ │
│ │ socket缓冲区 ◄──► epoll红黑树 ◄──► 网络数据包 │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Ⅱ 核心概念
① listenfd,epollfd,clientfd 区别
listenfd:监听 Socket,用于接收新的客户端连接请求
epollfd:epoll 实例,管理需要监听的文件描述符
clientfd:客户端连接 Socket,与具体客户端通信,recv() / send()
② 为什么 epoll_wait() 先检测,而不是 TCP 先发送
因为服务器是被动响应的,不是主动推动模式
时间线演示: T1: 服务器启动 Server: socket() → bind() → listen() → epoll_wait() 状态:等待连接,进入阻塞状态 T2: 客户端发起连接 Client: socket() → connect(server_ip:port) 网络:发送SYN包到服务器 T3: 服务器检测到连接事件 Server: epoll_wait() 返回,检测到listenfd有EPOLLIN事件 原因:内核接收到SYN包,将连接放入监听队列 T4: 服务器接受连接 Server: accept() → 创建新的clientfd 网络:三次握手完成,连接建立 T5: 客户端发送HTTP请求 Client: send("GET /index.html HTTP/1.1\r\n...") 网络:数据包通过已建立的TCP连接传输 T6: 服务器检测到数据事件 Server: epoll_wait() 返回,检测到clientfd有EPOLLIN事件 原因:内核接收缓冲区有数据 关键理解: ❓ 为什么服务器要"等待"? ✅ 因为服务器不知道客户端什么时候会连接或发送数据 ✅ epoll_wait()是"被动等待"机制,响应客户端的主动请求 ✅ 这就是"服务器"的本质:为客户端提供服务,而不是主动推送
③ epoll 的作用机制
epoll 和 传统方法对比
- select():O(n) 时间复杂度,fd 数量限制 1024,需要遍历所有 fd
- poll():O(n),无 fd 数量限制,需要遍历所有 fd
- epoll():O(1),事件驱动,内核直接返回就绪事件
epoll 核心机制
- 将 fd 注册到 epoll 实例
- epoll_wait() 阻塞等待,直到有事件就绪
- 内核直接返回就绪事件列表,不需要轮询
3,项目架构(分模块)
① 架构图
🌐 TinyWebServer 完整架构图 ================================================================================ 【网络层次模型】 ┌─────────────────────────────────────────────────────────────┐ │ 应用层 (Application Layer) │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 日志系统 │ │ 配置管理 │ │ Web页面 │ │ │ │ (LOG) │ │ (Config) │ │ (HTML/CSS) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────────┘ ║ ┌─────────────────────────────────────────────────────────────┐ │ 会话层/表示层 (Session/Presentation) │ │ ┌─────────────────────────────────────┐ │ │ │ HTTP 协议解析模块 │ │ │ │ │ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │主状态机 │ │从状态机 │ │ │ │ │ │(解析请求行) │ │(替换/r/n) │ │ │ │ │ └─────────────┘ └─────────────┘ │ │ │ │ ║ ║ │ │ │ │ ┌─────────────────────────────┐ │ │ │ │ │ HTTP_CODE 状态管理 │ │ │ │ │ └─────────────────────────────┘ │ │ │ └─────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ║ ┌─────────────────────────────────────────────────────────────┐ │ 传输层 (Transport Layer) │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ 线程池 │◄────────────►│ 连接管理 │ │ │ │(ThreadPool) │ │(Connection) │ │ │ │ │ │ │ │ │ │工作线程队列 │ │ ┌─────────┐ │ │ │ │任务分发机制 │ │ │定时器 │ │ │ │ │生产者消费者 │ │ │链表管理 │ │ │ │ └─────────────┘ │ └─────────┘ │ │ │ └─────────────┘ │ └─────────────────────────────────────────────────────────────┘ ║ ┌─────────────────────────────────────────────────────────────┐ │ 网络层 (Network Layer) │ │ ┌─────────────────────────────┐ │ │ │ epoll 事件驱动 │ │ │ │ │ │ │ │ ┌─────────┐ ┌─────────┐ │ │ │ │ │监听事件 │ │I/O事件 │ │ │ │ │ │EPOLLIN │ │EPOLLOUT │ │ │ │ │ └─────────┘ └─────────┘ │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ║ ┌─────────────────────────────────────────────────────────────┐ │ 数据链路层 (Data Link Layer) │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ Socket通信 │ │ MySQL连接池 │ │ │ │ │ │ │ │ │ │ TCP Socket │ │ 数据库连接 │ │ │ │ 非阻塞I/O │ │ RAII管理 │ │ │ │ 地址复用 │ │ 连接复用 │ │ │ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────────┘ 📊 数据流向图: 客户端请求 → Socket接收 → epoll事件触发 → 线程池分发 → HTTP解析(主从状态机) → 业务处理 → 数据库操作 → 响应生成 → Socket发送 → 日志记录
② 核心模块(9 大模块)
Ⅰ epoll 事件驱动核心(网络层)
位置:网络层,负责 I/O 多路复用
任务:监听 / 分发网络事件
实现思路:
eventLoop() 核心逻辑: while (!stop_server) { // 等待事件就绪 number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1); // 遍历所有就绪事件 for (i = 0; i < number; i++) { sockfd = events[i].data.fd; if (sockfd == listenfd) { // 新连接事件 → 调用accept() dealclinetdata(); } else if (events[i].events & EPOLLIN) { // 读事件 → 分发给线程池 dealwithread(sockfd); } else if (events[i].events & EPOLLOUT) { // 写事件 → 发送响应 dealwithwrite(sockfd); } } }
难点:
- 边缘触发 vs 水平触发:ET 模式需要一次性读完所有数据
- 事件处理顺序:信号处理优先级最高,然后是连接事件
- 非阻塞 I/O 配合:必须设置 Socket 为非阻塞模式
Ⅱ HTTP 协议解析(会话层 / 表示层)
位置:会话层,负责 HTTP 协议的解析和处理
核心:主从状态机模式
主从状态机转换:
CHECK_STATE_REQUESTLINE → CHECK_STATE_HEADER → CHECK_STATE_CONTENT process_read() 核心逻辑: while (继续解析条件) { switch (m_check_state) { case CHECK_STATE_REQUESTLINE: // 解析 "GET /index.html HTTP/1.1" parse_request_line(text); break; case CHECK_STATE_HEADER: // 解析 "Host: localhost:9006" parse_headers(text); break; case CHECK_STATE_CONTENT: // 解析POST数据 parse_content(text); break; } }
从状态机职责:
- 依次读取每一行,将末尾的 \r\n 替换成 \0\0(从缓冲区读取)
- 提供 LINE_OK,LINE_BAD,LINE_OPEN 三种状态(成功,失败,读取中)
- 为主状态机提供行级数据(字符串形式交给主状态机)
Ⅲ 线程池并发处理(传输层)
位置:传输层,负责并发任务处理
模式:生产者消费者模式
实现思路:
工作线程循环: while (true) { // 等待任务信号 m_queuestat.wait(); // 加锁取任务 m_queuelocker.lock(); request = m_workqueue.front(); m_workqueue.pop_front(); m_queuelocker.unlock(); // 处理任务 if (Reactor模式) { if (读任务) { request->read_once(); request->process(); } else { request->write(); } } }
Reactor vs Proactor:
- Reactor:线程池负责 I/O 操作 + 任务处理
- Proactor:主线程负责 I/O,线程池只处理业务逻辑
Ⅳ 定时器(传输层)
位置:传输层,管理连接生命周期
数据结构:升序双向链表
核心功能:
- 连接超时检测:定期清理超时连接
- 资源回收:自动关闭无效连接
- 链表维护:按超时时间升序排列,便于批量清理
Ⅴ 数据库连接池(数据链路层)
位置:数据链路层,管理数据库资源
设计:单例模式 + 对象池模式
核心机制:
连接获取: GetConnection() { reserve.wait(); // P操作,等待可用连接 lock.lock(); con = connList.front(); // 取出连接 connList.pop_front(); lock.unlock(); return con; } 连接释放: ReleaseConnection(con) { lock.lock(); connList.push_back(con); // 归还连接 lock.unlock(); reserve.post(); // V操作,释放信号 }
RAII 管理:使用 connectionRAII 类,自动管理连接生命周期
Ⅵ 日志系统(应用层)
位置:应用层,记录系统运行状态
设计模式:单例模式 + 生产者消费者模式
核心:
- 分级日志:DEBUG,INFO,WARN,ERROR 四个级别
- 按天分文件:自动按日期创建新的日志文件
- 异步写入:使用阻塞队列,实现异步日志写入
(怎么实现异步的?日志初始化函数 Log::init() 中,有个 max_queue_size > 0 就异步)- 线程安全:多线程环境下的安全日志记录
(怎么保证多线程安全?C++11的懒汉单例模式,局部静态变量,以及私有构造析构)
(单例:全局唯一日志实例 + 资源共享 / 所有线程共享一个日志文件)Ⅶ 线程池同步(传输层)
位置:传输层,管理工作线程的任务分配和同步
数据结构:生产者-消费者队列 + 信号量机制
核心功能:
- 任务队列管理:使用 std::list<T *> m_workqueue 存储待处理任务
- 信号量控制:通过 sem m_queuestat 实现资源计数,控制任务数量
- 互斥锁保护:locker m_queuelocker 保护任务队列临界区的访问
- 生产者消费者:主线程添加任务(append),工作线程取出任务(run)
关键代码:
// 任务添加(生产者) bool append(T *request, int state) { m_queuelocker.lock(); // 加锁保护 m_workqueue.push_back(request); // 添加任务 m_queuelocker.unlock(); // 解锁 m_queuestat.post(); // V操作,通知消费者 } // 任务处理(消费者) void run() { while (true) { m_queuestat.wait(); // P操作,等待任务 m_queuelocker.lock(); // 加锁获取任务 T *request = m_workqueue.front(); m_workqueue.pop_front(); m_queuelocker.unlock(); // 解锁 request->process(); // 处理任务 } }
Ⅷ 阻塞队列通信机制(会话层)
位置:会话层,日志系统异步写入
数据结构:循环队列 + 条件变量
核心:
- 线程间数据传递:阻塞队列,在写线程和日志线程间传递消息
- 条件变量通信:使用 cond m_cond 实现线程间的,等待唤醒机制
- 超时处理:支持带超时的 pop 操作,避免无限等待
- 队列状态管理:自动管理队列 满 / 空 的状态
关键代码:
// 生产者推送数据 bool push(const T &item) { m_mutex.lock(); m_array[m_back] = item; // 数据写入 m_size++; m_cond.broadcast(); // 广播通知所有等待线程 m_mutex.unlock(); } // 消费者获取数据 bool pop(T &item) { m_mutex.lock(); while (m_size <= 0) { // 队列为空时等待 m_cond.wait(m_mutex.get()); // 条件变量等待 } item = m_array[m_front]; // 取出数据 m_size--; m_mutex.unlock(); }
Ⅸ 信号管道通信(网络层)
位置:网络层,异步信号处理机制
数据结构:Unix 域 Socket 管道对
核心:
- 信号异步处理:异步信号转换为同步的 I/O 事件
- 管道通信:通过 socketpair 创建的管道传递信号信息
- epoll 统一管理:将信号管道加入 epoll,统一事件处理
- 进程间解耦:信号处理函数,只是写管道,主循环负责处理
关键代码:
// 信号处理器(写端) void sig_handler(int sig) { char a = sig; send(u_pipefd[1], &a, 1, 0); // 将信号值写入管道 } // 主循环处理(读端) bool dealwithsignal(bool &timeout, bool &stop_server) { char signals[1024]; int ret = recv(m_pipefd[0], signals, sizeof(signals), 0); for (int i = 0; i < ret; ++i) { switch (signals[i]) { // 根据信号值执行不同逻辑 case SIGALRM: timeout = true; break; case SIGTERM: stop_server = true; break; } } }
③ 完整流程
📊 完整请求处理流程: 1. 网络层:epoll_wait() 检测到读事件 ↓ 2. 传输层:dealwithread() 将任务加入线程池队列 ↓ 3. 传输层:工作线程从队列取出任务 ↓ 4. 会话层:调用 http_conn::process() ↓ 5. 会话层:主状态机解析HTTP请求 ├─ 从状态机解析行格式 ├─ 解析请求行、请求头、请求体 └─ 生成HTTP_CODE状态 ↓ 6. 应用层:根据请求类型处理业务逻辑 ├─ 静态文件服务(mmap内存映射) ├─ 动态页面处理(登录注册) └─ 数据库操作(连接池获取连接) ↓ 7. 会话层:生成HTTP响应 ↓ 8. 网络层:epoll检测写事件就绪 ↓ 9. 传输层:dealwithwrite() 发送响应数据 ↓ 10. 应用层:写入访问日志
4,GDB详细调试过程
第一步:发现服务器卡死
# 服务器运行中突然无响应 ps aux | grep webserver # 发现进程存在但CPU占用率为0% # 检查网络连接 netstat -tlnp | grep 9006 # 端口在监听,但无法建立新连接
第二步:使用GDB附加进程
# 附加到卡死的进程 sudo gdb -p 12345 # GDB输出 (gdb) Attaching to process 12345 Reading symbols from /path/to/webserver... 0x00007f8b8c0d7 in __pthread_mutex_lock_full () from /lib/x86_64-linux-gnu/libpthread.so.0
第三步:分析所有线程状态
(gdb) info threads Id Target Id Frame * 1 Thread 0x7f8b8c 0x00007f8b8c0d7 in __pthread_mutex_lock_full () 2 Thread 0x7f8b8d 0x00007f8b8c0d7 in __pthread_mutex_lock_full () 3 Thread 0x7f8b8e 0x00007f8b8c0d7 in __pthread_mutex_lock_full () 4 Thread 0x7f8b8f 0x00007f8b8c8a1 in sem_wait () from /lib/x86_64-linux-gnu/libpthread.so.0 5 Thread 0x7f8b90 0x00007f8b8c1f2 in epoll_wait () from /lib/x86_64-linux-gnu/libc.so.6 # 🚨 发现:线程1-3都在mutex_lock,线程4在sem_wait,线程5在epoll_wait
第四步:查看每个线程的详细堆栈
# 查看线程1堆栈 (gdb) thread 1 (gdb) bt #0 0x00007f8b8c0d7 in __pthread_mutex_lock_full () from /lib/x86_64-linux-gnu/libpthread.so.0 #1 0x0000555555556789 in locker::lock() at lock/locker.h:25 #2 0x0000555555557234 in threadpool<http_conn>::run() at threadpool/threadpool.h:187 #3 0x0000555555557456 in threadpool<http_conn>::worker(void*) at threadpool/threadpool.h:145 #4 0x00007f8b8c0ca6ba in start_thread () from /lib/x86_64-linux-gnu/libpthread.so.0 # 查看线程2堆栈 - 同样卡在lock() (gdb) thread 2 (gdb) bt #0 0x00007f8b8c0d7 in __pthread_mutex_lock_full () #1 0x0000555555556789 in locker::lock() at lock/locker.h:25 #2 0x0000555555557234 in threadpool<http_conn>::run() at threadpool/threadpool.h:187 # 查看线程4堆栈 - 卡在信号量等待 (gdb) thread 4 (gdb) bt #0 0x00007f8b8c8a1 in sem_wait () from /lib/x86_64-linux-gnu/libpthread.so.0 #1 0x0000555555556823 in sem::wait() at lock/locker.h:67 #2 0x0000555555557198 in threadpool<http_conn>::run() at threadpool/threadpool.h:185
第五步:检查关键变量状态
# 切换到主线程 (gdb) thread 1 # 查看信号量状态(需要手动计算) (gdb) p m_queuestat # 无法直接看到信号量值,需要查看相关变量 # 查看队列状态 (gdb) p m_workqueue.size() $1 = 0 # 🚨 队列为空! # 查看锁状态 (gdb) p m_queuelocker # 显示锁对象信息 # 查看当前正在等待锁的线程数 (gdb) info threads # 发现多个线程都在等待同一个锁
第六步:分析死锁形成过程
# 使用pstack查看所有线程状态(在另一个终端) sudo pstack 12345 # 输出显示: Thread 8 (Thread 0x7f8b8c): #0 pthread_mutex_lock_full #1 threadpool<http_conn>::run() (threadpool.h:187) Thread 7 (Thread 0x7f8b8d): #0 pthread_mutex_lock_full #1 threadpool<http_conn>::run() (threadpool.h:187) # 🚨 发现问题:多个线程都卡在第187行获取锁
😔二,C 语言基础
1. 指针与数组的本质区别
基本概念对比
- 指针的本质:指针是一个变量,里面存放的是另一个变量的内存地址
- 数组名的本质:数组名是一个常量,代表数组第一个元素的地址,不可改变
关键区别展示
- 可变性差异:
- char *p = "hello"; 后可以执行 p++,让p指向下一个字符(字符串中的字符,在内存中是连续的,这里 p 只想第一个字符)
- char arr[] = "hello"; 后不能执行 arr++,因为arr是地址常量
- 内存分配方式:
- 指针变量本身占用4/8字节(根据系统位数),指向的内容在别处
- 数组在栈上连续分配所有元素的空间
函数传参的陷阱
- 数组退化现象:void func(char arr[]) 实际等价于 void func(char *arr)
- 为什么会退化:C语言不允许按值传递整个数组,自动转换为指针传递
- 实际影响:函数内部无法通过sizeof获得原数组大小
2. 动态内存管理的完整机制
三大内存分配函数对比
- malloc:
功能:分配指定字节数的,未初始化的,内存
特点:速度快,但内容不确定(可能是垃圾数据)
使用场景:知道需要多少内存,不关心初始值时#include <stdio.h> #include <stdlib.h> int main() { // 分配5个int的内存 int *arr = (int*)malloc(5 * sizeof(int)); if (arr == NULL) { printf("分配失败!\n"); return -1; } // 查看未初始化的垃圾数据 printf("malloc未初始化内容: "); for (int i = 0; i < 3; i++) { printf("%d ", arr[i]); // 输出随机值 } printf("\n"); // 手动赋值 for (int i = 0; i < 5; i++) { arr[i] = i + 1; } printf("手动初始化后: "); for (int i = 0; i < 5; i++) { printf("%d ", arr[i]); // 输出: 1 2 3 4 5 } printf("\n"); free(arr); return 0; }
- calloc:
功能:分配内存并初始化为 0
特点:相当于malloc + memset,稍慢但更安全
使用场景:需要干净内存时,如结构体数组初始化#include <stdio.h> #include <stdlib.h> int main() { // 分配5个int的内存,自动初始化为0 int *arr = (int*)calloc(5, sizeof(int)); if (arr == NULL) { printf("分配失败!\n"); return -1; } // 查看自动初始化的内容 printf("calloc自动初始化: "); for (int i = 0; i < 5; i++) { printf("%d ", arr[i]); // 输出: 0 0 0 0 0 } printf("\n"); free(arr); return 0; }
- realloc:
功能:重新调整已分配内存的大小
智能机制:原地扩展或复制到新位置
注意事项:可能返回不同地址,原指针可能失效#include <stdio.h> #include <stdlib.h> int main() { // 初始分配3个int int *arr = (int*)malloc(3 * sizeof(int)); if (arr == NULL) return -1; // 初始化数据 for (int i = 0; i < 3; i++) { arr[i] = i + 1; } printf("原始数组(3个): "); for (int i = 0; i < 3; i++) { printf("%d ", arr[i]); // 输出: 1 2 3 } printf("\n"); // 扩展到6个int arr = (int*)realloc(arr, 6 * sizeof(int)); if (arr == NULL) return -1; // 为新位置赋值 for (int i = 3; i < 6; i++) { arr[i] = i + 1; } printf("扩展后(6个): "); for (int i = 0; i < 6; i++) { printf("%d ", arr[i]); // 输出: 1 2 3 4 5 6 } printf("\n"); // 缩减到2个int arr = (int*)realloc(arr, 2 * sizeof(int)); printf("缩减后(2个): "); for (int i = 0; i < 2; i++) { printf("%d ", arr[i]); // 输出: 1 2 } printf("\n"); free(arr); return 0; }
内存管理的最佳实践
- 配对原则:每个malloc必须对应一个free
- 防野指针:free后立即设置指针为NULL
- 检查返回值:malloc可能失败返回NULL
- 内存泄漏检测:使用工具如valgrind定期检查
3. 函数指针的深度应用
概念
文字说明:
Ⅰ 函数指针的标准声明格式为:返回类型 (*指针名)(参数列表)
Ⅱ 赋值时 --> 函数名或取地址符(add 或 &add),等价
Ⅲ 调用时 --> 用 func_ptr(a, b) 或 (*func_ptr)(a, b),等价
代码示例:
#include <stdio.h> // 函数指针起别名:指向返回void、参数为(int, void*)的函数 typedef void (*bt_callback)(int event, void* data); // 符合函数指针的实现 void on_event(int event, void* data) { printf("Event %d triggered, data = %s\n", event, (char*)data); } // 函数指针作为参数传入另一个函数 --> 也就是回调函数 void register_and_trigger(bt_callback cb) { char msg[] = "hello"; // 通过函数指针cb调用回调函数 cb(100, msg); } int main() { // 传递回调函数指针 register_and_trigger(on_event); return 0; }
回调机制
文字说明:
Ⅰ 回调机制常用函数指针作为参数,将处理逻辑“反向”传递给库或框架
Ⅱ 常见用法是用 typedef 定义回调类型,然后在需要时传递和调用
代码示例:
#include <stdio.h> // 定义回调类型:指向返回void、参数为(int, void*)的函数 typedef void (*bt_callback)(int event, void* data); // 回调函数实现,符合bt_callback类型 void on_event(int event, void* data) { printf("Event %d triggered, data = %s\n", event, (char*)data); } // 注册和触发回调 void register_and_trigger(bt_callback cb) { char msg[] = "hello"; // 通过函数指针cb调用回调函数 cb(100, msg); } int main() { // 传递回调函数指针 register_and_trigger(on_event); return 0; }
状态机实现
文字说明:
Ⅰ 状态机常用函数指针数组,将每个状态的处理函数放入数组,通过下标选择调用
代码示例:
#include <stdio.h> // 定义状态处理函数类型 -- 函数指针 typedef void (*state_func)(); // 各状态对应的处理函数 void state_idle() { printf("State: IDLE\n"); } void state_run() { printf("State: RUN\n"); } void state_error() { printf("State: ERROR\n"); } int main() { // 函数指针数组,每个元素指向一个状态处理函数 state_func states[] = { state_idle, state_run, state_error }; int current_state = 1; // 0: idle, 1: run, 2: error // 通过下标选择并调用对应的状态处理函数 states[current_state](); return 0; }
C 语言模拟多态
文字说明:
Ⅰ C 语言没有面向对象的多态,但可以用结构体+函数指针模拟
Ⅱ 每个“对象”结构体中包含一个函数指针,指向不同的实现,实现“多态”效果
代码示例:
#include <stdio.h> // 定义“基类”结构体,包含一个函数指针 typedef struct { void (*speak)(); // 指向说话函数 } Animal; // 不同“子类”的实现 void dog_speak() { printf("Woof!\n"); } void cat_speak() { printf("Meow!\n"); } int main() { // 创建不同“对象”,分别赋予不同的speak函数 Animal dog = { dog_speak }; Animal cat = { cat_speak }; // 用基类指针数组存储,实现多态 Animal* animals[] = { &dog, &cat }; for (int i = 0; i < 2; ++i) { animals[i]->speak(); // 多态调用 } return 0; }
4. 结构体内存对齐
对齐规则详解
- 成员对齐:每个成员的起始地址必须是该成员大小的整数倍
- 结构体对齐:整个结构体大小必须是最大成员大小的整数倍
- 填充字节:编译器自动插入填充字节保证对齐
实际对齐示例
struct example { char a; // 1字节,偏移0 int b; // 4字节,偏移4(不是1,因为要4字节对齐) char c; // 1字节,偏移8 // 总大小12字节(不是10,因为要按最大成员4字节对齐) };
对齐的影响和控制
- 性能考虑:对齐提高CPU访问效率,减少内存访问次数
- 空间权衡:对齐浪费内存空间,但换取访问速度
- 手动控制:#pragma pack(1) 可以取消对齐
- 协议解析场景:网络数据包解析时经常需要取消对齐
5. volatile 关键字
volatile 的深层含义
- 编译器优化问题:
编译器通常会假设变量只被本线程/本程序修改,因此可能将变量值缓存到寄存器,减少内存访问,提高效率。
- 问题场景:
如果变量可能被外部因素(如硬件、中断、其他线程)修改,编译器的优化会导致程序读到的值不是最新的,产生错误。
- volatile 作用:
告诉编译器“这个变量随时可能被外部修改”,禁止优化,每次都要从内存读取,不允许用寄存器缓存
三大应用场景
- 硬件寄存器
访问外设寄存器时,寄存器的值可能随时变化,必须每次都从内存(实际是硬件地址)读取
volatile unsigned int *gpio_reg = (volatile unsigned int*)0x40001000; *gpio_reg = 0x01; // 写寄存器 unsigned int val = *gpio_reg; // 读寄存器
- 中断处理
中断服务程序(ISR)可能修改全局变量,主程序必须用 volatile 保证每次都读最新值
volatile int flag = 0; void ISR() { // 中断服务程序 flag = 1; } int main() { while (!flag) { // 等待中断置位 } // 处理中断 }
- 多线程共享变量
多线程环境下,某个变量可能被其他线程修改,必须用 volatile 防止编译器优化
volatile int stop = 0; void* worker(void* arg) { while (!stop) { // 工作线程循环 } return NULL; } int main() { pthread_t tid; pthread_create(&tid, NULL, worker, NULL); // ... 其他操作 ... stop = 1; // 通知线程退出 pthread_join(tid, NULL); }
底层机制
- 内存屏障:volatile 保证每次访问都从内存读取/写入,不允许编译器优化为寄存器缓存
- 寄存器缓存失效:禁止将变量值长时间保存在寄存器,确保变量值的“可见性”
- 编译器行为:强制生成内存访问指令,防止优化
总结
- volatile 只保证“每次都从内存访问”,不保证原子性,也不保证多核间的同步
- 在多线程同步时,通常还需要配合互斥锁、原子操作等
6. 位运算在底层控制中的精妙应用
四大基础操作
- 置位操作:reg |= (1 << n) 将第n位设为1
- 清位操作:reg &= ~(1 << n) 将第n位设为0
- 翻转操作:reg ^= (1 << n) 翻转第n位
- 检测操作:if(reg & (1 << n)) 检查第n位是否为1
高级位操作技巧
- 多位提取:value = (reg >> start_bit) & ((1 << bit_count) - 1)
- 掩码应用:#define GPIO_MODE_MASK 0x03 提取特定位段
- 位域结构:用结构体的位域功能直接操作位
嵌入式开发实战
- GPIO控制:通过寄存器位控制引脚输入输出状态
- 中断屏蔽:设置中断使能寄存器的特定位
- 协议解析:提取数据包头部的控制位信息
7. 联合体的内存共享巧用
内存共享机制
- 基本原理:所有成员共享同一块内存空间
- 大小计算:联合体大小等于最大成员的大小
- 访问方式:可以用不同成员名访问同一块内存
经典应用模式
union data_converter { unsigned int full; // 32位整体访问 struct { unsigned char b0, b1, b2, b3; // 字节级访问 } bytes; };
实际应用价值
- 字节序转换:在大端和小端之间转换数据
- 协议解析:将接收到的字节流解释为不同的数据结构
- 内存节省:在资源受限的嵌入式系统中节省内存
8. 变量作用域和生命周期的精确控制
三种静态变量对比
- 全局变量:
- 作用域:整个程序
- 链接性:外部链接,可被其他文件访问
- 生命周期:程序运行期间
- 静态全局变量:
- 作用域:当前文件
- 链接性:内部链接,其他文件无法访问
- 用途:文件内部的"私有"全局变量
- 静态局部变量:
- 作用域:函数内部
- 生命周期:程序运行期间(不随函数结束而销毁)
- 用途:保持函数调用间的状态
内存分布差异
- 存储位置:静态变量存储在数据段,不在栈中
- 初始化时机:程序启动时初始化,只初始化一次
- 默认初始化:未显式初始化的静态变量自动初始化为0
9. 字符串操作的安全防护
危险函数的安全替代
- strcpy → strncpy:
- 危险:strcpy(dest, src) 不检查目标缓冲区大小
- 安全:strncpy(dest, src, sizeof(dest)-1); dest[sizeof(dest)-1] = '\0';
- strcat → strncat:
- 危险:字符串连接可能溢出
- 安全:使用strncat并确保总长度不超限
- sprintf → snprintf:
- 危险:格式化输出可能溢出缓冲区
- 安全:snprintf限制最大输出长度
长度计算的陷阱
- strlen vs sizeof:
- strlen计算字符串长度(不包括'\0')
- sizeof计算数组大小(包括'\0')
- 动态字符串处理:处理用户输入时必须验证长度
缓冲区溢出防护
- 边界检查:始终检查输入长度是否超过缓冲区
- 安全编程习惯:预留足够空间,添加结束符
- 输入验证:对所有外部输入进行长度和内容验证
10. 预处理器的高级应用技巧
宏定义的安全写法
- 参数加括号:#define MAX(a,b) ((a)>(b)?(a):(b)) 避免优先级问题
- 多语句宏:使用do-while(0)结构包装多语句宏
- 副作用防范:避免宏参数被多次计算
条件编译的实战应用
- 平台适配:
#ifdef _WIN32 // Windows特定代码 #elif defined(__linux__) // Linux特定代码 #endif
- 调试开关:通过宏控制调试信息的编译
- 功能裁剪:根据需求编译不同功能模块
高级预处理技巧
- 字符串化:# 将宏参数转换为字符串
- 标记连接:## 连接两个标记生成新标识符
- 包含保护:#ifndef/#define/#endif 防止头文件重复包含
#pragma
作用
- #pragma 是编译器指令,用于向编译器传递特定的编译信息,具体行为依赖编译器实现
- 常见用途:控制警告、结构体对齐、优化开关、头文件只包含一次
常用
Ⅰ 结构体对齐
#pragma pack(1) // 结构体按1字节对齐 struct S { char a; int b; }; #pragma pack() // 恢复默认对齐
Ⅱ 只包含一次(类似 include guard)
#pragma once // 头文件内容
Ⅲ 控制编译警告(MSVC 为例)
#pragma warning(disable:4996) // 禁用4996号警告
Ⅳ GCC 优化控制
#pragma GCC optimize("O3")
宏定义的安全写法
Ⅰ 参数加括号
#define MAX(a, b) ((a) > (b) ? (a) : (b))
Ⅱ 多语句宏
#define SWAP(a, b) do { \ int tmp = (a); \ (a) = (b); \ (b) = tmp; \ } while(0)
Ⅲ 副作用防范
避免宏参数被多次求值(如 MAX(i++, j++)),可用内联函数替代,或在宏内部用临时变量(但 C 里不易实现)
条件编译
Ⅰ 平台适配
#ifdef _WIN32 // Windows 特定代码 #elif defined(__linux__) // Linux 特定代码 #endif
Ⅱ 调试开关
#ifdef DEBUG printf("Debug info: %d\n", x); #endif
Ⅲ 功能裁剪
#define FEATURE_X #ifdef FEATURE_X // 编译 Feature X 相关代码 #endif