1. 1. C/C++
    1. 1.1. 运算符优先级
    2. 1.2. 原码、反码、补码转换
    3. 1.3. Volatile
    4. 1.4. 指针的大小
    5. 1.5. C/C++ 基本数据类型大小
    6. 1.6. C语言的隐式转换
    7. 1.7. 数组名num和&num的区别
    8. 1.8. 什么时候用 . ,什么时候用->
    9. 1.9. 指针数组和数组指针
    10. 1.10. 指针函数和函数指针
    11. 1.11. 常量指针和指针常量
    12. 1.12. 野指针和悬空指针
    13. 1.13. Free释放内存后,指针还能用吗
    14. 1.14. 引用是什么,有了指针为什么还需要引用
    15. 1.15. 数组和链表的区别
    16. 1.16. “#include "" 和 #include <> 区别
    17. 1.17. #ifndef #define #endif的作用
    18. 1.18. strlen和sizeof的区别
    19. 1.19. gets和scanf函数的区别
    20. 1.20. C语言内存分配的方式
    21. 1.21. struct 和 class 的区别
    22. 1.22. 结构体和联合体的区别
    23. 1.23. 宏函数注意点
    24. 1.24. 全局变量和局部变量的区别
    25. 1.25. #define、typedef、const 的区别
    26. 1.26. static 的作用
    27. 1.27. register的作用(了解)
    28. 1.28. memcpy 和 strcpy 的区别,哪个安全
    29. 1.29. 堆和栈的区别
    30. 1.30. malloc实现原理
    31. 1.31. malloc 和 new 的区别
    32. 1.32. malloc、calloc、realloc 内存申请函数
    33. 1.33. 被free回收的内存是立即返还给操作系统吗
    34. 1.34. void*类型指针的作用
    35. 1.35. 内存泄漏是什么
    36. 1.36. 深拷贝和浅拷贝的区别
    37. 1.37. 程序分为几个段
    38. 1.38. 一个 .c 文件怎么转换为可执行程序
    39. 1.39. 静态链接和动态链接的区别
    40. 1.40. strcpy、sprintf和memcpy的区别
    41. 1.41. ++a 和 a++ 的区别
    42. 1.42. # 和 ## 的作用
    43. 1.43. 不适用额外空间,交换两个数
    44. 1.44. 局部变量能不能和全局变量重名
    45. 1.45. C 语言位域(Bit-field)(了解)
    46. 1.46. 用位运算计算余数(了解)
    47. 1.47. 取消内存对齐的方法(了解)
    48. 1.48. 什么是回调函数
    49. 1.49. 如何判断一个整数是有符号还是无符号
    50. 1.50. 面向对象和面向过程的区别
    51. 1.51. 面向对象的三大特征
    52. 1.52. C++ 类的访问权限
    53. 1.53. 重载、重写、覆盖
    54. 1.54. 什么是智能指针,C++ 的智能指针有哪些,作用是什么
    55. 1.55. C++有几种new
    56. 1.56. C++有几种强制转换
    57. 1.57. 隐式转换,如何消除它
    58. 1.58. cout和printf的区别(了解)
    59. 1.59. 什么是虚函数
    60. 1.60. 什么是纯虚函数
    61. 1.61. C++中如何阻止一个类被实例化
    62. 1.62. 什么函数不能声明为虚函数
    63. 1.63. 为什么C++默认的析构函数不是虚函数
    64. 1.64. 为什么析构函数必须是虚函数
    65. 1.65. 静态函数和虚函数的区别
    66. 1.66. 构造函数有几种,分别什么作用
    67. 1.67. 构造函数和析构函数执行顺序
    68. 1.68. 构造函数、拷贝构造函数和赋值运算符的区别
    69. 1.69. 只定义析构函数,会自动生成哪些构造函数
    70. 1.70. 一个类默认会生成哪些函数
    71. 1.71. 什么是虚拟继承
    72. 1.72. 指针和引用的区别
    73. 1.73. 什么时候指针,什么时候引用
    74. 1.74. 类如何实现只能静态分配和只能动态分配
    75. 1.75. 什么是静态绑定和动态绑定
    76. 1.76. 结构体变量比较是否相等
    77. 1.77. 内联函数(inline)
    78. 1.78. extern “C” 的作用
    79. 1.79. C++中NULL和nullptr区别
    80. 1.80. C++11有哪些新特性
    81. 1.81. this指针
    82. 1.82. 左值和右值的区别
    83. 1.83. C/C++的关键字(了解)
    84. 1.84. vector的底层实现
    85. 1.85. vector 和 list 的使用场景与区别
    86. 1.86. Vector下的resize和reserve的区别
    87. 1.87. Vector.size()是capacity,还是已存储的元素个数
    88. 1.88. 如何理解迭代器,容器的迭代器是由什么组成的
    89. 1.89. STL中迭代器有什么用,有指针了为何还要迭代器
    90. 1.90. STL中有哪些容器,这些容器的迭代器是如何删除元素
    91. 1.91. STL中的map和set有什么区别,怎么实现的
    92. 1.92. STL中的map和unordered_map区别
    93. 1.93. STL的map插入方式
    94. 1.94. 什么是初始化列表,哪些情况下只能初始化列表,而不能赋值
    95. 1.95. 没有参数的函数能不能被重载
  2. 2. 计算机网络
    1. 2.1. OSI 四层模型 / 七层模型
    2. 2.2. HTTP是什么
    3. 2.3. HTTP 的发展史
    4. 2.4. HTTP请求和响应报文包含哪些字段
    5. 2.5. HTTP请求流程
    6. 2.6. GET和POST的区别
    7. 2.7. HTTP和HTTPS的区别
    8. 2.8. 对称加密和非对称加密
    9. 2.9. HTTPS请求流程
    10. 2.10. 常见HTTP状态码
    11. 2.11. 什么是DNS
    12. 2.12. DNS 负载均衡
    13. 2.13. TCP 和 UDP 的区别
    14. 2.14. MTU和MSS分别是什么
    15. 2.15. TCP 粘包和拆包问题,怎么解决
    16. 2.16. TCP 通信如何保证通信的可靠性
    17. 2.17. 如何区分流量控制和拥塞控制
    18. 2.18. TCP 三次握手(连接建立)
    19. 2.19. TCP 四次挥手(连接释放)
    20. 2.20. 为什么客户端最后还要等待2MSL
    21. 2.21. 什么是 Cookie
    22. 2.22. 什么是 Session
    23. 2.23. socket网络编程中用到哪些函数
    24. 2.24. DHCP 协议(Dynamic Host Configuration Protocol)(了解)
    25. 2.25. ARP和RARP
    26. 2.26. Ping命令基于什么协议(了解)
    27. 2.27. IP 地址、子网掩码、网关和 DNS 作用
    28. 2.28. IPv4 和 IPv6 的区别(了解)
    29. 2.29. 常用的网络拓扑类型(了解)
  3. 3. 操作系统
    1. 3.1. 进程、线程和协程的区别
    2. 3.2. 进程间通信方式
    3. 3.3. 进程间通信的选择
    4. 3.4. 进程间的状态
    5. 3.5. 线程间通信方式
    6. 3.6. 多进程和多线程的适用场景区别
    7. 3.7. 什么是线程池
    8. 3.8. 什么是线程安全
    9. 3.9. 僵尸进程、孤儿进程和守护进程
    10. 3.10. 僵尸进程有什么危害,如何解决
    11. 3.11. 什么是内核线程和用户线程
    12. 3.12. 进程调度算法有哪些
    13. 3.13. 并发和并行
    14. 3.14. 单核机械上写多线程程序,是否考虑加锁(了解)
    15. 3.15. 内存交换和覆盖有什么区别
    16. 3.16. 为什么使用虚拟内存
    17. 3.17. 逻辑地址转化为物理地址的基本流程
    18. 3.18. 动态分区分配算法
    19. 3.19. 磁盘调度算法
    20. 3.20. 页面置换算法
    21. 3.21. fork、exec、wait函数
    22. 3.22. fork 和 vfork 的区别
    23. 3.23. 当 for 循环遇到 fork 函数(了解)
    24. 3.24. 读写锁、自旋锁和互斥锁的区别
    25. 3.25. 什么是原子操作?
    26. 3.26. 原子性问题,可见性问题,有序性问题
    27. 3.27. 局部性原理是什么
    28. 3.28. 什么是死锁
  4. 4. Linux
    1. 4.1. 什么是系统调用
    2. 4.2. Linux 驱动的三大类型
    3. 4.3. 为什么分用户态和内核态
    4. 4.4. 什么是交叉编译
    5. 4.5. Linux 和 RTOS 的区别
    6. 4.6. Linux 内核由哪些构成?
    7. 4.7. Linux 系统组成部分
    8. 4.8. 什么是根文件系统
    9. 4.9. 什么是临界区
    10. 4.10. 什么是设备树
    11. 4.11. 什么是IO复用
    12. 4.12. select / poll / epoll 的区别与用处
    13. 4.13. 为什么 Linux 的中断分为上半部分和下半部分?
    14. 4.14. 硬中断 vs 软中断
    15. 4.15. 中断上下文和进程上下文的区别
    16. 4.16. Linux常见指令
  5. 5. STM32
    1. 5.1. ARM 中的寄存器,都有什么用
    2. 5.2. 大小端是什么
    3. 5.3. Cortex-M3 和 Cortex-M4 的区别
    4. 5.4. STM32的Flash和RAM占用
    5. 5.5. GPIO 工作模式
    6. 5.6. 什么是 IO 口悬空,可能带来哪些问题
    7. 5.7. 中断栈和任务栈有什么区别,保存在哪里
    8. 5.8. PWM的边沿对齐和中心对齐
    9. 5.9. 怎么理解AD采样里的通道和转换精度
    10. 5.10. 为什么 I2C 需要开漏输出 + 上拉电阻
    11. 5.11. I2C总线挂死,如何排查
    12. 5.12. I2C上拉电阻阻值
    13. 5.13. I2C通信出现低电平0.4V,高电平2.6V等中间电平
    14. 5.14. 怎么判断数据传输到目标后,数据没有丢失
    15. 5.15. SPI 的工作模式有哪些
    16. 5.16. SPI往屏上刷一个像素点,如何操作
    17. 5.17. RS232 和 RS485 的区别
    18. 5.18. STM32 上电后到 __main 的过程
    19. 5.19. MSP 和 PSP 两个指针是什么,都在什么时候用
    20. 5.20. 中断能有返回值和参数吗?
    21. 5.21. RAM、ROM 和 Flash 的区别
    22. 5.22. Cache 是什么,Cache 一致性又是什么
    23. 5.23. 什么是 SMP(了解)
    24. 5.24. 为什么单片机不使用 malloc 函数
    25. 5.25. 什么是中断嵌套
    26. 5.26. 如何处理 Flash 擦写寿命问题
    27. 5.27. CAN 通信的缺点
  6. 6. FreeRTOS
    1. 6.1. FreeRTOS 中使用的调度算法
    2. 6.2. FreeRTOS 的时间片的大小
    3. 6.3. FreeRTOS 中任务的状态
    4. 6.4. FreeRTOS 创建任务的方法和区别
    5. 6.5. FreeRTOS 的空闲任务以及它的作用
    6. 6.6. FreeRTOS 如何实现任务同步
    7. 6.7. FreeRTOS 中的 SVC、SysTick 和 PendSV 的作用和区别
    8. 6.8. 要实现任务调度,可以只有systick中断吗
    9. 6.9. FreeRTOS 为什么要用 PendSV
    10. 6.10. FreeRTOS 中的任务控制块是什么
    11. 6.11. FreeRTOS 如何实现任务切换,过程是什么
    12. 6.12. FreeRTOS 有哪两种内存分配方式,有哪五种分配算法
    13. 6.13. 为什么在 FreeRTOS 中信号量、队列等设计了两套函数
    14. 6.14. vTaskDelay 和 vTaskDelayUntil 的区别
    15. 6.15. 信号量和互斥量的区别
  7. 7. 硬件电路
    1. 7.1. STM32最小单元板构成
    2. 7.2. LDO稳压器(了解)
    3. 7.3. 单片机明明3.3V和5V就够用了,为什么还需要12V以上的供电
  8. 8. 数据结构
    1. 8.1. 常见的排序算法
    2. 8.2. 二叉树通过中序遍历和后序遍历,判断前序遍历
    3. 8.3. 什么是搜索二叉树
    4. 8.4. 什么是平衡二叉树
    5. 8.5. 二叉树通过中序遍历和后序遍历,判断前序遍历
    6. 8.6. 如何用栈模拟队列
    7. 8.7. 介绍一下哈希表
    8. 8.8. 反问
  9. 9. 其他
    1. 9.1. 电脑1G的空间,malloc(1.2G) 为什么可能成功
    2. 9.2. 常用的调试方法有什么(了解)
    3. 9.3. gdb 常见命令(了解)
    4. 9.4. 62-63=1,移动一个数字使其成立
    5. 9.5. 双缓冲机制(了解)
  10. 10. 说明

C/C++

运算符优先级

自上而下,优先级降低

  1. 括号 ()、数组 []、结构体成员 .、指针成员 ->

  2. 单目++--!~、取地址 &、解引用 *、类型转换 (type)

  3. 算术:乘除 % > 加减 > 移位。

  4. 比较:先大小,再相等。

  5. 位运算& > ^ > |

  6. 逻辑运算&& > ||

  7. 条件/赋值?: > =

  8. 最后:逗号 ,

1
2
3
4
5
6
7
8
// 逗号运算符从左到右以此求值
int a = 5;
printf("%d", (a = a * 3, a++));
// 输出:15

int a = 5;
printf("%d", (a * 3, a++));
// 输出:5

原码、反码、补码转换

正数:原码 = 反码 = 补码。

负数:补码 = 反码 + 1。

Q:计算机计算为什么使用补码?

A:加减运算可以统一为加法,硬件实现简单。


Volatile

作用:告诉编译器变量的值 随时可能发生变化,不要对其进行优化。(预处理操作)

