1.指针
1.1函数指针
定义:本质是指针 ,返回类型是函数
形式:返回类型 (*指针名)(参数列表);用括号 () 把 * 和名字包起来,才是函数指针
int (*func)(int, int) ;
//func是一个指针,指向"返回int并接受两个int参数的函数"的指针,函数返回int,该函数接受两个int参数。()的优先级高于*
int (*funcPtr)(int, int); // 指向返回int、有两个int参数的函数
void (*callback)(void); // 指向无参无返回值的函数
char* (*strFunc)(char*, char*); // 指向返回char指针、有两个char*参数的函数
易错:
int *func(int, int);
是一个函数声明,返回int*,func是函数名,两个int是参数
(int*) func(int, int);
对 func(int, int) 的结果做 int* 强制类型转换
1.2野指针
野指针是指向位置随机的、不正确的指针。
野指针产生的原因有: 1、创建指针时没有对指针进行初始化,导致指针指向一个随机的位置; 2、释放指针指向的内存后没有置空,从而指向垃圾内存;
1.3数组指针/指针数组
数组指针是一个指针,指向一个数组。指针数组由n个指针类型的数组元素组成。数组指针只是一个指针变量,似乎是C语言里专门用来指向二维数组的,它占有内存中一个指针的存储空间。指针数组是多个指针变量,以数组形式存在内存当中,占有多个指针的存储空间
1.4指针/引用
(1)指针是实体,占用内存空间;引用是别名,与变量共享内存空间。
(2)指针不用初始化或初始化为NULL;引用定义时必须初始化。
(3)指针中途可以修改指向;引用不可以。
(4)指针可以为NULL;引用不能为空。
(5)sizeof(指针)计算的是指针本身的大小;而sizeof(引用)计算的是它引用的对象的大小。
(6)如果返回的是动态分配的内存或对象,必须使用指针,使用引用会产生内存泄漏。
(7)指针使用时需要解引用;引用使用时不需要解引用'*'。
(8)有二级指针;没有二级引用。
2.函数
2.1指针函数
定义:本质是函数 ,返回类型是指针
形式:返回类型 *函数名(参数列表);
int* createArray(int size); // 返回int指针的函数
char* getName(int id); // 返回char指针的函数
float* calculateStats(float data[]); // 返回float指针的函数
2.2内联函数/宏函数
相同点: (1)二者都是通过将函数调用替换成完整的函数体,相比函数调用的时间、空间开销而言,二者提高了效率。 不同点: (1)宏定义不是函数,而内联函数时函数,因此内联函数可以调试,宏定义不能。 (2)宏定义的代码展开阶段是预处理阶段,而内联函数在编译阶段,因此内联 函数有类型安全检查,宏定义没有 (3)内联函数作为类的成员函数时,可以访问类的所有成员(公有、保护、私有),宏定义不能。
3.二进制
| 符号 | 名称 | 作用 | 示例 (a=5=0101, b=3=0011) |
|---|---|---|---|
~ |
按位取反 | 所有位取反 | ~a= 1010 (10) |
& |
按位与 | 同1为1,否则0 | a & b= 0001 (1) |
| |
按位或 | 有1为1,同0为0 | a | b= 0111 (7) |
^ |
按位异或 | 不同为1,相同为0 | a ^ b= 0110 (6) |
<< |
左移 | 左移n位,低位补0 | a << 1= 1010 (10) |
>> |
右移 | 右移n位,高位补0或符号位 | a >> 1= 0010 (2) |
eg.下列语句在32位和64位系统中各自的执行结果是?
int a =0; if((unsigned int)(-a) == -1){ printf("A1"); }else{ printf("A2"); }| 系统 |
int位数 |a = 0的二进制 |~a的结果 |
| 32位 | 32位 |0x00000000|0xFFFFFFFF|
64位 32位 0x000000000xFFFFFFFFint在 32位和64位系统中都是 32 位,不是64位。有符号和无符号混合比较,C 语言会进行整数提升,-1会被先提升为unsigned int,即0xFFFFFFFF=
=4294967295。所以返回A1,A1
4.补码、原码、反码
**原码:**最高位是符号位,0正1负,其余位是数值的绝对值,范围-(2ⁿ⁻¹-1) 到 +(2ⁿ⁻¹-1),有+0和-0两种零,加减法复杂
反码: 正数与原码相同,负数符号位不变,其他位取反,范围-(2ⁿ⁻¹-1) 到 +(2ⁿ⁻¹-1),仍有+0和-0
补码: 正数与原码相同,负数反码 + 1 ,范围-2ⁿ⁻¹ 到 +(2ⁿ⁻¹-1),只有一种零,加减法统一
5.RISC(精简指令集计算机)/与 CISC(复杂指令集计算机)
5.1RISC 与 CISC 的区别:
指令集 :RISC 简单规整(如 LOAD/STORE架构);CISC 复杂丰富(如 x86 有复杂内存操作指令)。
执行方式 :RISC 多为单周期指令,依赖流水线;CISC 复杂指令需多周期,早期采用微程序控制。
寄存器 :RISC 寄存器多,操作尽量在寄存器间进行;CISC 寄存器相对较少。
控制单元 :RISC 硬连线控制为主,速度快;CISC 微码控制占比高。
代码密度 :RISC 代码膨胀(需更多指令),CISC 代码紧凑。
5.2RISC为何"精简"反而高效?
简化指令使流水线更容易实现,减少CPI(每条指令周期数),通过编译器优化来调度指令,将复杂性从硬件转移到了软件。
6.ARM 架构特性
架构版本 :ARMv7(A32/T32 指令集,32位)、ARMv8(64 位)
工作模式 :User、FIQ、IRQ、SVC、Abort 等 7 种模式,考察模式切换场景(如中断、系统调用)。
寄存器组织 :37 个寄存器(ARMv7),包括通用寄存器(R0-R15)、程序计数器(PC)、链接寄存器(LR)、堆栈指针(SP)、程序状态寄存器(CPSR/SPSR)。
指令集特点 :条件执行 :几乎所有指令都可条件执行(如 ADDEQ R0, R1, R2),提高代码密度。移位操作 :可与 ALU 操作结合(如 ADD R0, R1, R2, LSL #2)。多寄存器加载/存储 :LDM/STM指令,用于现场保护和恢复,非常高效。
ARM 流水线 :经典 3 级(取指、译码、执行)或 5 级(增加存储、回写)流水线。流水线冒险 :重点考察数据冒险 (需前递/旁路 Forwarding 解决)和控制冒险 (分支预测、分支延迟槽)。
异常与中断处理流程**** **:**异常发生 → 保存 CPSR 到 SPSR_mode → 保存返回地址到 LR_mode → 切换模式 → 跳转到异常向量表。
返回方式 :MOVS PC, LR或 SUBS PC, LR, #4(注意 S后缀表示同时恢复 CPSR)。
寻址方式 :基址寻址、基址变址寻址(前变址、后变址、回写)。
大小端 :ARM 支持大端和小端格式,需明确配置。一般只默认小端。
7.软件复用
**优势:**缩短开发周期;降低开发成本;降低技术风险;提高软件质量/可靠性;促进标准化 ;
不能提升软件性能!
8.TCP/IP
8.1层级协议
| 层级 | 协议 | 功能 |
|---|---|---|
| 应用层 | HTTP、FTP、SMTP、DNS | 面向用户的应用程序 |
| 传输层 | TCP、UDP | 端到端的可靠/不可靠数据传输 |
| 网络层 | IP、ICMP、ARP | 寻址、路由、数据包转发 |
| 链路层 | Ethernet、WiFi、PPP | 物理网络传输 |
| 特性 | HTTP | HTTPS |
|---|---|---|
| 安全性 | 明文传输,不安全 | 加密传输,安全 |
| 端口 | 80 | 443 |
| 证书 | 不需要 | 需要 SSL/TLS 证书(由 CA 颁发) |
| 加密层 | 无 | TLS/SSL |
| URL 前缀 | http:// |
https:// |
8.2TCP 滑动窗口流量控制流程
【1】初始建立连接
三次握手时,双方交换各自的接收窗口大小
【2】数据传输阶段
发送方 ─》数据段 ─》接收方
《---ACK + 窗口大小──
发送方根据返回的 rwnd 调整发送窗口:可用窗口 = min(拥塞窗口, 接收窗口) - 已发送未确认
【3】窗口动态调整
接收方缓冲区满时:rwnd = 0 → 发送方停止发送
接收方处理后:rwnd 增大 → 发送方恢复发送
【4】零窗口探测
当 rwnd = 0 时,发送方启动持续计时器,超时后发送 **窗口探测报文**(1 字节数据),防止接收方恢复后通知报文丢失导致死锁
关键问题处理:
|------------------------------------------------------------------------------------------|----------------------------------|
| 场景 | 处理方式 |
| 接收方缓存不足 | 减小 rwnd,发送方降速 |
| 接收方处理不过来 | rwnd → 0,发送方暂停 |
| 零窗口恢复通知丢失 | 持续计时器 + 窗口探测报文 |
| **糊涂窗口综合征(SWS):**是 TCP 协议中一种典型的低效传输病理。它指的是通信双方陷入"发送极少量有效数据,却消耗完整 TCP/IP 包头"的高开销、低吞吐恶性循环。 | 接收方不通知小窗口,发送方避免发送小数据(Nagle 算法配合) |
| 流量控制 | 拥塞控制 | |
|---|---|---|
| 目的 | 防止发送方压垮接收方 | 防止发送方压垮网络 |
| 依据 | 接收窗口 rwnd | 拥塞窗口 cwnd(通过 RTT、丢包判断) |
| 实现 | 接收方主动通知 | 发送方被动探测 |
| 关系 | 实际发送窗口 = min(rwnd, cwnd) |
9.语言执行流程
高级语言(C/C++) → 编译器 → 汇编语言 →
↓
用户 ←------ 运行结果 ←------ CPU执行 ←------ 机器语言 ←------ 汇编器
↑
硬件描述语言(HDL) → 综合工具 → 电路网表 → 制造芯片
10.linux
10.1各类线程池
| 类型 | 适用场景 | 特点 |
|---|---|---|
| CPU密集型 | 计算密集型任务:编解码、加密解密、图像处理、科学计算 | 线程数 ≈ CPU 核心数,充分利用 CPU 算力 |
| I/O密集型 | 网络请求、文件读写、数据库操作 | 线程数可以较多(2×CPU核心数以上),因为大量时间等待 I/O |
| 内存消耗型 | 大数据处理、缓存操作 | 控制线程数防止 OOM |
**软解码:**是指完全用 CPU 进行视频/音频解码,不依赖 GPU 或专用硬件解码器。
**硬解码:**GPU/专用芯片(如 NPU、VPU),是I/O 密集型(只需把数据传给硬件)。
软解码流程:
读取数据(I/O)→ 熵解码(CPU)→ 反量化(CPU)→ IDCT变换(CPU) → 预测重建 (CPU)→运动补偿 (CPU)→色彩转换(CPU) →输出帧
10.2设备分类
| 设备类型 | 特点 | 典型例子 |
|---|---|---|
| 字符设备(Character Device) | 按字节流顺序访问,不可随机读写 | 串口、键盘、鼠标、终端(tty)、打印机、声卡 |
| 块设备(Block Device) | 按块(固定大小)随机读写,有缓存 | 磁盘、SSD、SD卡、U盘 |
| 网络设备(Network Device) | 特殊类型,通过 socket 接口访问 | 网卡、WiFi |
10.3PID/TID
| 维度 | PID (Process ID) | TID (Thread ID) |
|---|---|---|
| 作用域 | 进程 的唯一标识 | 线程 的唯一标识 |
getpid()返回值 |
进程的 PID | 返回的是主线程的 TID(数值上等于 PID) |
gettid()返回值 |
系统调用,返回当前线程的真实 TID | 线程的真实 TID(内核调度实体 ID) |
| 内核视角 | 早期是调度实体,现代 Linux 中线程才是实体 | 真正的调度实体,线程拥有独立的 PID(在内核中) |
| 唯一性 | 系统范围内唯一(每个进程一个) | 系统范围内唯一(与 PID 共用命名空间,每个线程一个,全局不重复) |
用户态通过 syscall(SYS_gettid)获取当前线程的真实 TID。在主线程中,getpid()与 gettid()返回值数值相等。但在子线程中,gettid()会返回一个独立的新 ID。在内核中,每个线程其实都有一个独立的 pid_t(即 TID)。我们通常说的"进程的 PID",其实是主线程的 TID。因此,现代 Linux 中,TID 才是内核调度的真正 ID。
Q:在 Linux 中,线程和进程的 ID 关系是怎样的?
A:从"线程是轻量级进程"切入。强调主线程的 TID 就是进程的 PID,子线程有自己独立的 TID。内核调度只看 TID。
ps -eLf的输出:使用该命令可以看到,一个多线程进程会有多个条目(LWP 列即 TID),但它们的 PID(即主线程 TID)是相同的。
kill(pid, sig):向整个进程(即所有线程)发送信号。因为所有线程共享同一个"进程 PID"。
tgkill(pid, tid, sig):向特定线程发送信号。需要指定 PID(线程组 ID)和 TID。
信号可以发给单个线程(如 SIGSEGV),但如果是发给进程的信号,则由任意一个不阻塞该信号的线程处理。
共享资源(PID 级别):内存空间、文件描述符表、信号处理函数。在 Linux 内核中,线程本质上就是共享资源的进程。
独享资源(TID 级别):线程 ID、寄存器状态、栈空间、调度策略/优先级、errno 变量。
Q:进程间通信方法?
A: 进程间通信是指不同进程之间进行信息传递和交换的机制。1.管道 是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系;2.命名管道 :命名管道也是一种半双工的通信方式,但它允许无亲缘关系的进程间通信;3.消息队列 :消息队列是由消息的链表组成,存放在内核中,并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点;4.共享内存 :共享内存是一种映射一段能被多个进程访问的内存的机制。多个进程可以直接访问共享内存,从而实现高效的数据共享;5.信号量 :信号量是一种计数器,用于控制多个进程对共享资源的访问。它常作为一种锁机制,防止多个进程同时访问共享资源;6.套接字 :套接字是一种用于不同进程间通信的机制,可以用于不同主机之间的进程通信;7.共享文件:多个进程可以通过共享文件进行通信,通过读写共享文件来交换信息。这些进程间通信方式各有特点,可以根据具体的需求选择合适的方式来实现进程间的信息传递和交换。
10.4内核态/用户态
|---------|--------------------------|--------------|
| 内核态 | 中断处理、系统调用、进程调度、内存管理、设备驱动 | Ring 0(最高权限) |
| 用户态 | 普通应用程序、Shell 命令->命令解释 | Ring 3 |
10.5linux设备驱动中的总线,设备和驱动的关系
总线将设备和驱动绑定。在系统每注册一个设备的时候,会寻找与之匹配的驱动;相反的,在系统每注册一个驱动的时候,会寻找与之匹配的设备,而匹配由总线完成。
一个现实的Linux设备和驱动通常都需要挂接在一种总线上。设备与驱动的关联通过总线的match()方法进行匹配,驱动挂载总线时与所有设备进行匹配,设备挂载总线时与所有的驱动进行匹配,所以驱动和设备的挂载无先后之分。匹配成功后会通过调用驱动的probo()方法来初始化设备。
11.链表
| 特性 | 链表 | 数组 |
|---|---|---|
| 随机访问 | ❌ 不支持(必须顺序遍历) | ✅ 支持(O(1) 通过下标) |
| 插入/删除 | ✅ O(1)(已知位置) |
❌ O(n)(需要移动元素) |
| 空间大小 | ✅ 动态扩展 | ❌ 预先固定 |
| 内存布局 | 分散(指针连接) | 连续 |
12.c代码注意点
12.1volatile
volatile 告诉编译器:该变量可能被外部因素修改,不要优化掉对它的访问。变量值可能被程序外部(硬件、中断、其他线程)改变。
| eg场景 | 是否需要 volatile |
分析 |
|---|---|---|
| 多线程应用中被几个任务共享的变量 | ✅ 需要 | 一个线程修改,另一个线程读取,编译器可能优化掉读操作 |
| 一个 ISR 中会访问到的非自动变量 | ✅ 需要 | 中断随时可能修改变量,主程序必须每次重新读取 |
| 并行设备的硬件寄存器 | ✅ 需要 | 硬件寄存器的值由设备决定,每次读都要访问真实硬件v |
| 函数中的局部变量 | ❌ 不需要 | 局部变量在栈上,生命周期只在函数内,不会被外部修改 |
12.2const
const修饰谁,谁就不可变
const与指针:const 放在 * 左边 → 内容不可变(const char *p 和 char const *p 完全等价)
const 放在 * 右边 → 指针地址不可变(char *const p)
| 写法 | 读法 | 指针地址 | 指向内容 |
|---|---|---|---|
const char *p |
指向 const char 的指针 | ✅ 可变 | ❌ 不可变 |
char const *p |
指向 const char 的指针(同上) | ✅ 可变 | ❌ 不可变 |
char *const p |
const 指针指向 char | ❌ 不可变 | ✅ 可变 |
const char *const p |
const 指针指向 const char | ❌ 不可变 | ❌ 不可变 |
12.3sizeof()
sizeof(int)=4字,得到字节大小
12.4宏
宏是纯文本替换,不是函数调用,如#define f(x) (x * 5) + 2,执行printf("%d\n", 3 * f(1));即输出3 * (1 * 5) + 2
12.5static
- 修饰局部变量(函数内部)
| 特性 | 普通局部变量 | static 局部变量 |
|---|---|---|
| 生命周期 | 函数执行期间 | 整个程序运行期 |
| 作用域 | 函数内部 | 函数内部 |
| 初始化 | 每次调用初始化 | 只初始化一次 |
| 存储位置 | 栈 | 静态/全局数据区 |
Q:为什么局部变量未赋值时,每次初始化的结果是不确定的?
**A:**定义局部变量,其实就是在栈中通过移动栈指针来给程序提供一个内存空间和这个局部变量名绑定。因为这段内存空间在栈上,而栈内存是反复使用的,上次用完没清零的,所以说使用栈来实现的局部变量定义时如果不显式初始化,值就是脏的,是不确定的。
2.修饰全局变量 / 函数
| 特性 | 普通全局变量/函数 | static 全局变量/函数 |
|---|---|---|
| 链接属性 | 外部链接(其他文件可访问) | 内部链接(仅限当前文件) |
| 作用 | 实现模块化封装,避免命名冲突 |
12.6malloc/new
| 特性 | malloc(c) | new(c++) |
| 内存位置 | malloc函数从堆上动态分配内存 | new操作符从自由存储区上为对象动态分配内存空间 |
| 返回类型安全性 | malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型 | new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符 |
| 内存分配失败返回值 | malloc分配内存失败时返回NULL | new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL |
| 是否需要指定内存大小 | malloc则需要显式地指出所需内存的尺寸 | 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算 |
|---|
区别:
13.程序内存
13.1程序中的内存分配方法
1、连续分配方式 2、基本分页存储管理方式 3、基本分段存储管理方式 4、段页式存储管理方式
13.2堆/栈
**区别:**1、栈由系统自动分配,而堆是人为申请开辟;2、栈获得的空间较小,而堆获得的空间较大;3、栈由系统自动分配,速度较快,而堆一般速度比较慢;4、栈是连续的空间,而堆是不连续的空间。
什么情况下会栈溢出?如何避免?
1.局部数组过大。当函数内部的数组过大时,有可能导致堆栈溢出。 2.递归调用层次太多。递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。 3.指针或数组越界。(数组越界:访问下标超出 [0, size-1]范围。指针越界:指针移动(p++, p--)后,指向了非法的内存地址。)这种情况最常见,例如进行字符串拷贝,或处理用户输入等等。
解决这类问题的办法有两个, 一是增大栈空间,二是改用动态分配,使用堆(heap)而不是栈(stack)。
14.DMA
DMA是在专门的硬件( DMA)控制下,实现高速外设和主存储器之间自动成批交换数据尽量减少CPU干预的输入/输出操作方式。主要作用就是减少CPU的负担。
15.编译过程/gcc(从一个源文件到可执行文件)过程
编译: 语法分析阶段、语义分析阶段、中间代码生成阶段、代码优化阶段、目标代码生成阶段**gcc:**1、预处理:文件包含,条件编译,注释,生成.i文件 2、编译:对文件进行语法检查,生成.s文件 3、汇编:将文件转化为二进制形式,生成.o文件 4、链接:生成可直接执行的文件
17.FreeRTOS
17.1二值信号量和互斥量的区别
二进制信号量,一个任务申请成功后,可以由另一个任务释放。
互斥型信号量必须是同一个任务申请,同一个任务释放,其他任务释放无效。同一个任务可以递归申请。
17.2任务通知的运行机制
任务通知的数据结构包含在任务控制块中,只要任务存在,任务通知数据结构就已经创建完毕,可以直接使用。
任务通知可以在任务中向指定任务发送通知,也可以在中断中向指定任务发送通知,FreeRTOS 的每个任务都有一个 32 位的通知值,任务控制块中的成员变量 ulNotifiedValue就是这个通知值。
只有在任务中可以等待通知,而不允许在中断中等待通知。如果任务在等待的通知暂时无效,任务会根据用户指定的阻塞超时时间进入阻塞状态,我们可以将等待通知的任务看作是消费者;其它任务和中断可以向等待通知的任务发送通知,发送通知的任务和中断服务函数可以看作是生产者,当其他任务或者中断向这个任务发送任务通知,任务获得通知以后,该任务就会从阻塞态中解除。
18.c++注意点
18.1深复制/浅复制
深复制: 复制指针,并复制指针指向的实际数据。为新对象重新分配内存,并将原数据拷贝到新内存中。复制后,两个对象拥有独立的数据副本。适用于管理动态资源的类(如文件句柄、网络连接、堆内存)
浅复制: 对基本类型 (int, double等)按值拷贝;对指针类型 ,只复制指针本身(地址值),不复制指针指向的实际数据。对对象成员 :调用该对象的复制构造函数(可能是深或浅)。复制后,两个对象的指针成员指向同一块内存。适用于无动态资源的类,或明确需要共享数据时
浅复制的典型问题 :双重释放 :两个对象析构时,会 delete 同一块内存;悬垂指针 :一个对象修改/释放数据,影响另一个对象;数据竞争 :多线程环境下访问共享内存,需额外同步