嵌入式开发面试八股文详解教程
嵌入式开发面试八股文详解教程
目录
- C语言基础
- 程序编译与内存
- 数据结构
- 网络通信
- 操作系统
- 嵌入式通信协议
- C++面向对象
C语言基础
1. 内联函数(inline)
概念:
内联函数是一种特殊的函数声明方式,通过在函数前面加上 inline
关键字来实现。
工作原理:
- 当调用内联函数时,编译器会将函数代码直接展开到调用处,而不是进行常规的函数跳转
- 普通函数调用需要保存局部变量和计算值到栈中,然后跳转到函数执行,执行完毕后再返回原位置
- 内联函数直接替换代码,避免了函数跳转的开销
优点:
- 减少函数调用开销
- 提高程序执行效率
示例:
inline int add(int a, int b) {return a + b;
}int main() {int result = add(3, 5); // 编译器会将add函数直接展开// 相当于:int result = 3 + 5;return 0;
}
2. strcpy 函数实现
函数原型:
char* strcpy(char* dest, const char* src);
功能:
- 将源字符串(src)复制到目标字符串(dest)
- 返回目标字符串的起始地址
实现代码:
char* my_strcpy(char* dest, const char* src) {char* ret = dest; // 保存目标地址用于返回// 使用while循环复制字符,直到遇到结束符'\0'while ((*dest++ = *src++) != '\0');return ret;
}
注意事项:
- 需要确保目标缓冲区有足够空间
- 会自动复制结束符
\0
3. strcmp 函数实现
函数原型:
int strcmp(const char* str1, const char* str2);
功能:
- 比较两个字符串是否相等
- 返回值:相等返回0,str1大于str2返回正数,小于返回负数
实现代码:
int my_strcmp(const char* str1, const char* str2) {while (*str1 && (*str1 == *str2)) {str1++;str2++;}return *(unsigned char*)str1 - *(unsigned char*)str2;
}
4. strcat 函数实现
函数原型:
char* strcat(char* dest, const char* src);
功能:
- 将源字符串追加到目标字符串末尾
- 返回连接后的字符串地址
实现思路:
- 定位到目标字符串的结尾处
- 从源字符串的头部开始复制
- 复制直到遇到源字符串的结束符
实现代码:
char* my_strcat(char* dest, const char* src) {char* ret = dest;// 定位到dest的结尾while (*dest != '\0') {dest++;}// 将src追加到dest后面while ((*dest++ = *src++) != '\0');return ret;
}
程序编译与内存
5. 程序的内存分段
程序在内存中主要分为以下几个段:
1. 代码段(Text Segment)
- 作用:存储程序的可执行指令
- 特性:一般为只读,防止被意外修改
2. 数据段(Data Segment)
- 作用:存储已初始化的全局变量和静态变量
- 示例:
int global_var = 10;
3. BSS段
- 作用:存储未初始化的全局变量和静态变量
- 特性:初始值默认为0
- 示例:
int global_var;
4. 堆(Heap)
- 作用:动态内存分配
- 管理:使用
malloc
和free
进行管理 - 特性:由程序员手动管理
5. 栈(Stack)
- 作用:存储局部变量、函数参数、返回地址
- 管理:由操作系统自动管理
- 特性:先进后出(LIFO)
内存布局图:
高地址
|--------|
| 栈 | ← 向下增长
|--------|
| ↓ |
| |
| ↑ |
|--------|
| 堆 | ← 向上增长
|--------|
| BSS段 |
|--------|
| 数据段 |
|--------|
| 代码段 |
|--------|
低地址
6. C文件编译为可执行程序的过程
一个 .c
文件转化为可执行程序需要经过四个步骤:
步骤1:预处理(Preprocessing)
- 操作:展开头文件、宏定义,删除注释
- 输出:
.i
文件 - 命令:
gcc -E source.c -o source.i
步骤2:编译(Compilation)
- 操作:将预处理后的源代码转换为汇编代码
- 输出:
.s
文件 - 命令:
gcc -S source.i -o source.s
步骤3:汇编(Assembly)
- 操作:将汇编代码转换为机器码
- 输出:
.o
目标文件 - 命令:
gcc -c source.s -o source.o
步骤4:链接(Linking)
- 操作:将所有目标文件链接成一个可执行程序
- 输出:可执行文件
- 命令:
gcc source.o -o program
完整流程图:
source.c → [预处理] → source.i → [编译] → source.s → [汇编] → source.o → [链接] → executable
7. 大小端存储
概念:
- 大端存储:高字节存储在低地址
- 小端存储:低字节存储在低地址
示例:
对于数据 0x12345678
:
地址 | 大端存储 | 小端存储 |
---|---|---|
0x00 | 0x12 | 0x78 |
0x01 | 0x34 | 0x56 |
0x02 | 0x56 | 0x34 |
0x03 | 0x78 | 0x12 |
判断系统大小端的代码:
#include <stdio.h>int main() {unsigned int num = 0x12345678;unsigned char* ptr = (unsigned char*)#if (*ptr == 0x78) {printf("小端存储\n");} else if (*ptr == 0x12) {printf("大端存储\n");}return 0;
}
数据结构
8. 栈与队列的区别
特性 | 栈(Stack) | 队列(Queue) |
---|---|---|
访问方式 | 先进后出(LIFO) | 先进先出(FIFO) |
操作方式 | 只能在栈顶操作 | 队尾插入,队首删除 |
应用场景 | 函数调用、表达式求值 | 任务调度、消息队列、广度优先搜索 |
栈的应用示例:
// 函数调用栈
void func3() { /* ... */ }
void func2() { func3(); }
void func1() { func2(); }
int main() { func1(); }
// 调用顺序:main → func1 → func2 → func3
队列的应用示例:
// 任务调度
任务1 → 任务2 → 任务3 → [队列] → 执行任务1 → 执行任务2 → 执行任务3
9. 堆与栈的区别
特性 | 堆(Heap) | 栈(Stack) |
---|---|---|
管理方式 | 手动管理(malloc/free) | 自动管理 |
生长方向 | 向上增长(低→高地址) | 向下增长(高→低地址) |
大小限制 | 较大,受系统内存限制 | 较小,通常几MB |
分配效率 | 较慢 | 较快 |
碎片问题 | 可能产生内存碎片 | 不会产生碎片 |
网络通信
10. TCP 与 UDP 的区别
特性 | TCP | UDP |
---|---|---|
连接方式 | 面向连接 | 无连接 |
可靠性 | 可靠传输,保证数据正确到达 | 不可靠传输 |
传输方式 | 面向字节流 | 面向数据报 |
速度 | 较慢 | 较快 |
应用场景 | HTTP、FTP、邮件 | 视频流、DNS |
流量控制 | 有流量控制和拥塞控制 | 无 |
11. TCP 通信流程
TCP 客户端流程:
- 创建套接字:
socket()
- 连接服务器:
connect()
- 发送/接收数据:
send()/recv()
或write()/read()
- 关闭连接:
close()
TCP 服务端流程:
- 创建套接字:
socket()
- 绑定地址:
bind()
- 监听连接:
listen()
- 接受连接:
accept()
- 发送/接收数据:
send()/recv()
或write()/read()
- 关闭连接:
close()
示例代码:
// TCP服务端
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
bind(sockfd, ...);
listen(sockfd, 5);
int client_fd = accept(sockfd, ...);
recv(client_fd, buffer, ...);
close(client_fd);
12. UDP 通信流程
UDP 客户端流程:
- 创建套接字:
socket()
- 发送数据:
sendto()
- 接收数据:
recvfrom()
- 关闭套接字:
close()
UDP 服务端流程:
- 创建套接字:
socket()
- 绑定地址:
bind()
- 接收数据:
recvfrom()
- 发送数据:
sendto()
- 关闭套接字:
close()
特点:
- UDP不需要建立连接,直接发送数据
- 使用
sendto()
和recvfrom()
进行通信
操作系统
13. 进程与线程的区别
特性 | 进程 | 线程 |
---|---|---|
资源占用 | 独立的地址空间 | 共享进程的地址空间 |
开销 | 创建、切换开销大 | 创建、切换开销小 |
通信方式 | IPC(管道、消息队列等) | 共享内存,通信简单 |
独立性 | 相互独立 | 属于同一进程 |
14. 进程间通信方式
-
管道(Pipe)
- 半双工通信
- 只能在具有亲缘关系的进程间使用
-
命名管道(FIFO)
- 可在无亲缘关系的进程间通信
-
消息队列
- 在内核中创建消息链表
- 克服信号传递信息少的缺点
-
共享内存
- 最快的IPC方式
- 在内核空间创建共享内存区域
-
信号量
- 用于进程同步
-
信号
- 用于通知进程某个事件已发生
-
套接字(Socket)
- 可用于不同机器间的进程通信
15. 进程的五种状态
-
创建状态
- 进程正在被创建
-
就绪状态
- 进程获得了除CPU外的所有资源,等待CPU分配
-
运行状态
- 进程正在CPU上执行
-
阻塞状态
- 进程等待某个事件发生(如I/O完成)
-
终止状态
- 进程执行完毕或异常终止
状态转换图:
创建 → 就绪 ⇄ 运行 → 终止↓ ↑阻塞 ←┘
16. CPU调度算法
-
先来先服务(FCFS)
- 按到达顺序调度
- 简单但可能造成长作业等待
-
短作业优先(SJF)
- 优先调度执行时间最短的作业
- 可能导致长作业饥饿
-
时间片轮转
- 每个进程分配固定时间片
- 时间片用完则切换到下一个进程
-
优先级调度
- 根据优先级调度
- 可能导致低优先级进程饥饿
17. 系统调用
概念:
系统调用是用户空间与内核空间之间的接口,通过这个接口可以使用户空间访问到内核空间。
为什么需要系统调用:
- 直接访问内核空间非常不安全
- 恶意用户可能修改内核内容导致内核崩溃
- 系统调用提供了安全的访问机制
调用流程:
用户空间 内核空间
应用程序↓
open/read/write → 系统调用接口↓sys_open/sys_read/sys_write
常见系统调用:
- 文件操作:
open()
,read()
,write()
,close()
- 进程控制:
fork()
,exec()
,wait()
- 内存管理:
mmap()
,brk()
嵌入式通信协议
18. SPI 与 I²C 寻址方式的区别
SPI 寻址方式:
- 线路:4根线(MOSI、MISO、SCLK、CS)
- 寻址:通过CS片选信号选择对应设备
- 特点:支持多从设备,每个设备需要独立的CS线
I²C 寻址方式:
- 线路:2根线(SDA数据线、SCL时钟线)
- 寻址:通过设备地址进行寻址(7位或10位地址)
- 特点:所有设备共享总线,通过地址区分
对比表:
特性 | SPI | I²C |
---|---|---|
线路数量 | 4根 | 2根 |
寻址方式 | 片选信号 | 设备地址 |
速度 | 较快 | 较慢 |
硬件复杂度 | 较高 | 较低 |
19. FIFO(队列缓冲区)
概念:
FIFO是一种先进先出的缓冲区,常用于数据传输和任务调度。
实现方式:
- 软件实现:使用环形缓冲区
- 硬件实现:高端芯片自带FIFO硬件
应用场景:
- 串口通信
- 数据采集
- 任务队列
使用FIFO的优点:
传统方式:
CPU → 取数据 → 发送到串口 → 等待发送完成(阻塞其他任务)使用FIFO:
CPU → 数据放入FIFO → 继续执行其他任务↓FIFO → 串口发送(后台进行)
- 减轻CPU负担
- 提高系统响应速度
- 避免数据丢失
20. 交叉编译
概念:
交叉编译是指在一个平台上编译出另一个平台的程序。
应用场景:
- 在Ubuntu(x86)上编译ARM平台的程序
- 开发板上通常没有编译工具链
示例:
# 在Ubuntu上使用交叉编译工具链
arm-linux-gnueabihf-gcc main.c -o main# 将编译好的程序拷贝到ARM开发板运行
scp main root@192.168.1.100:/home/
为什么需要交叉编译:
- 嵌入式设备资源有限,无法运行编译器
- 在PC上编译速度更快
- 便于开发和调试
21. DMA(直接内存访问)
概念:
DMA是一种允许外设直接访问内存的技术,无需CPU介入。
传统方式 vs DMA:
传统方式:
外设 → CPU读取 → 内存← CPU写入 ←
使用DMA:
外设 ←→ DMA控制器 ←→ 内存
CPU只需配置,数据传输自动完成
优点:
- 减轻CPU负担
- 提高数据传输效率
- CPU可以并行处理其他任务
应用场景:
- 大量数据传输
- 高速串口通信
- ADC数据采集
C++面向对象
22. 构造函数与析构函数
构造函数(Constructor)
作用:
- 初始化对象
- 设置初始值
特性:
- 名称与类名相同
- 没有返回值
- 可以有多个重载版本(支持不同初始化方式)
示例:
class Student {
private:string name;int age;public:// 默认构造函数Student() {name = "Unknown";age = 0;}// 带参数的构造函数Student(string n, int a) {name = n;age = a;}// 拷贝构造函数Student(const Student& other) {name = other.name;age = other.age;}
};
析构函数(Destructor)
作用:
- 释放资源
- 执行清理操作
特性:
- 名称为
~类名
- 没有参数
- 没有返回值
- 不能重载(每个类只能有一个析构函数)
示例:
class FileHandler {
private:FILE* file;public:FileHandler(const char* filename) {file = fopen(filename, "r");}// 析构函数:关闭文件~FileHandler() {if (file != nullptr) {fclose(file);file = nullptr;}}
};
总结
本教程涵盖了嵌入式开发面试中的核心知识点:
- C语言基础:内联函数、字符串操作函数实现
- 程序编译与内存:内存分段、编译流程、大小端存储
- 数据结构:栈、队列、堆的区别与应用
- 网络通信:TCP/UDP协议及通信流程
- 操作系统:进程、线程、IPC、调度算法、系统调用
- 嵌入式通信:SPI、I²C、FIFO、DMA、交叉编译
- C++面向对象:构造函数与析构函数