典型场景

  • 多线程/中断共享变量(值可能被中断/其他线程修改)。

  • 硬件寄存器(如单片机的 IO、外设状态寄存器)。

  • 内存映射设备(读写必须严格执行,不能省略)。

volatile 不能保证 原子性,需要配合 锁/关中断 保护临界区


指针的大小

  • 32 位平台:指针大小通常 4 字节
  • 64 位平台:指针大小通常 8 字节
  • 不管 int*char*double*,指针大小都一样。

  • 1Byte = 8 Bit

  • 指针只能相减,不能相加(通常会越界

Q:C语言如何判断一个系统是32位还是64位?

sizeof(void *);如果等于4,为32位;如果等于8,则为64位。


C/C++ 基本数据类型大小

  • 32 位
类型 大小 (字节) 说明
char 1 最小存储单元
short 2 至少 16 位
int 4 通常 32 位
long 4 int 一样大
long long 8 至少 64 位
float 4 IEEE 754 单精度
double 8 IEEE 754 双精度
long double 12 或 16 依平台而定(常见 12B 或 16B 对齐到 16)
指针 4 存放地址,32 位宽度
  • 64位
类型 大小 (字节) 说明
char 1 依旧是 1 字节
short 2 至少 16 位
int 4 32 位
long 8 注意:比 32 位多一倍
long long 8 至少 64 位
float 4 单精度
double 8 双精度
long double 16 常见实现是 16B(对齐到 16)
指针 8 存放地址,64 位宽度

C语言的隐式转换

char/shortintlongfloatdoublelong double


数组名num和&num的区别

  • 一维数组

    • num+1 是偏移到下个元素
    • &num+1是偏移整个数组
  • 二维数组

    • num+1 是偏移一个一维数组
    • &num+1是偏移整个数组
  • int num[6];

    • num,类型int*,即首元素的指针

    • &num,类型int (*)[6],即整个数组地址

    • *num,等价于a[0] —> *(a+i) = a[i]

Q1:int a[6] = {1, 2, 3, 4, 5, 6};printf(“%d\n”, *((int *)(&a + 1) - 1))的输出

A:(&a + 1)是a + 6, 这里的*(int *)没什么意义,*((int *)(&a + 1) - 1)是a + 5,即6。

Q2:char *buff[] = {“char”, “int”, “double”}; printf(“%c\n”, *(buff+1)[1])的输出

A:(buff+1)是"int",[]的优先级大于*,所以*(buff+1)[1]=*(buff+1+1)=*(buff+2),取"double"的首元素"d"。

Q3:如何不使用sizeof求数据类型字节的大小

#define mysizeof(value) (char*)(&value+1) - (char*)(&value)

Q4:*p++和*++p的区别

*p++先取 p指向的值,然后 p自增(指向下一个元素)。

*++p先使 p自增(指向下一个元素),然后取新 p指向的值。

两者都会移动指针,但取值时机不同。

什么时候用 . ,什么时候用->

.(点运算符)

  • 作用于 对象/结构体变量 本身。
1
2
3
struct S { int a; };
struct S s;
s.a = 10;

->(箭头运算符)

  • 作用于 指向结构体/对象的指针,相当于 (*p).member
1
2
3
struct S { int a; };
struct S *p = &s;
p->a = 20;

变量 → .

指针 → ->


指针数组和数组指针

指针数组: int *arr[10];

数组里的每个元素都是一个指针,arr是数组名,arr[i]是一个指针。

数组指针: int (*p)[10];

一个指针,指向整个数组


指针函数和函数指针

指针函数:int* func();

函数的返回值是指针。

函数指针:

1
2
3
4
int add(int a, int b) { return a + b; }
int (*fp)(int, int); // 声明函数指针
fp = add; // 让 fp 指向 add
int result = fp(2, 3); // 调用函数

指向函数的指针变量


常量指针和指针常量

常量指针:const int *a; / int const *a;

底层Const,指针所指内容是常量,不能通过指针修改(但指针可以指向别处)

指针常量:int *const a;

顶层Const指针本身是常量,初始化后不能再指向别的地址


野指针和悬空指针

  • 野指针未初始化或指向不可知区域的指针
  • 悬空指针指向的内存已释放/失效,但没有置NULL,仍被使用。

避免方法:指针初始化为 NULL;释放后及时置为 NULL


Free释放内存后,指针还能用吗

不能直接使用free 只释放内存,不会清空指针,继续访问会造成 悬空指针

但如果非要使用,也是可以的,不过数据可能是垃圾值


引用是什么,有了指针为什么还需要引用

引用:变量的别名,和原变量共享同一块内存

初始化后不能更换绑定,不会重新分配内存

  • 安全:引用必须初始化,不能为 NULL

  • 简洁:用法像普通变量,不需 *->

  • 清晰:函数传参时避免拷贝。


数组和链表的区别

  • 数组连续内存,存储的是同一类型数据,支持随机访问,插入/删除效率低
  • 链表离散内存插入/删除效率高,查找效率低

使用场景:查询多 → 数组;插入删除多 → 链表。


“#include ""#include <> 区别

  • #include "":先从当前目录查找,再去系统目录查找。
  • #include <>:只从系统目录查找。

#ifndef #define #endif的作用

防止头文件被重复包含(“头文件保护”),用来避免重复定义、编译错误。

#program once 和它功能一样


strlen和sizeof的区别

strlen(s):计算 字符串内容长度(遇到 \0 停止,不含 \0),需在运行时遍历。

sizeof(x):计算 对象/类型所占内存字节数,在编译期确定(数组包含 \0,会计算\0的空间)。

区别strlen 得到“内容长度”,sizeof 得到“占用空间”。

计算x[]数组中元素个数:sizeof(x) / sizeof(x[0])


gets和scanf函数的区别

输入内容

  • gets(char *s):读取一行输入,直到遇到换行符(\n),换行符会被丢弃。
  • scanf("%s", &s):读取一个字符串,遇到空格、Tab 或换行符就结束。

安全性

  • gets不安全,不检查缓冲区大小,容易造成缓冲区溢出(C11 标准已废弃)。
  • scanf("%s", ...):同样可能溢出,但可以通过指定最大宽度控制:

C语言内存分配的方式

  • 静态存储区:全局变量、静态变量,程序运行期间始终存在。

  • 栈(Stack):局部变量、函数参数,由编译器自动分配/释放。

  • 堆(Heap):动态内存(malloc/freecallocrealloc),由程序员手动管理


struct 和 class 的区别

  • 在 C++ 中

    • 访问权限

      • struct:默认成员是 public
      • class:默认成员是 private
    • 继承方式

      • struct:默认是 public 继承
      • class:默认是 private 继承
  • 除此之外,两者在功能上基本相同

Q:C和C++里的struct有什么区别呢?

  • 访问权限

    • C:没有访问控制关键字(全部公开)。

    • C++:默认 public,可以使用 public / private / protected

  • 定义函数

    • C:不能包含成员函数。
    • C++:可以包含 成员函数、构造函数、析构函数、运算符重载
  • 继承

    • C:不支持继承。

    • C++:支持继承(默认是 public 继承)。

  • 定义变量

    • C:定义变量必须写 struct 关键字(除非用 typedef)。

    • C++:不需要写 struct,和 class 一样用法。


结构体和联合体的区别

结构体 (struct):成员 独立存储,总大小 ≥ 成员之和,内存对齐最大类型

联合体 (union):所有成员 共用同一块内存,大小 = 最大成员大小,整体大小也需按 最大成员的对齐要求 对齐。

Q1:下面这个的输出是多少?

1
2
3
4
5
struct S{
char c[2];
int i;
};
printf("%zu\n", sizeof(struct S));

A:结果是8 Byte,char类型对齐int类型的大小
[c[0]] [c[1]] [pad] [pad] [i0] [i1] [i2] [i3]

Q2:如何用联合体来判断大小端?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
union U {
int i;
char c;
};

int main() {
union U u;
u.i = 0x1234;
if (u.c == 0x12)
printf("大端\n"); // 低地址存高字节
else
printf("小端\n"); // 低地址存低字节
return 0;
}

可以使用#pragma pack或__attribute__(pack)来禁用内存对齐


宏函数注意点

  • 宏函数通过 #define 定义,预处理阶段替换无类型检查
  • 缺点:可能重复计算参数,调试困难。
  • 改进:参数和表达式用括号保护,推荐使用 内联函数 替代。
1
#define SQUARE(x) ((x) * (x))   // 宏函数

全局变量和局部变量的区别

  • 全局变量:定义在函数外,作用域是整个文件或程序,存储在静态区
  • 局部变量:定义在函数/代码块内,存储在栈,作用域仅在定义范围内。

Q:一个变量,没有初始化,如果它被使用了,会是什么值?

A:如果是全局变量,是0;局部变量,是垃圾值

  • 已经初始化的全局变量,在数据段(.data段)

  • 未初始化的全局变量,在BSS段(.bss段)

  • 无论是否初始化的局部变量,都在栈上


#definetypedefconst 的区别

  • #define直接文本替换,预处理指令,无类型检查
  • typedef给类型取别名有类型检查
  • const:定义常量,编译器检查类型分配内存

建议:常量用 const,类型别名用 typedef/using,少用 #define

特别说明

#define myptr int* p

//a 是 int * a, b 是 int b

myptr a,b;

typedef int* myptr;

//a 是 int * a, b 是 int b*

myptr a,b;


static 的作用

static修饰的参数若不赋值,则默认初始化为0。

  • 在 C 中

    • 局部静态变量:存放在静态存储区,函数退出后仍然存在,下次调用保留上次的值(有记忆的,只会被初始化一次)。

    • 全局静态变量:存放在静态存储区,作用域仅限当前文件,避免与其他文件的全局变量冲突。

    • 静态函数:作用域仅限当前文件,避免外部访问。

  • 在 C++ 中

    • 静态成员变量:属于类本身,所有对象共享同一份,不随对象的创建和销毁而改变。

    • 静态成员函数:不依赖对象调用,只能访问类的静态成员变量,不能访问 this 指针。


register的作用(了解)

作用:提示编译器把变量尽量存放在 CPU 寄存器 中,并且是整数

特点

  • 属于 存储类说明符
  • 只是“建议”,编译器可忽略。
  • register 变量可能没有内存地址,因此 不能对其使用取地址运算符 &

memcpy 和 strcpy 的区别,哪个安全

  • memcpy:按字节拷贝,适合任意类型数据(需指定长度)。
  • strcpy:拷贝字符串,遇到 \0 停止,需保证目标空间足够大。
  • 安全性memcpy 更通用更安全;strcpy 可能造成溢出,推荐用 strncpy

堆和栈的区别

  • 栈(Stack)系统自动分配/释放,空间小,效率高,向低地址扩张
  • 堆(Heap)自己手动申请/释放,空间大,效率低,向高地址扩张

常见问题:使用不当可能导致 内存泄漏内存碎片

堆往往是不连续的内存区域,栈是连续的内存区域


malloc实现原理

内存申请方式

  • 小块内存(< 128K):通过 brk() 扩展堆顶指针。
  • 大块内存(≥ 128K):通过 mmap() 直接向内核申请独立内存区域。

内存池管理

  • malloc 会向系统申请一大块内存作为 堆区,再划分为多个小块。
  • 用户申请时,从堆区挑选合适的空闲块返回,而不是每次都调用系统接口。

空闲块组织

  • 使用 隐式双向链表 管理空闲块。
  • 每个空闲块会记录自身大小和指向下一块的指针。
  • 分配时可能会 分割 空闲块;释放时可能会 合并 相邻块,减少碎片。

malloc 采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将 堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快。采用隐式链 表将所有空闲块,每一个空闲块记录了一个未分配的、连续的内存地址


malloc 和 new 的区别

  • malloc(C语言库函数):需要手动计算内存大小,返回 void*,需手动 free不会调用构造函数
  • new(C++ 运算符):类型安全,分配内存并调用构造函数,失败抛出异常,需用 delete 释放,返回具体类型指针。

malloc、calloc、realloc 内存申请函数

malloc(size)

  • 分配指定字节大小的内存,内容不初始化(可能是垃圾值)。

calloc(n, size)

  • 分配 n * size 字节,并全部置零

realloc(ptr, size)

  • 调整已分配内存大小:扩容或缩小,可能搬移到新地址。

被free回收的内存是立即返还给操作系统吗

不会,而是交给C的堆分配器。

内存分配器机制(以 ptmalloc 为例)

  • 内部维护多个 空闲链表(bins)

  • 使用 双向链表管理空闲块

  • 支持 相邻空闲块合并,减少碎片。

  • 小块用 brk() 分配,大块可能用 mmap()


void*类型指针的作用

void* 是 C/C++ 中的通用指针类型,可指向任意数据类型的内存地址。其核心价值在于实现泛型编程,例如:

  • 内存管理函数(如 malloccalloc)返回 void*,让调用者决定如何解释和使用分配的内存。
  • 通用函数接口(如 qsortmemcpy)使用 void* 参数来处理未知类型的数据

使用 void* 时,必须先转换为具体指针类型才能解引用或访问数据。这种设计提供了灵活性,但也牺牲了类型安全


内存泄漏是什么

程序通过 malloc/new 申请了内存,但在使用完后没有通过 free/delete 释放,导致这部分内存无法再被使用或回收。

影响:长期运行会占满内存,导致程序变慢甚至崩溃。

Q:如何避免?

  • 申请和释放配对使用 (malloc/free, new/delete)。

  • 避免重复释放或遗忘释放。

  • 推荐使用 智能指针(C++)管理内存。


深拷贝和浅拷贝的区别

  • 浅拷贝:复制指针,不复制实际资源多个指针对象指向同一块内存
  • 深拷贝:复制内容,独立资源 → 避免内存释放冲突。

程序分为几个段

典型的 C/C++ 程序在内存中分为:

  • 代码段(text):存放只读存储区文本区
  • 数据段(data):存放已初始化的全局变量、静态变量
  • BSS 段:存放未初始化的全局变量、静态变量
  • 堆(heap)动态分配的内存。
  • 共享内存段存储动态链接库以及调用mmap函数的文件映射
  • 栈(stack)局部变量、函数的参数,函数的返回值

一个 .c 文件怎么转换为可执行程序

  • 预处理:宏展开、头文件展开,生成 .i 文件。

    • 将所有的#define删除,并且展开所有的宏定义
    • 处理所有的条件预编译指令,如#if、#ifdef
    • 处理#include预编译指令
    • 过滤注释
    • 添加行号和文件名标识
  • 编译:把预处理结果转为汇编代码 .s

    • 目标代码生成:生成汇编代码
    • 目标代码优化
  • 汇编:汇编器将 .s 转为机器码 .o

    • 汇编代码转变成机器可以执行的指令
  • 链接:把多个目标文件和库链接成可执行文件。

    • 不同的源文件产生的目标文件进行链接

过程:.c —> .i —> .s —> .o


静态链接和动态链接的区别

  • 静态链接库函数编译时直接打包进可执行文件
    • 优点:独立运行;
    • 缺点:体积大,更新麻烦。
  • 动态链接运行时加载共享库
    • 优点:节省空间,易更新;
    • 缺点:依赖库文件。

strcpy、sprintf和memcpy的区别

  • strcpy:仅用于字符串必须有 \0 结尾;有缓冲区溢出分险,建议用strncpy。

  • sprintf:用于格式化文本(带数字、拼接等),输出到字符串;不检查越界,建议用snprintf。

  • memcpy拷贝任意内存,包括结构体、数组等,适用于“非字符串”。

1
2
3
4
5
>char buf[100];
>int id = 42;
>const char* name = "Alice";
>sprintf(buf, "User %d: %s", id, name);
>// buf内容是:User 42: Alice

++a 和 a++ 的区别

  • ++a先自加不开辟空间,效率更高。

  • a++需要开辟空间

++a -> a = a + 1,使用的是左值,而a++使用的是右值。


# 和 ## 的作用

#(字符串化)

  • 把宏参数转成 字符串

    1
    2
    #define TO_STR(x) #x
    printf("%s", TO_STR(Hello)); // 输出 "Hello"

##(连接符/粘合剂)

  • 把宏参数和其他标识符拼接在一起。

    1
    2
    #define VAR(n) var##n
    int VAR(1) = 10; // 变成 int var1 = 10;

不适用额外空间,交换两个数

1
2
3
4
5
6
7
8
9
10
11
// 算术
x = x + y;
y = x - y;
x = x - y;

// 异或(只能对char和int)(了解)
x = x ^ y;
y = x ^ y;
x = x ^ y;
y = y ^ x;
x = x ^ y;

局部变量能不能和全局变量重名

可以重名,局部变量会屏蔽同名的全局变量,在作用域内优先使用局部变量。


C 语言位域(Bit-field)(了解)

  • 定义:在 struct 中,用指定位数存放成员,节省空间。
  • 用途:常用于 硬件寄存器映射标志位集合
  • 注意:位域排布依赖编译器/平台,移植性差。

示例

1
2
3
4
5
struct Flags {
unsigned int a : 1; // 1 bit
unsigned int b : 3; // 3 bit
unsigned int c : 4; // 4 bit
};

用位运算计算余数(了解)

只了解到可以对2的幂次方进行取余操作

  • 对 2 的幂取余x % (2^n) == x & (2^n - 1)

例:13 % 8 == 13 & 7 == 5

8 -> 1000,使用0111,可以取后几位。


取消内存对齐的方法(了解)

  • GCC__attribute__((packed))
  • MSVC#pragma pack(1)

什么是回调函数

  • 一个函数作为参数传递给另一个函数,被在特定时机调用。
  • 常用于 事件驱动异步处理

比如Bootloader执行完MSR_SP后,就需要加载回调函数load()。


如何判断一个整数是有符号还是无符号

1
#define isunsing(a) ((a >= 0) && (~a >= 0))

面向对象和面向过程的区别

面向过程 (Procedure-Oriented)

  • 强调 过程/函数,把问题拆解成步骤。
  • 数据与函数分离。

面向对象 (Object-Oriented)

  • 强调 对象,数据和操作绑定在一起。
  • 支持 封装、继承、多态

面向对象的三大特征

  • 封装

    • 概念:把数据和操作打包在类里,隐藏实现细节,对外只暴露必要接口。
    • C++ 实现class / struct + 访问控制符publicprotectedprivate)。
    • 作用:提高模块化与安全性,防止随意修改内部数据。
  • 继承

    • 概念:子类自动获得父类已有的成员与方法,可以复用或扩展。
    • C++ 实现class Derived : public Base { ... };
    • 作用:代码复用,形成层次结构,支持多级继承。
  • 多态

    • 概念:同一接口在不同对象上表现出不同的行为。

    • C++ 实现

      • 编译期多态:函数重载、运算符重载(通过名字/参数列表区分)。
      • 运行期多态:虚函数 + 基类指针/引用调用派生类实现(依赖 virtual 关键字 + 动态绑定)。
    • 作用:提高灵活性,便于扩展。


