嵌入式面经(二)
1、关键字volatile有什么用?
volatile
关键字在C和C++编程中用于指示编译器某个变量的值可能在程序的控制流之外被更改。使用volatile
时,当多个线程在访问同一个变量时,如果其中一个线程修改了变量的值,那么其他线程应该能够立即看到修改后的值。使用volatile
修饰的变量告诉编译器每次访问该变量时都必须从内存中读取,而不是从寄存器或其他缓存中(备份值)读取。它主要用于处理与多线程、中断处理和硬件寄存器等相关的情况。如下下情况:
- 处理中断和硬件寄存器:在中断处理程序中,某些变量可能由硬件直接修改,而不是通过常规的变量赋值操作。在这种情况下,使用
volatile
关键字可以确保编译器不会对这些变量的访问进行优化,以避免出现不一致的行为。 - 防止编译器优化:编译器在优化代码时会尝试将变量的访问操作优化为更高效的方式,例如将变量的值缓存在寄存器中。然而,对于某些特殊的变量,如多线程环境下的共享变量、中断处理中的标志位、硬件寄存器等,这种优化可能会导致意外的行为。使用 volatile 关键字可以告诉编译器不要对该变量进行优化,确保每次访问都从内存中读取或写入。
- 处理多线程共享变量:例如:在多线程编程中,当一个变量被多个线程共享并且可能被一个线程修改时,而另一个线程在不知情的情况下使用了这个变量(已经被修改过)。而volatile关键字告诉编译器,读取的值是一个易变的变量,可能会被程序的其他部分(例如其他线程、硬件设备等)改变。因此,每次引用改变量时,都需要直接从它在内存中的地址读取,而不能依赖可能已经过时的、保存在寄存器中的值。这样可以防止编译器对共享变量的优化,确保每个线程都能正确地读取到最新的值。
总结:volatile
关键字的主要作用是告诉编译器每次访问被修饰的变量时都必须从内存中读取,而不是使用寄存器或缓存中的值。使用volatile
关键字可以确保程序能够读取到这些变量的最新值,从而避免由于编译器优化导致的意外行为。
2、指针可以被volatile修饰?
指针可以被 volatile
修饰。volatile
关键字用于告诉编译器,被其修饰的变量可能会在程序的控制之外被修改,因此编译器不应该对其进行优化,以避免意外的行为发生。
3、DMA有什么用?
在嵌入式系统中,DMA(直接内存访问)主要用于提高数据传输效率和释放CPU资源。以下是DMA在嵌入式系统中的一些主要用途和优势:
用途 | 优势 | 典型应用场景 | 特点 |
---|---|---|---|
高速数据传输 | 提高传输速度,减轻CPU负担 | 从存储设备读取数据,如SD卡 | 直接内存与外设之间的数据传输,CPU无需参与 |
音频和视频数据处理 | 实时处理大量数据,减少延迟 | 音频数据传输到DAC,视频数据传输到显示器 | 确保音视频数据流的连续性,避免丢帧或音频中断 |
网络通信 | 提高网络吞吐量,减轻CPU处理网络数据的负担 | 数据包从NIC读取或写入NIC | 高效处理大量网络数据包,减少CPU在网络传输上的负担 |
外设通信 | 高效数据交换,减少中断次数 | SPI、I2C、UART通信 | 自动化外设数据传输,减小CPU参与频率 |
数据采集 | 实时、高速采集数据,确保数据完整性 | 传感器数据读取,ADC采集 | 确保数据采集的实时性和准确性,防止数据丢失或延迟 |
4、DMA为什么能提高效率?
DMA(Direct Memory Access,直接内存访问)能提高效率的原因主要有几点:
- 减少CPU介入:在没有DMA的情况下,数据传输通常需要CPU的直接介入,CPU需要在数据传输过程中处理每个数据包。而有了DMA,数据传输可以直接在设备和内存之间完成,减少了CPU的负担,释放了CPU的时间用于处理其他任务,提高了系统的整体效率。
- 减少总线争用:DMA控制器可以独立地使用系统总线,而不需要频繁地与CPU竞争总线的使用权。这样可以减少因总线争用而导致的等待时间,提高了数据传输的效率。
- 数据传输速度更快:DMA通常能够以更高的速度传输数据,因为它可以使用高速缓冲区或者直接访问内存的方式来执行数据传输,避免了每次传输都需要CPU介入和处理的延迟。
- 支持大数据块传输:DMA可以一次性传输大量数据,而不需要将数据分成小块交给CPU逐个处理,这对于需要高效处理大数据量的应用非常重要。
5、怎样把unsigned short拆分为2个字节?
可以使用位操作和类型转换来将一个 unsigned short
(无符号的短整数类型。它通常占用16位(2个字节)的存储空间。)拆分为两个字节
#include <stdio.h>
int main() {
unsigned short value = 0xABCD; //示例值,十六进制表示),在内存中,它将被表示为两个字节:高字节(高8位):0xAB 低字节(低8位):0xCD
unsigned char highByte, lowByte;
// 拆分为高字节和低字节
highByte = (value >> 8) & 0xFF; //右移8位,取高字节 0xABCD(二进制为 10101011 11001101),那么 value >> 8 将得到 0x00AB(二进制为 00000000 10101011);与 0xFF(二进制为 00000000 11111111)进行按位与操作。这将保留低8位,而将其他位清零。
lowByte = value & 0xFF; //低字节
// 输出结果
printf("High byte: 0x%02X\n", highByte); //0xAB
printf("Low byte: 0x%02X\n", lowByte); //0XCD
return 0;
}
6、堆栈方面的内存分布,堆空间 栈空间 分别存什么?
堆栈内存分布总结:
特点 | 堆(Heap) | 栈(Stack) |
---|---|---|
分配和释放 | 由程序员显式管理,通过malloc /free 等 | 由编译器自动管理 |
生命周期 | 程序员控制,直到调用free | 随函数调用和返回自动管理 |
用途 | 动态分配大型数据结构、对象实例等 | 局部变量、函数参数、返回地址等 |
内存布局 | 不连续,动态增长或缩减 | 连续 |
速度 | 相对较慢,因为需要动态分配和释放 | 相对较快,因为自动管理,无需显式释放 |
碎片 | 可能会产生内存碎片 | 不会产生内存碎片 |
大小限制 | 由系统的可用内存决定 | 由栈大小(通常较小)决定 |
堆空间:适合动态分配的大数据结构,生命周期由程序员控制。
栈空间:适合局部变量和函数参数,自动管理,速度快但大小有限。
7、malloc 和 free 函数
malloc
和 free
是 C 语言中用于动态内存管理的标准库函数:
-
malloc
函数:malloc
(memory allocation)用于在堆上动态分配指定大小的内存,并返回一个指向该内存块的指针。分配的内存块中的内容未初始化,即其值是未定义的。void* malloc(size_t size); //`size`:要分配的内存块的大小,以字节为单位。
-
free
函数:free
用于释放之前使用malloc
、calloc
或realloc
分配的内存,避免内存泄漏。释放的内存可以重新分配使用。void free(void* ptr); //`ptr`:指向要释放的内存块的指针。如果 `ptr` 为 `NULL`,则 `free` 不执行任何操作。
8、malloc/new区别:不用sizeof行不行?
区别:
特性 | malloc | new |
---|---|---|
语言 | C | C++ |
头文件 | <stdlib.h> | 无,直接使用 |
用法 | void* malloc(size_t size); | type* ptr = new type; type* ptr = new type(initializers); |
内存分配 | 分配指定字节数的内存块 | 分配并初始化一个指定类型的对象 |
初始化 | 不进行对象初始化 | 可以使用构造函数进行对象初始化 |
返回类型安全 | 返回 void* 需要显式类型转换来使用 | 返回指定类型的指针,类型安全,不需要显式转换 |
内存分配失败 | 返回 NULL | 抛出 std::bad_alloc 异常 |
释放内存 | void free(void* ptr); | delete ptr; 或 delete[] ptr; |
适用场景 | C 程序中的动态内存分配 | C++ 程序中的对象动态分配和构造 |
sizeof 的必要性:
无论是使用 malloc
还是 new
,都需要指定要分配的内存块的大小。在实际应用中,几乎总是需要使用 sizeof
来获取要分配的数据类型的大小,因为:
- 确定分配的内存大小:
sizeof
返回的是指定类型或变量的字节大小,这是确保分配足够空间的关键。 - 类型安全:使用
sizeof
可以避免硬编码大小,减少出错的可能性。 - 跨平台兼容性:不同平台上相同类型的大小可能不同,使用
sizeof
可以提高代码的可移植性。
9、 C 和 C++ 的区别
特性 | C | C++ |
---|---|---|
编程范式 | 面向过程编程语言,注重简洁和高效。 | 面向对象和泛型编程语言,支持类、继承、多态等特性。 |
面向对象支持 | 不支持面向对象编程。 | 完全支持面向对象编程,包括类、继承、多态、封装等。 |
异常处理 | 不支持异常处理机制。 | 支持异常处理机制,可以使用 try-catch 块捕获和处理异常。 |
编程风格 | 更加接近硬件和系统级编程,适用于嵌入式开发等场景。 | 更加面向对象和现代编程风格,适用于大型应用和复杂系统的开发。 |
- 面向过程:面向过程的编程思想强调问题解决过程中对步骤和操作的关注,通过按照特定顺序依次执行一系列函数来完成任务。它将问题分解为多个可重用的函数,并通过函数之间的参数传递数据来实现协作。
- 面向对象:将数据和操作数据的方法(函数)封装在一起,形成对象。程序被组织为一组相互协作的对象,这些对象通过消息传递来进行交互和处理。
10、平衡二叉树相关
- 平衡二叉树是一种二叉搜索树(Binary Search Tree,BST),其左右子树的高度差不超过1,并且左右子树本身也是平衡二叉树
- 每个节点的左子树和右子树的高度差不超过1,保持树的高度相对较小,使得插入、删除和查找等操作的时间复杂度能保持在较低的水平
- 因为平衡的特性,平衡二叉树的查找、插入和删除操作的时间复杂度通常为 O(log n),其中 n 是树中节点的数量
- 平衡二叉树的平衡因子是指每个节点的左子树高度和右子树高度之差的绝对值。在 AVL 树中,平衡因子的绝对值不能超过1
- 平衡二叉树的设计旨在优化二叉搜索树的操作复杂度,以保证在动态数据插入和删除的情况下,树的结构能够保持相对平衡,从而保证操作的高效性
10、linux进程间通信的方式
管道、信号量、信号、消息队列、共享内存、socket
-
管道:管道数据只能单向流动,所以如果要实现双向通信,就要创建2个管道,只能承载无格式的字节流。
匿名管道:只能在父子进程关系中使用。
-
命名管道可以在不关联的两个进程间使用。
-
信号量:信号量是一个计数器,可以用来控制多个进程对资源的访问,通常作为一种锁机制,防止某个进程正在访问共享资源,其他进程也访问资源。
-
信号:信号是进程之间唯一的异步通信机制,信号传递的信息比较少,开销少。
-
消息队列:消息队列克服了信号传递信息少、管道只能承载无格式的字节流,消息到了就放进去,需要的时候去取。
-
共享内存:共享内存就是映射一段能被进程之间共享的内存,这段内存由一个进程创建,但是多个进程都可以共享访问,是最快的一种进程间通信的方式(不需要从用户态到内核态的切换),它是针对其他进程间通信方式运行效率低而专门设计的。
-
socket:不仅仅可以用于本地进程通信,还可以用于不通主机进程之间的通信。
11、TCP/UDP主要区别
- TCP与UDP区别总结:
- TCP主要面向连接;udp是无连接的,发送数据之前不需要连接
- TCP提供可靠的服务,传输数据不丢失、无重复且按序到达;UDP不保证可靠交付
- UDP具有较好的实时性,工作效率比TCP高,适用于对速度实时性要求较高的通信;
- TCP连接是点对点的;UDP支持一对一、一对多、多对多、多对一的交互通信;
- TCP对系统资源要求较多,UCP对系统资源要求较少
12、堆栈溢出一般是由什么原因导致的?
- 函数调用层次太深:函数递归调用时,系统要在栈中不断保存函数调用时的现场和产生的变量,如果递归调用太深,就会造成栈溢出,这时递归无法返回。再有,当函数调用层次过深时也可能导致栈无法容纳这些调用的返回地址而造成栈溢出。
- 动态申请空间使用之后没有释放:由于C语言中没有垃圾资源自动回收机制,因此,需要程序主动释放已经不再使用的动态地址空间。申请的动态空间使用的是堆空间,动态空间使用不会造成堆溢出。
- 数组访问越界:C语言没有提供数组下标越界检查,如果在程序中出现数组下标访问超出数组范围,在运行过程中可能会内存访问错误。
- 指针非法访问:指针保存了一个非法的地址,通过这样的指针访问所指向的地址时会产生内存访问错误。
13、指针和引用的区别?
特性 | 指针(Pointer) | 引用(Reference) |
---|---|---|
定义 | 存储变量地址的变量。 | 变量的别名。 |
声明 | int *ptr = &var; | int &ref = var; |
存储地址 | 存储变量的内存地址。 | 本质上是变量的常量指针。 |
重新赋值 | 可以在生命周期内被重新赋值。 | 不能重新绑定到其他变量。 |
算术运算 | 可以进行指针算术运算。 | 不能进行算术运算。 |
用法 | 常用于动态内存分配和数组操作。 | 常用于函数参数传递和返回值。 |
14、全局变量和局部变量的区别?
特性 | 全局变量(Global Variable) | 局部变量(Local Variable) |
---|---|---|
定义和声明 | 所有函数外部声明 | 函数或代码块内部 |
作用域 | 整个程序内都可访问 | 仅在声明它的函数或代码块内部有效 |
生命周期 | 程序运行期间始终存在,从程序启动到程序结束 | 从声明开始直到离开作用域(函数或代码块),生命周期结束 |
初始值 | 默认初始值为0(如果未显式初始化) | 未显式初始化时值是未定义的 |
示例代码 | int globalVar = 0; | void func() { int localVar = 0; } |
15、什么是预编译,何时需要预编译?
-
预编译:(Precompilation)是一种编译优化技术,它在正式编译之前对源代码进行处理,以加速后续的编译过程。预编译通常用于处理头文件或静态不变的代码片段,将这些内容编译成预编译头(Precompiled Header, PCH)文件,以便在多个源文件中重用,从而减少重复编译的开销。
-
何时需要预编译:
- 在包含大量头文件的大型项目中
- 重复包含头文件
- 不常更改的头文件
16、为什么要使用宏,宏有什么优缺点?
(1)为什么要使用宏:宏可以定义一些常用的代码片段或函数,可以在程序中多次使用,减少代码的重复编写。
(2)缺点:
- 可读性差:过度使用宏可能会导致代码可读性下降,因为宏定义是简单的文本替换,可能会隐藏代码的实际逻辑,使得代码难以理解和调试。
- 作用域问题:宏是全局替换的,不像函数具有局部作用域,可能会引入意外的命名冲突或不可预见的副作用。
- 没有类型检查:宏是简单的文本替换,不进行类型检查,可能会导致潜在的类型错误或难以察觉的错误。