C++ 类的访问权限

  • public:对所有代码可见。
  • protected:仅类本身和派生类可见。
  • private:仅类本身可见。

重载、重写、覆盖

  • 重载(Overload)
    • 概念:同一个作用域内,函数名相同,但 参数列表不同(个数或类型不同)。
    • 发生位置:同一个类中(或全局函数)。
    • 关键点:编译期根据实参匹配 → 静态多态
  • 重写(Override)
    • 概念:派生类中 重新定义 基类的 虚函数函数名必须相同
    • 发生位置:继承体系中。
    • 关键点:运行时通过虚函数表决定调用 → 动态多态
  • 隐藏(Hide/Overwrite)
    • 概念派生类定义了一个与基类同名但非虚函数参数列表不同的函数会隐藏基类的同名函数
    • 结果:基类版本被“遮蔽”,不是严格的多态。
  • 重载:同域,同名,不同参数 → 编译期多态。

  • 重写:继承,虚函数,相同签名 → 运行时多态。

  • 覆盖:继承,同名但非虚函数或不同签名 → 基类函数被隐藏。


什么是智能指针,C++ 的智能指针有哪些,作用是什么

  • 作用:自动管理内存,避免new/delete 手工管理带来的内存泄漏。
  • C++ 智能指针
    • std::auto_ptr(C++11 废弃)。
    • std::unique_ptr:独占所有权→ 同一时间只能有一个指针指向资源,不能拷贝,只能移动。
    • std::shared_ptr:引用计数共享所有权 → 多个指针可以同时指向资源,内部用 引用计数 管理,最后一个销毁时释放内存。
    • std::weak_ptr:弱引用 → 不增加引用计数,用来观察 shared_ptr 管理的对象,解决循环引用问题。
  • 这里的所有权 (ownership)谁负责在对象生命周期结束时释放内存

Q:为什么auto_ptr会被废弃?

  • auto_ptr 拷贝/赋值时,会把所有权转移(源指针变空)。
  • 容易导致意外失效,比如容器里的元素被拷贝后全部失效。

Q:智能指针的本质?

本质是通过封装原始指针,并利用 RAII(资源获取即初始化)技术以及所有权管理和引用计数等机制,自动化管理动态分配资源(如内存、文件句柄等)生命周期的对象


C++有几种new

类型 语法示例 说明
普通 new(plain new) int* p = new int(5); 分配内存并调用构造函数
placement new new(ptr) Type(args); 在已有内存上构造对象
nothrow new int* p = new(std::nothrow) int; 分配失败返回 nullptr,不抛异常

C++有几种强制转换

转换类型 语法示例 用途简述
static_cast static_cast<int>(3.14) 普通转换(类型安全)如基本类型、父子类指针转换
dynamic_cast dynamic_cast<Derived*>(basePtr) 向下转型(需 RTTI)父类 → 子类,失败时返回 nullptr
const_cast const_cast<char*>(cstr) 加/去 const
reinterpret_cast reinterpret_cast<int*>(p) 按位重解释(低级暴力)能转但风险高!

隐式转换,如何消除它

编译器自动帮你做的类型转换,不需要你写 cast 或构造函数,常在赋值、传参、运算中发生。

  • 使用 explicit 关键字
  • 使用 delete 禁用不想要的转换
  • 使用强转类型转换(明确表达)

cout和printf的区别(了解)

cout和printf都是输出对应数据,而cout有缓冲输出,使用flush立即强迫缓冲输出


什么是虚函数

  • 定义:用 virtual 修饰的成员函数;用于运行时多态(动态绑定)。

  • 机制(实现惯例):每个多态对象含一根 虚指针指向 虚函数表;通过 虚函数表 在运行时决定调用哪个函数实现。

虚函数的底层原理基于虚函数表(vtable)虚函数表指针(vptr)

每个包含虚函数的类都有一个对应的虚函数表,该表是一个函数指针数组,存储了该类所有虚函数的入口地址。每个该类的对象内部都包含一个隐藏的虚函数表指针(vptr),指向其类的虚函数表。

当通过基类指针或引用调用虚函数时,程序会:

  1. 通过对象的 vptr 找到其虚函数表。
  2. 在虚函数表中根据偏移量找到对应的函数指针。
  3. 通过该函数指针调用正确的函数实现(若派生类重写了虚函数,则调用派生类的版本)。

这个过程实现了动态绑定(运行时多态),使得调用的函数版本取决于对象的实际类型,而非指针或引用的声明类型。如果派生类重写了基类的虚函数,其虚函数表中对应的函数指针会被更新为指向派生类的函数。

虚函数类定义的,但调用属于对象

虚函数位于文本段(.text),虚函数表位于数据段(.data),虚函数表指针(vptr)存放在每个对象实例的内存空间开头(具体位置与对象存储位置相同,可能在栈、堆或数据段)


什么是纯虚函数

定义:在基类中声明但没有实现的虚函数,语法:

1
virtual void func() = 0;

特点

  1. 含有纯虚函数的类是 抽象类,不能实例化。
  2. 常用于定义 接口/规范

Q:纯虚函数和虚函数的区别?

A:纯虚函数只是相当于一个接口名。


C++中如何阻止一个类被实例化

  • 构造函数设为 private/protected
  • 纯虚析构函数抽象类

什么函数不能声明为虚函数

  • 构造函数:对象还没完全构造,不能通过虚表实现多态。

  • 静态成员函数:不依赖对象(没有 this 指针),无法放入虚表。

  • 内联函数inline):可以是虚函数,但一旦被声明为虚函数,就失去强制内联的意义

  • 友元函数:不属于类成员,不能放入虚表。

  • 模板函数:普通模板函数不能直接虚化。


为什么C++默认的析构函数不是虚函数

  • 普通类通常 不会被继承,非虚析构更高效。

  • 只有涉及 多态使用(基类指针/引用指向派生类对象) 时,才需要虚析构。


为什么析构函数必须是虚函数

  • 如果通过 基类指针删除派生类对象,没有虚析构会只调用基类析构,导致派生类资源未释放 → 内存泄漏/未定义行为

  • 设为虚函数,能保证删除时按正确的继承层次依次调用析构函数。


静态函数和虚函数的区别

静态函数(static)

  • 属于类本身,不依赖对象
  • 不能访问非静态成员(没有 this 指针)。
  • 调用方式:ClassName::func()
  • 绑定方式:编译期绑定(静态绑定)。

虚函数(virtual)

  • 属于对象,支持 运行时多态
  • 通过虚函数表(vtable)在运行时决定调用哪个版本。
  • 必须通过对象或对象指针/引用调用。
  • 绑定方式:运行时绑定(动态绑定)。

构造函数有几种,分别什么作用

  • 默认构造函数
    • 无参数或参数有默认值。
    • 作用:创建对象时提供默认初始化
  • 有参构造函数
    • 带参数。
    • 作用:用指定值初始化对象。
  • 拷贝构造函数
    • 形如 Class(const Class& obj)
    • 作用:用已有对象初始化新对象(值传递/返回对象时会调用)。
  • 移动构造函数(C++11)
    • 形如 Class(Class&& obj)
    • 作用:接管临时对象的资源,避免拷贝开销。

拓展:

  • 委托构造函数(C++11)
    • 一个构造函数调用同类的另一个构造函数。
    • 作用:减少代码重复。
  • 显式构造函数(explicit)
    • explicit 修饰。
    • 作用:防止隐式类型转换。

构造函数和析构函数执行顺序

  • 构造函数
    • 基类构造函数
    • 成员类对象构造函数
    • 派生类构造函数
  • 析构函数
    • 派生类析构函数
    • 成员类对象析构函数
    • 基类析构函数

构造函数、拷贝构造函数和赋值运算符的区别

  • 构造函数对象不存在创建一个新对象时调用
  • 拷贝构造函数对象不存在,用别的已经存在的对象来初始化
  • 赋值运算符对象存在,用别的对象给它赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<iostream>
using namespace std;
class A{
public:
A(){
cout << "构造函数" << endl;
}
A(const A& a){
cout << "拷贝构造函数" << endl;
}
A& operator = (A& a){
cout << "赋值运算符" << endl;
return this;
}
~A(){}
};
int main()
{
A a1; //构造函数
A a2 = a1; //拷贝构造
a2 = a1; //赋值运算符
}
//打印出的结果:
// 构造函数
// 拷贝构造
// 赋值运算符

只定义析构函数,会自动生成哪些构造函数

  • 默认构造函数
  • 拷贝构造函数
  • 拷贝赋值运算符

一个类默认会生成哪些函数

  • 无参构造函数(默认构造函数)

  • 拷贝构造函数

  • 拷贝赋值运算符 (operator=)

  • 析构函数(非虚,除非基类里有虚函数)

1
2
3
4
5
// 与上方一一对应
Empty();
Empty(const Empty&);
Empty& operator=(const Empty&);
~Empty();

在 C++11 及以后,可能还会自动生成:

  • 移动构造函数
  • 移动赋值运算符

什么是虚拟继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
using namespace std;

class A{}
class B : virtual public A{};
class C : virtual public A{};
class D : public B, public C{};

int main()
{
cout << sizeof(A) << endl; // 1,空对象
cout << sizeof(B) << endl; // 4,一个bptr指针
cout << sizeof(C) << endl; // 4,一个bptr指针
cout << sizeof(D) << endl; // 8,两个bptr指针
}

上述中B和C虚拟继承A,D又继承B和C,这种方式是菱形继承/钻石继承,无论基类被继承多少次,只会存在一个实体。


指针和引用的区别

  • 指针存地址引用变量别名

  • 指针可为空引用必须初始化且不可为空

  • 指针可多级,引用只有一级

  • 指针能改变指向引用绑定后不可改变指向


什么时候指针,什么时候引用

  • 用指针的场景

    • 参数可以为空。

    • 需要在函数中改变指向。

    • 需要返回函数内部申请的内存。

    • 与 C 接口交互(C 风格就是指针)。

  • 用引用的场景

    • 必须有对象,不允许空。

    • 对空间敏感(如递归、大对象),引用避免拷贝,开销小


类如何实现只能静态分配和只能动态分配

  • 只能静态分配(禁止堆上创建):把new、delete重载private属性
  • 只能动态分配(禁止栈上创建):把构造、析构函数设为protected属性,再用子类动态创建

什么是静态绑定和动态绑定

  • 静态绑定编译时决定,非虚函数,效率高,无需特殊关键字说明

  • 动态绑定运行时决定,虚函数 + 指针/引用,支持多态virtualoverride)。


结构体变量比较是否相等

C语言

  • 不支持 == 比较结构体变量
  • 必须手动逐个字段比较,或用 memcmp(不推荐,可能有填充位)。

C++98 / C++11 / C++14 / C++17

  • 默认不支持结构体比较。
  • 需要手动重载 operator==
1
2
3
4
5
6
struct Point {
int x, y;
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
};

C++20及以上

  • 支持结构体自动生成比较操作(需要使用 = default)。
1
2
3
4
struct Point {
int x, y;
auto operator<=>(const Point&) const = default; // 自动生成 ==、<、<=、> 等
};

内联函数(inline

作用:建议编译器在调用处直接展开函数体,减少函数调用开销(避免压栈/跳转)。

特点

  • 编译阶段,有类型检查。

  • 只是建议,编译器可选择忽略。

  • 适合短小、频繁调用的函数。

  • 递归函数过大函数通常不会被内联。

  • 内联函数必须在调用前可见

优势

  • 提高性能(减少调用开销)。
  • 保持函数形式(比宏函数更安全,有类型检查)。

局限

  • 会增加代码体积(代码膨胀)。
  • 不能用于虚函数的多态调用(运行时决定的调用无法内联)。

extern “C” 的作用

  • 用于 C++ 中:告诉编译器按 C 语言方式 编译函数。
  • 作用:避免 C++ 的 函数名修饰 (name mangling),便于与 C 代码或库兼容。

C++中NULL和nullptr区别

NULL来自C语言,nullptr则是C++11新增关键字。

  • NULL(void*)0,有类型歧义,不推荐

  • nullptr 是专门的空指针类型,类型安全

Q:C语言可以没有NULL吗?

  • NULL 不是必须的,只是 (void*)0 的别名。

  • C语言可以不用 NULL,但实际开发强烈建议用 NULL 表示空指针,提升可读性和规范性。


C++11有哪些新特性

  • 智能指针
  • auto
  • 右值引用
  • 匿名函数lambda
  • 多线程支持库

还有其他的,建议自己再搜索


this指针

  • this 是一个指向当前对象首地址的指针

  • this只能在成员函数使用,不能在全局函数、静态函数中使用。

  • 存储位置因编译器不同而不同。

Q1:this指针是什么时候创建?

A1:在每次调用非静态成员函数时,编译器会隐式传入当前对象的地址作为 this 参数。所以 this指针 不是在构造对象时创建的,而是在成员函数执行时临时传入的

Q2:this指针存放在哪里?

A2:this指针如果是一个局部变量,通常存放在寄存器/栈中。


左值和右值的区别

  • 左值 (lvalue):表示一块可寻址的内存,可以出现在赋值符号左边。
    • 例:变量名 a,数组元素 arr[2],解引用 *p
  • 右值 (rvalue):表示 临时值、常量、表达式结果,生命周期短,不能单独取地址
    • 例:常量 10,表达式 a+b
  • C++ 扩展(右值细分)
    • 纯右值 (prvalue):字面量、临时对象。
    • 将亡值 (xvalue):可被“偷资源”的临时对象(支持移动语义)。
  • 左值:有名字、有地址、能长期存在。
  • 右值:没名字、临时用完就丢。

常见易混例子

  • i++右值(返回旧值的临时量,不能赋值)。
  • ++i左值(返回自增后的变量本身,可继续赋值)。
  • arr[0]左值(数组元素,可寻址)。
  • a+b右值(表达式结果,临时)

C/C++的关键字(了解)

C语言

1
2
3
4
5
6
7
auto        break       case        char        const  
continue default do double else
enum extern float for goto
if inline int long register
restrict return short signed sizeof
static struct switch typedef union
unsigned void volatile while

相较于C语言,C++新增

1
2
3
4
5
6
7
8
9
10
11
12
13
alignas     alignof     and         and_eq      asm  
bitand bitor bool catch char16_t
char32_t class compl const_cast constexpr
consteval constinit co_await co_return co_yield
decltype delete dynamic_cast explicit export
extern "C" false friend inline mutable
namespace new noexcept nullptr operator
or or_eq private protected public
reflexpr reinterpret_cast requires static_assert
static_cast struct template this thread_local
throw true try typeid typename
union using virtual wchar_t xor
xor_eq

vector的底层实现

结构:动态数组,内存连续,可随机访问。

扩容容量不足时按倍数增长,重新分配并搬移元素

迭代器失效:扩容后所有迭代器失效;非扩容时,插入/删除点之后的迭代器失效


vector 和 list 的使用场景与区别

  • vector
    • 底层实现:动态数组,元素连续存储。
    • 优势:支持 随机访问 O(1),缓存友好,遍历效率高。
    • 劣势:中间插入/删除需要移动大量元素,效率低。
    • 适用场景:查询、顺序访问多,增删少。
  • list
    • 底层实现:双向链表,元素分散存储。
    • 优势插入/删除 O(1)(已知位置时),不会移动其他元素。
    • 劣势:不支持随机访问,只能顺序遍历;节点额外指针开销大,缓存不友好。
    • 适用场景:需要频繁在中间插入、删除。

Q:如果有10w个数据,那么查找一个元素,list和vector哪个效率更高?

A:查找要根据实际情况选用,而不是纯理论。

  • 如果位于中间,则vector
  • 如果位于开头或结尾,则list

(如果不知道处于什么位置,那就用vector吧,更简单)


Vector下的resize和reserve的区别

resize(n)改变大小,若变大则会构造新元素(对内置类型填充为 0,对类调用默认构造),可能导致 迭代器失效

reserve(n)只调整容量,不改变元素数量;不会插入新元素,所以不会填充 0,只是预留空间;若实际扩容则 迭代器失效

Vector的内存占用空间只增不减,即使erase/clear也是不变


Vector.size()是capacity,还是已存储的元素个数

已存储的元素个数

size 是“已经放了几个”,capacity 是“最多能塞几个”。


如何理解迭代器,容器的迭代器是由什么组成的

定义:迭代器是“指针的泛化”,用来访问容器中的元素。

作用:统一访问方式,让不同容器都能用同样的算法操作。

底层通常是 指针类对象,封装了:

  • 当前元素的位置(指针/索引)。
  • 运算操作(++, --, *, -> 等)。
  • 类型定义(value_type, reference, pointer 等)。

STL中迭代器有什么用,有指针了为何还要迭代器

作用:在不同容器上提供统一的访问方式,配合算法使用。

为什么不直接用指针

  • 不是所有的容器都用连续内存存储(如 list、map)。
  • 迭代器可像指针一样操作,但能适配各种容器。

STL中有哪些容器,这些容器的迭代器是如何删除元素

常见容器

  • 顺序容器:vectordequelist

  • 关联容器:setmapmultisetmultimap

  • 无序容器:unordered_setunordered_mapunordered_multisetunordered_multimap

删除元素与迭代器

  • vector / dequeerase(it),返回下一个迭代器;删除点及其后的迭代器全部失效

  • listerase(it),只使 it 失效,其他迭代器不变

  • map / set / 无序容器erase(it),只使当前迭代器失效,其他迭代器保持有效

STL容器结构

序列式容器

  • vector动态数组(连续内存块,通常维护起始、尾后和容量末尾三个指针)
  • deque分块数组 + 中控器(一个指针数组管理多个固定大小的连续内存块)
  • list双向循环链表(每个节点包含指向前后节点的指针和数据)
  • forward_list单向链表(每个节点仅包含指向下一个节点的指针和数据)
  • array静态数组(固定大小的连续内存,编译时确定)

关联式容器(有序)

  • set / map / multiset / multimap红黑树

关联式容器(无序)

  • unordered_set / unordered_map / unordered_multiset / unordered_multimap哈希表

容器适配器

  • stack:默认基于 deque 封装(也可指定 vectorlist
  • queue:默认基于 deque 封装(也可指定 list
  • priority_queue:默认基于 vector 封装(堆算法,默认为大顶堆)

STL中的map和set有什么区别,怎么实现的

  • 存储内容

    • map:存储 key-value 键值对,key 唯一。

    • set:只存储 key,元素唯一。

  • 访问方式

    • map[key] 可直接通过下标访问或修改 value。

    • set 只能通过迭代器遍历查找。

  • 底层实现

    • 两者通常都用 红黑树(平衡二叉搜索树) 实现,元素有序。

    • map 的节点存 (key, value)set 的节点只存 key

    • 插入、删除、查找复杂度均为 O(log n)

Q:为什么map和set使用红黑树

它们要求自动排序,而红黑树能够实现这一功能,并且时间复杂度较低


STL中的map和unordered_map区别

map

  • 底层实现红黑树(平衡二叉搜索树)。
  • 存储内容:节点存放 (key, value)
  • 存放规则:按 key 有序 排列。
  • 复杂度:查找/插入/删除 O(log n)
  • 遍历:迭代器中序遍历即有序。

unordered_map

  • 底层实现哈希表(bucket + 链表/拉链法 或 开放地址法)。
  • 存储内容:元素 (key, value) 存放在某个桶中。
  • 存放规则:按 哈希值分桶,无序。
  • 复杂度:平均 O(1),最坏 O(n)
  • 遍历:迭代器顺序取决于哈希分布,不能保证有序。

STL的map插入方式

  • 用insert函数插入pair

  • 用insert函数插入value_type

  • 用insert函数插入make_pair()

  • 用数组方式插入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
map<int, string> myMap;

// 1. 用insert函数插入pair
myMap.insert(pair<int, string>(1, "One"));

// 2. 用insert函数插入value_type
myMap.insert(map<int, string>::value_type(2, "Two"));

// 3. 用insert函数插入make_pair()
myMap.insert(make_pair(3, "Three"));

// 4. 用数组方式插入数据
myMap[4] = "Four"; // 键不存在,插入
myMap[4] = "Four Again"; // 键已存在,覆盖原有值

Q:map中的[]与find的区别?

A:operator[]用于访问或插入元素,若键不存在则会自动创建新键值对(值默认初始化)并返回其引用;find仅用于查找元素,返回迭代器,若键不存在则返回 end()迭代器,不会修改map。若需检查键是否存在且避免意外插入,应优先使用find


什么是初始化列表,哪些情况下只能初始化列表,而不能赋值

初始化列表 (Initializer List)

  • 概念:在构造函数冒号后,用 {}() 直接对成员进行初始化。

    1
    2
    3
    4
    5
    6
    class A {
    int x;
    int y;
    public:
    A(int a, int b) : x(a), y(b) {} // 初始化列表
    };
  • 区别

    • 初始化列表 → 在对象创建时直接初始化成员。
    • 构造函数体赋值 → 先默认初始化,再在函数体里赋值。

必须用初始化列表的情况

  1. const 成员
  2. 引用成员 &
  3. 无默认构造函数的成员对象
  4. 基类构造函数调用

没有参数的函数能不能被重载

  • 可以重载,但前提是 参数列表不同

  • 如果都是 无参数,则无法区分,编译报错。

1
2
3
4
5
6
7
void fun();          //  定义1
void fun(int x); // 定义2(重载)
void fun(double x); // 定义3(重载)

// 错误:重复定义
void fun();
void fun();

计算机网络

OSI 四层模型 / 七层模型

  • 四层模型

    1. 应用层:提供应用服务(HTTP、FTP、SMTP、DNS)。

    2. 传输层:端到端通信,保证可靠/快速(TCP、UDP)。

    3. 网络层:选择路径、寻址转发(IP、ICMP)。

    4. 网络接口层:封装/解封装成帧并传输(以太网、PPP、物理层比特流)。

  • 七层模型

    1. 应用层:面向用户的软件(HTTP、FTP、SMTP)。

    2. 表示层:数据表示、加密/解密、压缩/解压缩。

    3. 会话层:会话建立、管理和终止。

    4. 传输层:端到端通信,流量控制、差错控制(TCP、UDP)。

    5. 网络层:逻辑寻址和路由选择(IP、ICMP)。

    6. 数据链路层:成帧、差错检测、MAC 地址(ARP、PPP、以太网)。

    7. 物理层:比特流传输(网线、网卡标准)。


HTTP是什么

定义:超文本传输协议(HyperText Transfer Protocol),基于 TCP/IP 的应用层协议,用于浏览器和服务器之间传输数据。


HTTP 的发展史

  • HTTP/1.0短连接,请求一次建立一次 TCP 连接。
  • HTTP/1.1:默认长连接,支持流水线。
  • HTTPS:HTTP + SSL/TLS,加密传输
  • HTTP/2.0多路复用、头部压缩、二进制帧,用流的形式发送。
  • HTTP/3.0:基于 QUIC 协议(UDP),减少握手延迟。

HTTP请求和响应报文包含哪些字段

  • 请求报文
    • 请求行
    • 请求头
    • 请求体
  • 响应报文
    • 状态行
    • 响应头
    • 响应体

HTTP请求流程

  1. DNS解析域名IP
  2. 根据IP,建立TCP连接
  3. 发送HTTP请求
  4. 服务器响应请求,得到HTML代码
  5. 关闭TCP连接
  6. 浏览器解析HTML代码,并请求相关资源(js、css、图片)
  7. 浏览器渲染页面呈现

GET和POST的区别

  • GET是获取数据,POST是修改数据
  • GET是幂等,POST不是幂等
  • GET后服务器响应200 ok;POST后服务器先响应100 Continue,再响应200 ok

因此POST会产生两个TCP数据包

  • GET提交的数据有上限,POST没有上限

HTTP和HTTPS的区别

  • HTTPS协议是由SSL+HTTP协议构建的可加密传输的网络协议;

  • HTTPS需要申请CA证书

  • HTTPS使用443端口HTTP使用80端口


对称加密和非对称加密

  • 对称加密
    • 加密和解密使用同一个密钥
    • 速度快,适合加密大量数据。
    • 核心问题:如何安全地共享密钥。
  • 非对称加密
    • 使用一对密钥:公钥私钥
    • 公钥加密的数据,只能用对应的私钥解密
    • 私钥签名的数据,可以用对应的公钥验证来源。
    • 速度慢,主要用于密钥交换和数字签名。

HTTPS请求流程

  1. 先通过 DNS 解析获取服务器的 IP 地址
  2. 接着 TCP 三次握手建立网络连接;
  3. 客户端向服务端发起一个 HTTPS 请求(含有Client RandomSSL/TLS套件版本);
  4. 服务器会将其数字证书(包含公钥)发送给客户端(含有Server Random);
  5. 客户端会验证该证书的合法性,包括检查其是否由可信机构颁发、是否在有效期内以及域名是否匹配等。验证通过后,客户端会生成一个用于后续对称加密Pre-Master Secret(预主密钥);
  6. 客户端用服务器证书中的公钥对Pre-Master Secret加密后发送给服务器;
  7. 服务器使用自己的私钥解密Pre-Master Secret,获取该预主密钥;
  8. 随后,客户端和服务器会利用握手过程中交换的 Client Random(客户端随机数)、Server Random(服务端随机数)和刚刚协商出的 Pre-Master Secret,各自独立计算生成相同的会话密钥(Master Secret);
  9. 此后的所有通信数据都将使用这个会话密钥进行对称加密和解密,确保数据传输的机密性和完整性。

常见HTTP状态码

HTTP状态码是服务器对请求处理结果的标识。

  • 1xx(信息性状态码):表示请求已被接收,需要继续处理。

    • 100 Continue(客户端可继续发送请求)
  • 2xx(成功状态码):表示请求已成功被服务器接收、理解并处理。

    • 200 OK(请求成功)
    • 201 Created 表示新资源被创建
    • 204 No Content 表示成功但无内容返回
  • 3xx(重定向状态码):表示需要进一步操作以完成请求。

    • 301 Moved Permanently 是永久重定向
    • 302 Found 是临时重定向
    • 304 Not Modified 告知客户端可使用缓存资源
  • 4xx(客户端错误状态码):表示请求可能出错。

    • 400 Bad Request 指请求有语法错误
    • 401 Unauthorized 表示需要身份验证
    • 403 Forbidden 是服务器拒绝请求
    • 404 Not Found 表示资源不存在
  • 5xx(服务器错误状态码):表示服务器处理请求时出错。

    • 500 Internal Server Error 是服务器内部错误
    • 503 Service Unavailable 表示服务暂时不可用

什么是DNS

Domain Name System,域名系统;是域名和IP地址相互映射数据库

DNS域名解析通过UDP协议,输入域名后流程:

  1. 检查浏览器缓存是否包含这个域名映射的IP地址;若没有,执行2;
  2. 检查操作系统缓存;若没有,执行3;
  3. 检查本地域名服务器解析(LDNS);若没有,执行4;
  4. 检查根域名服务器(.com),一步一步往下传,最终返回对应的IP地址。

DNS 负载均衡

DNS 负载均衡是一种通过 DNS 解析过程 来分配网络流量的策略。其核心是 一个域名对应多个 IP 地址。当用户访问这个域名时,DNS 服务器会根据预设的策略,从这些 IP 地址中选择一个返回给用户,从而将访问流量分散到不同的服务器上。


TCP 和 UDP 的区别

  • TCP:面向连接,可靠传输,基于字节流的,速度慢。
  • UDP:面向报文,无连接,不可靠传输,速度快。

MTU和MSS分别是什么

Maximum Transmission Unit,最大传输单元,即IP头+TCP头+DATA

Maximum Segment Size,最大段长,即DATA


TCP 粘包和拆包问题,怎么解决

  • 原因:TCP 是字节流协议,不保证消息边界 → 多个包合并或一个包被拆分。
  • 解决方法
    • 固定长度消息
    • 使用分隔符
    • 在消息头加长度字段
    • 应用层协议处理(如 HTTP、MQTT)

TCP 通信如何保证通信的可靠性

  • 确认应答机制(ACK)
  • 校验和
  • 有序性保证(序列号保证数据按顺序到达)
  • 超时重传
  • 滑动窗口(流量控制):当接收方来不及处理发送方数据,可以通过滑动窗口,提示发送方降低发送速率
  • 拥塞控制(慢启动、拥塞避免、拥塞发生、快速重传/恢复)

如何区分流量控制和拥塞控制

  • 流量控制属于通信双方协商,拥塞控制涉及通信链路全局
  • 流量控制需要通信双方各维护一个发送窗和接收窗;发送窗由接受方响应的TCP窗口大小确定,接收窗由自身决定
  • 拥塞控制的窗口大小变化由试探性发送一定数据量数据探查网络状态后自适应调整
  • 实际发送窗口 = min{流量发送窗口,拥塞窗口}

TCP 三次握手(连接建立)

  1. 客户端 → 服务端:发送 SYN=1, seq=x
    • 表示请求建立连接,并告知初始序列号 x
    • 客户端进入 SYN_SENT 状态**(半连接)**。
  2. 服务端 → 客户端:发送 SYN=1, ACK=1, seq=y, ack=x+1
    • 确认收到了客户端的 SYN,同时自己也发起连接请求。
    • 服务端进入 SYN_RCVD 状态。
  3. 客户端 → 服务端:发送 ACK=1, ack=y+1
    • 确认收到了服务端的 SYN。
    • 客户端进入 ESTABLISHED 状态。
    • 服务端收到 ACK 后,也进入 ESTABLISHED(全连接)

👉 为什么三次?

  • 防止已失效的连接请求报文突然到达而引起错误(历史 SYN 报文问题)。
  • 双方要确认 对方的收发能力

🍊 什么是半连接队列和全连接队列?

  • 服务器第一次接收到客户端的SYN,会处于SYN_RECV状态,此时是半连接队列。
  • 服务器第二次接收到客户端的SYN+ACK,会处于SYN_ACK状态,此时是全连接队列。

⚙️ 三次握手可以携带数据吗?

  • 第一次第二次握手不允许携带第三次可以携带

TCP 四次挥手(连接释放)

  1. 客户端 → 服务端:发送 FIN=1, seq=u
    • 表示“客户端已无数据要发”,请求关闭连接。
    • 客户端进入 FIN_WAIT_1 状态。
  2. 服务端 → 客户端:发送 ACK=1, ack=u+1
    • 确认收到了 FIN,但可能还有数据要发。
    • 服务端进入 CLOSE_WAIT 状态;客户端进入 FIN_WAIT_2
  3. 服务端 → 客户端:发送 FIN=1, seq=v
    • 当服务端也没有数据要发时,主动关闭。
    • 服务端进入 LAST_ACK 状态。
  4. 客户端 → 服务端:发送 ACK=1, ack=v+1
    • 确认收到服务端的 FIN。
    • 客户端进入 TIME_WAIT,等待 2MSL 确保最后 ACK 不丢失。
    • 服务端收到 ACK 后进入 CLOSED,释放连接。

👉 为什么要四次?

  • TCP 是全双工的,关闭要分成两个方向。
  • 一方发送 FIN 表示“我这边没数据了”,但另一方可能还有数据要发 → 所以 ACK 和 FIN 分开发。

为什么客户端最后还要等待2MSL

MSL(Maximum Segment Lifetime):报文在网络中的最长存活时间。

等待 2MSL 的原因

  1. 保证 ACK 能到达
    • 客户端最后发给服务端的 ACK 可能丢失。
    • 等待 2MSL 可确保服务端若未收到 ACK,重发 FIN,客户端还能再次回应。
  2. 清除旧报文
    • 2MSL 时间足够让网络中的所有旧 TCP 报文消失,避免影响后续新的连接。

Cookie 是服务器发送到用户浏览器并保存在本地的一小块文本数据。浏览器会存储它,并在后续向同一服务器发起的请求中自动携带。

  • 会话状态管理(如保持登录)
  • 个性化设置(如语言偏好)
  • 浏览器行为追踪

什么是 Session

Session 是一种在服务器端保存用户状态信息的机制。服务器会为每个用户会话创建一个唯一的标识(Session ID),通常通过 Cookie 传递给客户端。后续请求中,客户端凭此 ID 即可让服务器识别出用户并访问其对应的会话数据,从而在无状态的 HTTP 协议上实现有状态的交互


socket网络编程中用到哪些函数

  • 服务端(Server)
  1. socket() → 创建套接字。
  2. bind() → 绑定 IP 和端口。
  3. listen() → 监听端口,等待连接。
  4. accept() → 接收客户端连接,返回新的套接字。
  5. recv() / send()read() / write() → 收发数据。
  6. close() → 关闭连接。

  • 客户端(Client)
  1. socket() → 创建套接字。
  2. connect() → 连接服务端。
  3. recv() / send()read() / write() → 收发数据。
  4. close() → 关闭连接。

DHCP 协议(Dynamic Host Configuration Protocol)(了解)

  • 作用:自动为主机分配网络参数(IP 地址、子网掩码、网关、DNS 等),避免手工配置。
  • 工作方式:基于 UDP,通常通过广播通信。

应用层:DHCP 协议本身(运行在客户端和服务器之间,分配网络参数)。

传输层:使用 UDP(客户端端口 68,服务器端口 67)。

网络层:依赖 IP(广播 255.255.255.255 或子网广播地址)。

链路层:在第一次请求时,客户端可能还没有 IP,会用 MAC 地址标识自己。


ARP和RARP

ARP:IP地址转物理地址

RARP:物理地址转IP地址


Ping命令基于什么协议(了解)

Ping是基于网络层的ICMP协议实现。通过向对方发送一个ICMP回送请求报文


IP 地址、子网掩码、网关和 DNS 作用

  • IP 地址:设备在网络中的唯一标识。
  • 子网掩码:划分网络号和主机号。
    • 网络号 = IP 地址 & 子网掩码
    • 主机号 = IP 地址 & (子网掩码取反)
  • 网关:跨网络通信的出口。
  • DNS:域名解析,将域名转换为 IP 地址。

IPv4 和 IPv6 的区别(了解)

地址长度

  • IPv4:32 位地址,约 43 亿个地址。
  • IPv6:128 位地址,几乎无限,解决地址枯竭问题。

地址表示

  • IPv4:点分十进制(192.168.0.1)。
  • IPv6:冒号十六进制(2001:db8::1)。

首部结构

  • IPv4:首部字段复杂,最多 60 字节。
  • IPv6:首部简化,固定 40 字节,处理更高效。

地址配置

  • IPv4:可手动、DHCP。
  • IPv6:支持自动配置(无状态地址自动配置,SLAAC)。

安全性

  • IPv4:依赖应用层/扩展(如 IPSec 可选)。
  • IPv6:IPSec 是强制支持的。

广播方式

  • IPv4:支持广播。
  • IPv6:取消广播,改用 组播/任播

常用的网络拓扑类型(了解)

  • 星型:所有节点通过中心设备连接,可靠但中心单点故障。
  • 总线型:共享一条总线,成本低但冲突多。
  • 环型:形成闭环,适合定时传输。
  • 树型:层次化管理。
  • 网状型:高可靠性,多路径冗余。

操作系统

进程、线程和协程的区别

进程(Process)

  • 操作系统进行 资源分配 的基本单位。
  • 进程间相互独立,通信需要 IPC(Inter-Process Communication)。
  • 切换开销大。

线程(Thread)

  • CPU 资源执行 的最小单位。
  • 共享内存资源(代码段、堆),但有独立的栈和寄存器。
  • 切换开销比进程小,但仍需内核参与

协程(Coroutine)

  • 用户态的 轻量级线程,由程序自身调度
  • 主动让出 CPU(非抢占式),切换只保存寄存器/栈指针,开销极小。
  • 适合大量并发 IO 场景(如异步网络请求)。

Q:C++中创建线程的函数?

A:std::thread


进程间通信方式

管道(Pipe)

  • 无名管道(只能在有血缘关系的进程间通信)和 有名管道(FIFO,可在无血缘进程间通信)。
  • 半双工:同一时间只能单向传输。

消息队列(Message Queue)

  • 内核维护的 消息链表
  • 适合多对多通信,但消息体积受内核限制。

共享内存(Shared Memory)

  • 多个进程共享同一块物理内存。
  • 速度最快,因为数据不需要在内核与用户空间拷贝。
  • 通常需要配合 信号量 做同步。

共享内存核心是将同一块物理内存映射到多个进程的虚拟地址空间,使这些进程能直接读写该内存区域,无需内核中转数据,从而获得极高的通信速度。

信号量(Semaphore)

  • 一个 计数器,用于控制对共享资源的访问。
  • 常用于 进程/线程间的互斥与同步,本身不传递数据。

信号(Signal)

  • 一种 异步通知机制,用于告诉进程发生了某事件(如 Ctrl+C 触发 SIGINT)。
  • 常用于进程控制、异常处理。

套接字(Socket)

  • 可用于 同一台机器不同机器 之间的通信。
  • 支持 双向通信,是网络编程的核心。

进程间通信的选择

  • 管道(Pipe)
    • 简单、快速,适合 父子进程间少量数据传递
  • 消息队列(Message Queue)
    • 适合 多进程间有序/分类消息传递,但大数据效率较低。
  • 共享内存(Shared Memory)
    • 最快,适合 大量数据、高性能通信;需配合信号量保证同步。
  • 信号量(Semaphore)
    • 计数器,控制同步与互斥,通常与共享内存结合使用。
  • 信号(Signal)
    • 事件通知/异常处理,不适合传递大数据,如Ctrl+C。
  • 套接字(Socket)
    • 跨主机通信,适合分布式系统和网络通信。

简单父子通信 → 管道

多进程消息传递 → 消息队列

大量数据共享 → 共享内存 + 信号量

事件通知 → 信号

跨主机/网络 → 套接字


进程间的状态

  • 创建(new):进程创建中。
  • 就绪(ready):等待 CPU 调度。
  • 运行(running):占用 CPU 正在执行。
  • 阻塞(waiting):等待 I/O 或事件。
  • 终止(terminated):执行完成或被终止。

线程间通信方式

临界区(Critical Section)

  • 保护共享资源的那段代码片段,一次只允许一个线程进入。

互斥量(Mutex)

  • 互斥锁机制,线程必须先获取锁才能访问资源。
  • 适合 独占访问

信号量(Semaphore)

  • 计数器,允许多个线程同时访问一定数量的资源。
  • 适合 并发限流

条件变量(Condition Variable)

  • 线程可等待某条件满足后再执行,常与互斥锁配合。
  • 适合 线程间同步/通知

读写锁(RWLock)

  • 同时允许多个读,但写时独占。
  • 适合 读多写少 的场景。

事件(Event,对象/信号量变体)

  • 一个线程发出信号,唤醒等待的线程。
  • 常用于 线程间通知/状态同步

互斥量/临界区 → 独占资源

信号量 → 限制并发数量

条件变量/事件 → 通知与同步

读写锁 → 读多写少优化


多进程和多线程的适用场景区别

  • 多进程稳定性好,进程隔离,不限制开销和效率的场景。适合多核 CPU、大规模并发服务。
  • 多线程:共享内存,切换开销小,效率高。适合计算密集型、轻量级并发。

什么是线程池

  • 线程池:预先创建一定数量的线程,重复利用来执行任务。
  • 优点:减少频繁创建/销毁线程的开销,提高并发性能。

典型应用:服务器请求处理。


什么是线程安全

  • 一个函数/代码段在多线程环境下被多个线程同时调用时,能保证结果正确。
  • 实现方法:加锁、原子操作、TLS(线程局部存储)。

僵尸进程、孤儿进程和守护进程

  • 僵尸进程:子进程结束但父进程未回收(未调用 wait)。
  • 孤儿进程:父进程退出,子进程被 init/systemd 接管。
  • 守护进程:在后台运行、无终端控制的进程。

僵尸进程有什么危害,如何解决

  • 占用进程号(PID),大量僵尸进程会耗尽系统可用 PID,导致新进程无法创建。

  • 占用少量内核资源(PCB)。

解决方法

  • 父进程调用 wait() / waitpid() → 正常回收子进程。

  • 父进程结束 → 子进程由 init(PID 1)接管并回收。

  • 发送信号杀父进程 → 触发系统回收子进程。


什么是内核线程和用户线程

  • 内核线程(Kernel Thread)

    • 操作系统内核 创建和管理。

    • 线程调度、切换都在内核态完成。

    • 开销较大,但能充分利用多核 CPU。

  • 用户线程(User Thread)

    • 完全由 用户态库 实现,内核无感知。

    • 线程切换开销小(不陷入内核)。

    • 缺点:若一个线程阻塞,整个进程都会阻塞(N:1 模型)。


进程调度算法有哪些

  • 先来先服务(FCFS)

    • 按到达顺序调度,公平但可能等待时间长。
  • 短作业优先(SJF)

    • 选择运行时间最短的进程,平均等待时间最小;对长作业不利。
  • 最短剩余时间优先(SRTF)

    • SJF 的抢占式版本,剩余时间短的优先。
  • 优先级调度

    • 按优先级选择进程,高优先级可能导致低优先级饥饿。
  • 时间片轮转(RR)

    • 每个进程按时间片轮流执行,适合分时系统。
  • 多级队列调度

    • 不同类型进程放在不同队列,队列间有优先级。
  • 多级反馈队列调度(MLFQ)

    • 进程可在队列间动态调整,综合考虑响应与效率。

并发和并行

并发(Concurrency)- 单CPU

  • 逻辑上同时发生:在同一时间段内交替执行多个任务。
  • 依赖 操作系统调度,强调 任务切换

并行(Parallelism)- 多CPU

  • 物理上同时发生:在同一时刻由多个处理器同时执行。
  • 需要 多核/多处理器 支持,强调 真正同时运行

单核机械上写多线程程序,是否考虑加锁(了解)

要考虑加锁,原因:

  • 可抢占与切换:单核也会在任意时刻发生线程切换,共享可变数据会产生 竞态
  • 内存可见性:没有同步就没有 happens-before 关系;编译器/CPU 重排序、缓存导致线程间 看不见彼此更新
  • 原子性:读取-修改-写入这类复合操作在无锁下会被打断,出现数据损坏。

内存交换和覆盖有什么区别

内存交换技术主要在不同进程间进行,而内存覆盖是在同一个进程中。


为什么使用虚拟内存

  • 隔离进程互不干扰,更安全。

  • 扩展地址空间大于物理内存,扩大空间

  • 简化:程序只管虚拟地址,底层分配由系统处理。

  • 保护防止非法访问

  • 共享可让多个进程共享一块物理内存


逻辑地址转化为物理地址的基本流程

  1. CPU执行指令时生成逻辑地址(虚拟地址),该地址由页号和页内偏移量组成。

  2. 内存管理单元(MMU)负责截获此逻辑地址

  3. MMU首先查询快表(TLB),若找到缓存的页表项(即TLB命中)则直接获取物理页框号;若未命中,则需查询内存中的页表以获取对应的物理页框号,并更新TLB。

  4. 获取物理页框号后,将其与逻辑地址中的页内偏移量组合形成物理地址,计算公式为:物理地址 = 物理页框号 × 页面大小 + 页内偏移量。最终,使用该物理地址访问实际内存单元。

假设:

  • 页面大小为 1024 字节。

  • 页表如下(记录了逻辑页号与物理块号的映射关系):

    逻辑页号 (p) 物理块号 (f)
    0 2
    1 5
    2 8

现在需要将逻辑地址 2500 转换为物理地址。

转换过程如下:

  1. 计算页号和页内偏移
    页号 (p) = 逻辑地址 / 页面大小 = 2500 / 1024 ≈ 2.44 → ​​取整数部分,得到页号 2​
    页内偏移 (d) = 逻辑地址 % 页面大小 = 2500 % 1024 = ​​452​​ (这里的 % 是取余运算)
  2. 查页表,获取物理块号
    根据计算得到的​​逻辑页号 p = 2​​,查找页表,找到其对应的​​物理块号 f = 8​​。
  3. 组合物理地址
    物理地址 = (物理块号 × 页面大小) + 页内偏移 = (8 × 1024) + 452 = 8192 + 452 = ​​8644​
    所以,逻辑地址 2500 对应的物理地址是 ​​8644​​。

动态分区分配算法

  • 首次适应算法(First Fit, FF)
    • 低地址开始顺序查找空闲分区链(表),找到第一个能满足大小的空闲分区即进行分配。
  • 临近适应算法(Next Fit, NF)
    • 上次分配的位置之后开始顺序查找,找到第一个能满足要求的空闲分区。
  • 最佳适应算法(Best Fit, BF)
    • 空闲分区按容量从小到大排序,分配时找到能满足要求的最小空闲分区,以减少浪费。
  • 最坏适应算法(Worst Fit, WF)
    • 空闲分区按容量从大到小排序,分配时总是选择最大的空闲分区进行分割。

磁盘调度算法

  • 先来先服务 (FCFS) 严格按照请求到达的先后顺序进行调度。
  • 最短寻道时间优先 (SSTF) 优先选择距当前磁头所在磁道距离最近的磁道进行访问,以使每次的寻找时间最短。
  • 扫描算法 (SCAN) 在磁头当前移动方向上选择与当前磁头所在磁道距离最近的请求作为下一次服务的对象(也称电梯算法)。
  • 循环扫描算法 (CSCAN) 在SCAN算法基础上规定磁头单向移动来提供服务,回返时直接快速移动至起始端而不服务任何请求。

页面置换算法

  • 最佳置换算法(OPT, Optimal) 淘汰在未来最长时间内不会被访问的页面。这是理论上的最优算法,但无法实际实现(需要预知未来的页面访问序列),主要用于评估其他算法的性能。
  • 先进先出置换算法(FIFO, First-In First-Out) 淘汰最早进入内存的页面。实现简单,但可能会产生Belady异常(即分配更多物理块时缺页率反而升高)。
  • 最近最久未使用置换算法(LRU, Least Recently Used) 淘汰最长时间没有被访问的页面。性能接近OPT算法,能有效利用程序运行的局部性原理,但实现开销较大(需记录或维护页面访问时间或顺序)。
  • 时钟置换算法(CLOCK) 一种LRU的近似算法。通过一个循环队列和使用位(Use Bit) 来模拟页面访问历史。检查页面时,若使用位为1则置0并留下;若为0则淘汰。是实际系统中常用的折中方案。
  • 改进型时钟置换算法(Enhanced CLOCK) 在简单时钟算法基础上,额外考虑修改位(Modify Bit)。优先淘汰既未使用又未修改的页面,以减少将修改过的页面写回磁盘的I/O开销。

fork、exec、wait函数

fork()

  • 创建一个子进程
  • 子进程拷贝父进程的 页表(写时拷贝,COW),看似共享同一份内存。

exec()

  • 在子进程中调用,用新的程序(如 elf 文件)替换当前进程的代码和数据
  • 从此子进程运行新的程序,不再执行原来的代码。(分离父进程和子进程)

wait()

  • 父进程调用 wait/waitpid 后会 阻塞,直到子进程结束或状态改变。
  • 用于回收子进程资源,避免僵尸进程。

父进程通过fork函数创建一个子进程,此时这个子进程拷贝了父进程的页表,两个进程都读同一个内存,exec函数可以加载一个elf文件去替换父进程,从此子进程就可以运行不同的程序,父进程wait函数之后会阻塞,直到子进程状态发生改变


fork 和 vfork 的区别

  • fork:子进程复制父进程地址空间(写时拷贝),父子进程几乎独立
  • vfork:子进程与父进程共享地址空间,直到执行 execexit。效率更高,但风险更大。

当 for 循环遇到 fork 函数(了解)

1
2
3
for (int i = 0; i < 3; i++) {
fork();
}
  • 每次 fork 都会创建新进程,最终进程数为 2^3 = 8

读写锁、自旋锁和互斥锁的区别

  • 互斥锁 (mutex):一个任务独占,阻塞等待。
  • 自旋锁 (spinlock):忙等,不释放 CPU,适合临界区很短的情况。
  • 读写锁 (rwlock):允许多个读者,但写者独占,读多写少时效率高。

什么是原子操作?

  • 不可分割的操作,执行过程不会被中断。
  • 特点要么完全执行,要么完全不执行
  • 典型例子:加减计数、位操作。
  • 实现方式:关中断、总线锁、CPU 提供的原子指令。

原子性问题,可见性问题,有序性问题

原子性问题

  • 操作不可分割,中途不能被打断。

可见性问题

  • 一个线程对共享变量的修改,其他线程不能立即看到。

有序性问题

  • 程序执行顺序与代码顺序不一致。

局部性原理是什么

主要包括时间局部性空间局部性

  • 时间局部性:如果执行了某个指令,那么不久后这条指令很有可能再被执行
  • 空间局部性:如果访问了某个内存,那么不久后这个内存或其附近的内存很有可能再被访问

什么是死锁

  • 定义:多个进程/线程在执行过程中,因争夺资源而相互等待,导致无法继续推进。
  • 产生条件(死锁四要素)
    • 互斥:资源一次只能被一个进程占用。
    • 请求保持:进程已持有资源,同时又提出新的资源请求。
    • 不可剥夺:资源只能由占有它的进程主动释放。
    • 循环等待:多个进程形成环形等待链。

预防方法

  • 资源一次性分配:避免请求保持。

  • 可剥夺资源:当请求未满足时,主动释放已有资源。

  • 资源有序分配:给资源编号,进程按顺序申请,按逆序释放,避免环路等待。


Linux

什么是系统调用

  • 用户态进程向内核请求服务的接口。
  • 通过软中断或陷阱指令进入内核。

Linux 驱动的三大类型

  • 字符设备驱动:顺序访问,按字节流操作,如串口。
  • 块设备驱动:以数据块为单位,如硬盘。
  • 网络设备驱动:面向数据包,如网卡。

为什么分用户态和内核态

核心目的是保护操作系统内核的安全与稳定

用户态普通应用程序提供受限的执行环境,使其无法直接访问硬件和关键系统数据。

内核态则赋予操作系统核心最高权限,以管理硬件、内存等所有关键资源。

这种隔离确保了即使应用程序出错或恶意,也不会导致整个系统崩溃。应用程序需通过系统调用这一“大门”来请求内核提供服务,从而实现了受控且安全的资源访问。


什么是交叉编译

  • 一个平台 上编译生成 另一个平台 可运行的程序。
  • 常见于嵌入式开发(如在 PC 上编译 ARM 设备的程序)。

Linux 和 RTOS 的区别

  • Linux:通用操作系统,功能强大,调度非实时。
  • RTOS:实时操作系统,保证任务实时性,资源占用小。

Linux 内核由哪些构成?

  • 进程管理
  • 内存管理
  • 虚拟文件系统
  • 网络接口
  • 中断进程间通信

Linux 系统组成部分

  • 内核(Kernel):管理硬件资源(CPU、内存、设备、进程调度等)。
  • Shell:命令解释器,用户与内核交互的接口。
  • 文件系统(File System):统一管理数据存储与访问。
  • 应用程序(Applications):运行在用户空间的软件。

什么是根文件系统

  • Linux 系统启动后挂载的第一个文件系统 /
  • 包含:驱动、库文件、系统命令、配置文件.

什么是临界区

  • 多个线程/进程访问共享资源的代码区域
  • 要求:在同一时刻只允许一个执行,通常用锁保护。

什么是设备树

  • 定义:一种 树状的数据结构,用文本/二进制文件(.dts / .dtb)描述硬件资源(CPU、内存、外设、中断等)。
  • 作用
    • 把硬件信息从内核代码中抽离 → 内核与硬件解耦
    • 让同一个内核可以适配不同硬件平台,只需更换设备树文件,而无需重新编译内核

什么是IO复用

I/O复用(I/O Multiplexing)是一种单线程或单进程同时监听多个I/O事件的机制。

它允许程序将多个文件描述符(如套接字)注册到内核,并由内核监控这些描述符的状态。当其中任意一个描述符准备好进行读写或出现异常时,内核会通知应用程序进行处理。

其核心价值在于用单线程管理大量连接,避免了为每个I/O操作创建独立线程或进程的巨大资源开销和上下文切换成本,从而显著提升高并发场景下的性能和资源利用率。常见的实现有 select、poll 和 epoll。


select / poll / epoll 的区别与用处

  • select
    • 位图 保存 fd,最大支持数有限(通常 1024)。
    • 每次调用都要 重新设置 fd 集合
    • 内核返回后还需遍历所有 fd,效率低。
    • 适合 少量连接、简单场景。
  • poll
    • 数组(链表形式) 保存 fd,没有数量限制
    • 每次调用仍需遍历所有 fd,效率依旧不高。
    • 比 select 灵活,但高并发时性能差。
  • epoll
    • 内核用 红黑树管理 fd,就绪 fd 放在 就绪队列
    • 用户只需处理“就绪的 fd”,无需遍历全部。
    • 支持 LT(水平触发)ET(边缘触发) 两种模式。
    • 大规模并发连接(如服务器)场景下性能最好。
  • fd(File Descriptor,文件描述符): 就是“内核给打开的文件/资源的编号”,程序靠它操作资源。

  • 位图(Bitmap):用一组 二进制位(bit) 来表示某些状态或资源是否存在/可用。


为什么 Linux 的中断分为上半部分和下半部分?

  • 上半部分快速处理中断(响应快),只做必要操作。
  • 下半部分延迟处理耗时任务(软中断、tasklet、工作队列)。
  • 原因:缩短中断屏蔽时间,提高系统实时性。

硬中断 vs 软中断

  • 硬中断(Hardware Interrupt)
    • 来源:外部硬件设备(键盘、网卡、磁盘等)。
    • 特点:异步发生,由硬件信号触发,CPU 立即响应。
    • 用途:处理 I/O 请求、定时器等。
  • 软中断(Software Interrupt)
    • 来源:软件指令触发(如 int 指令、系统调用)。
    • 特点:同步发生,由程序主动触发,用于进入内核。
    • 用途:系统调用、调试、异常处理。

中断上下文和进程上下文的区别

  • 进程上下文:任务在用户态或内核态下运行,可以休眠/调度
  • 中断上下文:响应硬件中断时执行,不能休眠,不能调用阻塞函数

Linux常见指令

【Linux】Linux常用命令60条(含完整命令语句)_linux必学的60个命令-CSDN博客


STM32

STM32学习笔记里记录地很详细,推荐看那里的,这里是引申。

ARM 中的寄存器,都有什么用

  • 通用寄存器 (R0–R12)

    • 临时存储、函数参数、返回值。
    • 约定:
      • R0–R3:函数参数 & 返回值
      • R4–R11:通常作为被调用者保存
      • R12 (IP):暂存寄存器

    SP (Stack Pointer, 栈指针)

    • 指向当前栈顶。
    • 分为:
      • MSP (Main SP):异常/中断、启动时使用
      • PSP (Process SP):线程模式任务栈(RTOS 常用)

    LR (Link Register, 链接寄存器)

    • 保存函数返回地址。
    • 在异常返回时,LR 含特殊值(如 EXC_RETURN),决定返回到线程/处理模式和使用 MSP/PSP。

    PC (Program Counter, 程序计数器)

    • 指向将要执行的下一条指令。
    • ARM 架构中一般是“取值 = 当前指令地址 + 偏移”。

    xPSR (Program Status Register, 程序状态寄存器)

    • 包含条件标志位 (N, Z, C, V),执行状态,当前异常号等信息。

大小端是什么

  • 大端字节存储在地址。

  • 小端字节存储在地址。

例子int a = 0x12345678;默认LSB优先

  • 大端存储:12 34 56 78
  • 小端存储:78 56 34 12

Cortex-M3 和 Cortex-M4 的区别

同时支持大端/小端模式

  • Cortex-M3:支持基本的 ARMv7-M 架构,32 位 RISC 内核,适合通用控制。
  • Cortex-M4:在 M3 基础上增加 DSP 指令集单精度浮点运算 (FPU),适合信号处理。

M4 = M3 + DSP + 可选FPU


STM32的Flash和RAM占用

  • Flash存放代码段(.text)+常量数据(.rodata)
  • RAM存放全局变量(.bss/.data)+栈(stack)+堆(heap)

GPIO 工作模式

  • 输入模式:上拉输入、下拉输入、浮空输入、模拟输入
  • 输出模式:推挽输出、开漏输出
  • 复用功能:外设复用(USART、SPI 等)

什么是 IO 口悬空,可能带来哪些问题

  • IO 悬空:输入引脚未接电路。(浮空输入)
  • 问题:电平不确定,可能产生抖动、功耗增加、误触发。
  • 解决:加上拉或下拉电阻。

中断栈和任务栈有什么区别,保存在哪里

任务栈

  • 每个任务都有独立栈,用来保存任务自己的局部变量、函数调用现场、寄存器
  • 由 RTOS 在创建任务时分配,通常放在 SRAM

中断栈

  • 中断发生时,CPU 自动把部分寄存器(PC、xPSR、LR 等)压入当前正在使用的栈
  • 裸机:使用主栈指针 MSP 作为中断栈。
  • RTOS(如 FreeRTOS):通常把中断都跑在 MSP 上,而任务各自用 PSP

PWM的边沿对齐和中心对齐

  • 边沿对齐模式(Edge-Aligned)

    • 计数器单向计数(通常从0递增到最大值ARR,然后复位)
    • 开关动作(电平跳变)集中在每个PWM周期的开始或结束时刻
  • 中心对齐模式(Center-Aligned)

    • 计数器双向计数(从0递增到最大值ARR,再递减回0)

    • 开关动作(电平跳变)发生在每个PWM周期的中间阶段(峰值和谷值附近)

应用场景

  • 中心对齐模式:特别适合对电磁兼容性(EMC)和效率要求高的场合。
    • 电机驱动(如FOC矢量控制)、高频数字电源(如LLC谐振变换器)、D类音频功放
  • 边沿对齐模式:适用于对噪声和效率不敏感的简单控制场景。
    • LED调光简单直流电机调速蜂鸣器驱动

怎么理解AD采样里的通道和转换精度

AD采样中的通道(Channel)指的是ADC能同时或分时采集的模拟信号输入路径数量。它决定了你能同时采集多少路不同的信号,比如多个传感器的数据。

转换精度(Resolution)则是指ADC对单路模拟信号进行数字化时所能达到的精细程度,通常用位数(如8位、12位、16位)表示。位数越高,ADC能够区分的电压等级就越多,将模拟电压值转换为数字量时也就越精确,量化误差越小。

通道数关乎“能采几个信号”,转换精度关乎“采得有多准”

AD转换精度受到分辨率、参考电压、时钟抖动等影响。

AD的基本流程:采样—保持—量化—编码


为什么 I2C 需要开漏输出 + 上拉电阻

  • 开漏输出:只能拉低电平,不能输出高电平。
  • 上拉电阻:总线空闲时保持高电平,实现线与。
  • 原因:多个设备共享总线,避免总线冲突。

I2C总线挂死,如何排查

  1. 硬件排查:首先用示波器或逻辑分析仪查看SCL和SDA波形。若发现任一线被持续拉低(通常是SDA),则逐个断开从设备,定位故障源。同时检查电源稳定性上拉电阻值(常用4.7kΩ)。
  2. 软件恢复:在代码中添加超时检测机制(例如等待ACK或总线释放时,设置100ms超时)。一旦超时,触发恢复序列:手动生成9-16个SCL时钟脉冲(通过GPIO模拟),强制从设备释放SDA线。

I2C上拉电阻阻值

  • 标准模式(100kbit/s)4.7kΩ~10kΩ
  • 快速模式(400kbit/s)2.2kΩ~4.7kΩ

I2C通信出现低电平0.4V,高电平2.6V等中间电平

  1. 总线争抢:主机与从机同时试图控制SDA线(推挽输出模式争抢),或电平转换芯片方向控制切换不及时。
  2. 上拉电阻过大或电源异常:上拉电阻过大导致高电平无法在规定时间内被拉至VDD,或电源电压本身异常。
  3. 端口配置错误:MCU模拟I2C时,SDA引脚错误配置为推挽输出模式而非开漏输出。在推挽模式下,若主机输出高电平而从机试图拉低,会产生中间电平。

怎么判断数据传输到目标后,数据没有丢失

  • ACK和CRC只能判断本帧数据没有丢失、没有错误;不能保障传输过程中其他帧没有丢失。
  • 加入包头机制,来进行判断;如果丢失,就做出重传操作,类似TCP。

SPI 的工作模式有哪些

  • CPOL(时钟极性)CPHA(时钟相位) 决定,共 4 种模式

    CPOL(Clock Polarity,时钟极性)

    • =0:时钟空闲时为 低电平
    • =1:时钟空闲时为 高电平

    CPHA(Clock Phase,相位)

    • =0:第 1 个边沿采样(第一个有效边沿采样数据)
    • =1:第 2 个边沿采样(先在第一个边沿切换数据,第二个边沿采样)
  • 模式 0:CPOL=0, CPHA=0

  • 模式 1:CPOL=0, CPHA=1

  • 模式 2:CPOL=1, CPHA=0

  • 模式 3:CPOL=1, CPHA=1


SPI往屏上刷一个像素点,如何操作

  1. 初始化SPI总线和设备
  2. 设置地址窗口到(x, y)
  3. 发送像素颜色(RGB565两字节,高在前)

RS232 和 RS485 的区别

  • RS232
    • 单端信号点对点通信
    • 距离短
    • 抗干扰弱
    • 逻辑“1”≈ −3V ~ −15V,逻辑“0”≈ +3V ~ +15V
  • RS485
    • 差分信号,支持多点总线
    • 距离远
    • 抗干扰强
    • A、B 线电压差 +2V~+6V 表示逻辑 1,−2V~−6V 表示逻辑 0

STM32 上电后到 __main 的过程

  • 硬件复位

    • 上电/复位后,Cortex-M 内核自动完成:
      • 取向量表第 0 项 → Initial_SP,装入 MSP。
      • 取向量表第 1 项 → Reset_Handler 地址,跳转执行。
  • Reset_Handler(启动文件 startup_xx.s 中定义)

    • 设置堆栈指针(MSP)、初始化中断向量表基址。

    • 调用 SystemInit()

      • 配置系统时钟(HSE/PLL 等)。
      • 配置 Flash 访问延时、总线分频。
      • 初始化外设时钟(FPU、Cache、MPU 等)。
  • 有的厂商库里叫 HardwareInit() / SystemClock_Config(),作用类似。
  • C 运行时环境初始化

    • 数据段初始化:将已初始化全局变量从 Flash 拷贝到 RAM。

    • BSS 段清零:未初始化的全局/静态变量置 0。

    • 可能还会初始化堆区指针。

  • 调用 __main(C 库入口函数)

    • 进一步完成 C/C++ 环境初始化:

      • 运行全局/静态对象的构造函数(C++)。
      • 设置标准库需要的运行环境。
    • 最终调用 __main()


MSP 和 PSP 两个指针是什么,都在什么时候用

  • MSP (Main Stack Pointer):主堆栈指针,复位后默认使用,主要用于中断和异常。
  • PSP (Process Stack Pointer):进程堆栈指针,通常用于线程/任务。
  • FreeRTOS:任务使用 PSP,内核/异常使用 MSP。

中断能有返回值和参数吗?

  • 不能:中断服务函数由硬件调用,没有返回值和参数。
  • 传参方法:使用全局变量、队列、消息通知等方式。

RAM、ROM 和 Flash 的区别

  • RAM(随机存取存储器):读写速度快,断电数据丢失。
  • ROM(只读存储器):出厂时写入,通常不可修改。
  • Flash:非易失存储,可擦写,常用于固件和数据存储。

NOR Flash读取单位是字节,NAND Flash读取单位是页


Cache 是什么,Cache 一致性又是什么

  • Cache:CPU 与内存之间的高速缓存,加快数据访问速度,它既能存指令,也能存数据
  • Cache 一致性:多核 CPU 或 DMA 等访问共享数据时,确保各个缓存的数据与主存一致。
    • 解决方式:MESI协议

什么是 SMP(了解)

  • SMP (Symmetric Multi-Processing,对称多处理):多个 CPU 核心共享同一内存和 I/O,运行同一个操作系统。
  • 特点:负载均衡、可并行执行任务,常用于多核处理器系统。

为什么单片机不使用 malloc 函数

  • 资源限制

    • 单片机 RAM 很小(几 KB ~ 几十 KB),动态分配容易造成内存碎片
  • 实时性要求

    • malloc/free 的执行时间 不可预测,会破坏实时性。
  • 可靠性问题

    • 容易导致内存泄漏,调试困难,系统长期运行不稳定

什么是中断嵌套

在处理中断 A 时,允许更高优先级的中断 B 打断执行。

  • 优点:提高实时性。
  • 缺点:过多嵌套可能导致栈溢出

如何处理 Flash 擦写寿命问题

  • 磨损均衡 (Wear Leveling):均匀分布写入次数。
  • 文件系统优化:使用专门的嵌入式文件系统(如 FATFS + WL)。
  • 缓存/批量写入:减少频繁擦写。

CAN 通信的缺点

  • 速率较低(典型 1Mbps)。
  • 帧长度有限(8字节数据)。
  • 总线型拓扑,节点过多易导致仲裁延迟。
  • 无内置安全机制,易受干扰。

FreeRTOS

FreeRTOS 中使用的调度算法

  • 基于优先级的抢占式调度

  • 相同优先级下,采用 时间片轮转

  • 协作式调度,任务不会被强制打断,只有当任务主动调用、阻塞或结束时,才会切换到其他就绪任务


FreeRTOS 的时间片的大小

  • 时间片由 SysTick 定时器中断周期决定。

FreeRTOS 中任务的状态

  • 就绪态(Ready)
  • 运行态(Running)
  • 阻塞态(Blocked)(等待事件或超时)
  • 挂起态(Suspended)(不可被调度,需手动恢复)
  • 终止态(Deleted)

FreeRTOS 创建任务的方法和区别

  • xTaskCreate:普通创建任务。
  • xTaskCreateStatic:静态创建,用户提供栈和 TCB。
  • 区别:静态方式更安全,避免动态内存分配。

FreeRTOS 的空闲任务以及它的作用

  • 空闲任务 (Idle Task):系统启动后自动创建。
  • 默认优先级最低,永远存在。
  • 作用
    • 回收已删除任务的资源。
    • 执行用户定义的钩子函数(如省电模式)。

FreeRTOS 如何实现任务同步

  • 信号量:用于任务间同步(包括二值信号量、计数信号量)。
  • 互斥量:解决资源互斥。
  • 队列:任务间数据传递。
  • 事件组:多任务事件同步

FreeRTOS 中的 SVC、SysTick 和 PendSV 的作用和区别

  • SVC (Supervisor Call):系统调用,进入内核模式。
    • 系统启动时用 SVC_Handler 进入第一个任务(第一次上下文切换)。
    • 相当于“进入内核”的入口。
  • SysTick:系统定时器中断,触发任务调度,给操作系统提供 时间基准 (tick)
    • 周期性触发 SysTick_Handler
    • 内核更新系统时钟(xTickCount)。
  • PendSV最低优先级中断,用于任务上下文切换
    • 因优先级最低,确保不会打断其他更重要的中断。
    • 保存当前任务寄存器(上下文),切换栈指针(PSP),恢复下一个任务的上下文。

要实现任务调度,可以只有systick中断吗

理论上可以:

  • SysTick 定时中断里直接做任务切换(保存现场、恢复下一个任务)。

但这样会导致:

  • 调度逻辑绑死在 SysTick,不灵活。
  • 若有其它触发(信号量、事件、外设中断唤醒任务),也必须塞进 SysTick,复杂度高。

FreeRTOS 为什么要用 PendSV

  • PendSV 可设为最低优先级,确保任务切换不会打断其他中断处理
  • 这样 上下文切换延迟最小,效率最高

FreeRTOS 中的任务控制块是什么

  • 任务控制块 (TCB, Task Control Block):保存任务的所有信息。
  • 主要内容
    • 栈指针(pxTopOfStack)
    • 任务优先级(uxPriority)
    • 任务状态(xStateListItem)
    • 延时/阻塞时间(pxStack)
    • 任务名(pcTaskName)

FreeRTOS 如何实现任务切换,过程是什么

触发条件

  • 周期性 SysTick 中断(时间片/延时到期)。
  • 任务调用阻塞函数(如 vTaskDelayxQueueReceive)。
  • 高优先级任务进入就绪态。

切换机制

  1. 触发调度请求:SysTick 或 taskYIELD() 设置 PendSV 异常挂起。
  2. 保存上下文PendSV_Handler 将当前任务的 CPU 寄存器压入其任务栈(PSP)。
  3. 调度器决策:根据优先级选择下一个要运行的任务。
  4. 恢复上下文:从新任务栈中弹出寄存器,恢复执行环境。
  5. 返回任务:CPU 跳转到新任务的断点继续运行。

FreeRTOS 有哪两种内存分配方式,有哪五种分配算法

  • 方式
    • 静态分配:用户提供内存(更安全)。
    • 动态分配:系统在堆中申请。
  • 算法(heap_1 ~ heap_5)
    • heap_1:最简单,允许释放无内存碎片
    • heap_2:允许释放但可能产生碎片
    • heap_3:直接调用 malloc/free
    • heap_4:合并相邻空闲块,减少碎片。
    • heap_5:支持多个非连续内存区域碎片合并

为什么在 FreeRTOS 中信号量、队列等设计了两套函数

  • FreeRTOS 提供 带阻塞时间不带阻塞时间 两套接口。
  • 原因
    • 带阻塞时间:任务可在等待期间挂起,避免忙等。
    • 不带阻塞时间:适合中断服务例程 (ISR),因为中断中不能阻塞。

vTaskDelay 和 vTaskDelayUntil 的区别

  • vTaskDelay:延时相对当前时间,延迟N个SystemTick(会累积误差)。
  • vTaskDelayUntil:延时基于绝对时间(适合周期性任务,抖动小)。

信号量和互斥量的区别

信号量包含二元信号量和计数信号量;二元信号量包括0和1,计数信号量就是个计数器。

信号量所有任务都可以操作(获取或释放)的计数器,没有所有权概念。互斥量是具有所有权概念的锁,必须由获取它的任务亲自释放


硬件电路

STM32最小单元板构成

  • MCU
  • 时钟电路
  • 复位电路
  • 电源电路
  • 启动电路
  • 外设接口

LDO稳压器(了解)

定义:LDO(Low Dropout Regulator)是一种 低压差线性稳压器,只能 降压,即输出电压 < 输入电压。

  • 比较 取样的输出电压基准电压。根据差值调节功率管。

  • 优点电路简单,噪声低,纹波小,适合模拟/射频电路供电。

  • 缺点效率低(受输入/输出电压差限制),只能降压,不能升压。


单片机明明3.3V和5V就够用了,为什么还需要12V以上的供电

  • 满足大功率负载的需求,拥有更强的电压驱动能力,比如电机;
  • 实现强弱电隔离,保证系统稳定性

数据结构

常见的排序算法

  • 冒泡排序: 相邻元素两两比较,大的往后移,循环多次把最大值“冒”到最后。

  • 插入排序: 前面保持有序序列,取下一个元素,按大小插入到合适位置。

  • 选择排序: 每一轮从未排序区选择最小(或最大)的,与当前位置元素交换。

  • 快速排序: 取一个基准值,把小于的放左边,大于的放右边,然后递归分区排序。

  • 归并排序: 不断二分数组,递归到底后再逐层合并两个有序序列(需辅助数组)。

  • 基数排序: 按数位(个位、十位、百位…)依次排序,常用稳定排序作为子过程。

  • 希尔排序: 基于插入排序,先按较大间隔分组排序,再逐渐缩小间隔至 1。

  • 堆排序: 利用大顶堆/小顶堆,每次取堆顶元素放到末尾,再调整堆结构。

  • 计数排序: 统计每个元素出现次数,通过计数下标映射回输出(需额外数组)。

  • 桶排序: 按数值范围映射到不同桶内,每个桶独立排序,最后合并所有桶。


二叉树通过中序遍历和后序遍历,判断前序遍历

前序遍历:中左右;中序遍历:左中右;后序遍历:左右中。

例子

  • 中序:D B E A F C
  • 后序:D E B F C A

推导过程

  1. 后序末尾 = A → 整棵树根。
    • 在中序里,A 左边是 D B E,右边是 F C
  2. 左子树 = D B E
    • 在对应后序子序列 D E B,末尾 B 是根。
    • 在中序 D B E 中,B 左边是 D,右边是 E
    • → 左子树前序 = B D E
  3. 右子树 = F C
    • 在对应后序子序列 F C,末尾 C 是根。
    • 在中序 F C 中,C 左边是 F,右边为空。
    • → 右子树前序 = C F
  4. 拼接前序
    • A + 左子树 B D E + 右子树 C F
    • 前序 = A B D E C F

后序定根,中序分左右,递归拼前序


什么是搜索二叉树

  1. 左子树所有节点的值 小于 根节点值;

  2. 右子树所有节点的值 大于 根节点值;

中序遍历结果是 递增序列

什么是平衡二叉树

  • 一种二叉搜索树,任意节点的左右子树高度差不超过 1
  • 作用:保持查找、插入、删除的时间复杂度 O(log n)。

典型实现:AVL 树、红黑树。


二叉树通过中序遍历和后序遍历,判断前序遍历

前序遍历:中左右;中序遍历:左中右;后序遍历:左右中。

例子

  • 中序:D B E A F C
  • 后序:D E B F C A

推导过程

  1. 后序末尾 = A → 整棵树根。
    • 在中序里,A 左边是 D B E,右边是 F C
  2. 左子树 = D B E
    • 在对应后序子序列 D E B,末尾 B 是根。
    • 在中序 D B E 中,B 左边是 D,右边是 E
    • → 左子树前序 = B D E
  3. 右子树 = F C
    • 在对应后序子序列 F C,末尾 C 是根。
    • 在中序 F C 中,C 左边是 F,右边为空。
    • → 右子树前序 = C F
  4. 拼接前序
    • A + 左子树 B D E + 右子树 C F
    • 前序 = A B D E C F

后序定根,中序分左右,递归拼前序


如何用栈模拟队列

使用两个栈(inStackoutStack)模拟队列。所有新元素直接压入 inStack。当需要出队或查看队首时,若 outStack为空,则将 inStack的所有元素依次弹出并压入 outStack,此举将元素顺序反转,使得最早进入的元素位于 outStack栈顶。随后从 outStack弹出或查看栈顶元素即可。队列为空的条件是两个栈均为空。


介绍一下哈希表

哈希表(Hash Table)是一种通过键(Key)直接访问内存存储位置的数据结构,能在平均情况下实现接近常数时间复杂度(O(1))的快速查找、插入和删除操作。

其核心是哈希函数,它将键转换为数组索引。理想情况下,哈希函数应计算快、分布均匀,以减少冲突(不同键映射到同一位置)。

解决冲突的常见方法:

  • 链地址法:数组每个位置连接一个链表,冲突元素依次加入链表。
  • 开放定址法:发生冲突时,按照某种探测方法(如线性探测)在数组中寻找下一个空闲位置。

为了维持高效性能,哈希表通常会在元素数量过多(由负载因子衡量)时进行扩容,即创建一个更大的数组并重新插入所有元素。


反问

只做参考,有些红线不要碰

  1. 薪资构成,绩效占比,绩效考核指标,税前还是税后,发放时间
  2. 有没有五险一金,缴纳基数是多少。
  3. 一年几次调薪机会,调薪幅度是多少,评定标准
  4. 公司的未来发展方向是什么,我会承担什么样的职务
  5. 团队规模多大,各自是怎么分配工作的?
  6. 入职后有没有培训?平时有什么学习机会?
  7. 岗位是否有定期考核要求
  8. 有无试用期,试用期多少时间;试用期工资多少,缴纳五险一金吗
  9. 转正的明确目标是什么
  10. 双休?大小周?每天上班时间,法定节假日正常休息吗
  11. 是否加班,是有加班费
  12. 是否需要打卡,迟到、请假等怎么扣钱
  13. 有没有年假,带薪还是不带薪?
  14. 近期有无搬迁工位计划?
  15. 落户以及人才补贴有哪些?

其他

电脑1G的空间,malloc(1.2G) 为什么可能成功

  • 虚拟内存机制malloc 申请的是虚拟地址空间,不一定立刻分配物理内存
  • 实际情况:只要虚拟内存够(物理内存+交换空间),就能申请成功。

常用的调试方法有什么(了解)

  • 软件调试printf、日志输出、断点调试(gdb)。
  • 硬件调试:示波器、逻辑分析仪、JTAG/SWD。
  • 分析工具:perf、valgrind、strace、Wireshark。

gdb 常见命令(了解)

  • 启动调试
    • gdb ./a.out → 以 gdb 打开可执行文件。
    • run (简写 r) → 开始运行程序。
  • 断点控制
    • break main (简写 b main) → 在 main 函数入口设置断点。
    • break 10 → 在源码第 10 行设置断点。
    • delete → 删除断点。
  • 单步调试
    • next (简写 n) → 单步执行,函数整体当成一步(不进入函数体)。
    • step (简写 s) → 单步执行,遇到函数会进入函数体。
    • finish → 运行到当前函数结束并返回。
  • 程序控制
    • continue (简写 c) → 继续运行,直到下一个断点或程序结束。
    • quit (简写 q) → 退出 gdb。
  • 信息查看
    • print x (简写 p x) → 打印变量 x 的值。
    • bt (backtrace) → 查看调用栈,显示当前函数调用路径。
    • info locals → 查看当前函数的局部变量。
    • list (简写 l) → 查看当前源码附近的代码。

62-63=1,移动一个数字使其成立

2663=1 2^6 - 63 = 1

双缓冲机制(了解)

双缓冲是一种使用两个存储区(通常称为A和B)交替工作的数据传输技术。其核心原理是读写分离:一个缓冲区用于写入数据(生产者操作),另一个缓冲区用于读取数据(消费者操作),通过交替切换避免对同一缓冲区的读写冲突。

这种方式主要为了消除等待时间,使得数据生产和消费能并行操作,从而提高吞吐量,并避免数据竞争。它在图形渲染中能有效防止画面闪烁,在数据采集和实时传输(如音频、视频流)中能确保数据连续性和完整性,减少丢失风险。


说明

随缘更新,有问题跟我讲,有好的问题也可以分享。