嵌入式开发面试题全解析:从基础语法到内存操作,手把手教你吃透核心考点

一、填空选择题

1. 函数功能分析

题目:分析下列函数的功能

fun(char *a, char *b) {  
    while((*b = *a) != '\0') {  
        a++;  
        b++;  
    }  
}  

选项
A)将 a 所指字符串赋给 b 所指空间
B)使指针 b 指向 a 所指字符串
C)将 a 所指字符串和 b 所指字符串进行比较
D)检查 a 和 b 所指字符串中是否有 '\0'


详细解析
  1. 分析函数核心操作

    • 函数中 *b = *a 是 赋值操作,将 a 指针指向的字符复制到 b 指针指向的内存空间。
    • 随后判断 (*b = *a) 的结果是否为 '\0'(字符串结束标志)。若不是,a 和 b 指针同时后移,继续复制下一个字符。
  2. 逐项分析选项

    • 选项 A
      • 函数通过循环逐字符复制 a 指向的字符串到 b 指向的空间,直到遇到 '\0'。例如,若 a 指向 "hello"b 指向一块足够大的空闲内存,函数执行后 b 指向的空间内容会变为 "hello"。因此,选项 A 正确
    • 选项 B
      • 函数并非修改 b 指针的指向,而是修改 b 指针指向的内存空间的内容。假设 b 初始指向地址 0x1000,函数执行过程中 b 会逐步后移(如 0x1000 → 0x1001 → …),但这是为了复制内容,而非让 b 指向 a 的字符串。选项 B 错误
    • 选项 C
      • 代码中 *b = *a 是赋值(=),而比较操作应为 *b == *a==)。因此,函数不是在比较两个字符串。选项 C 错误
    • 选项 D
      • 虽然循环条件涉及 '\0',但目的是通过判断复制的字符是否为 '\0' 来结束循环(即复制完整个字符串),而非检查 a 和 b 中是否有 '\0'选项 D 错误

总结:该函数的核心是 字符串复制,将 a 所指字符串赋给 b 所指空间,正确答案为 A

通过这道题,需掌握:

  • 指针操作(* 取值、++ 指针移动)。
  • 字符串结束标志 '\0' 的作用。
  • 赋值(=)和比较(==)操作的区别。

后续遇到类似题目,可先分析代码核心操作(如赋值、比较、移动等),再结合选项逐一排除。

2. 错误定义语句判断

题目:以下错误的定义语句是( ),值等于 0x38 的元素是______。

A)char x1[][3] = { {'1'}, {'2'}, {'3','4','5'} };
B)char x2[4][3] = { {'6'}, {'7','8'}, {'9'} };
C)char x3[4][] = { {10,11,12}, {13,14,15}, {16,17,18}, {19,20,21} };
D)char x4[][3] = {22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39};

涉及知识点:二维数组
1. 二维数组的概念

二维数组可视为 “表格” 或 “矩阵” 形式的数据结构,由多个一维数组组成。例如,一个 m 行 n 列的二维数组,类似有 m 行、每行含 n 个元素的表格。在 C 语言中,常用于存储需按行和列组织的数据,如矩阵、表格或字符网格等。

2. 二维数组的定义方式
  • 完整定义类型 数组名[行数][列数]
    • 示例:int arr[3][4];,定义 3 行 4 列的整型二维数组,共 3×4 = 12 个元素。
  • 省略行数定义类型 数组名[][列数],通过初始化数据确定行数。
    • 示例:int arr[][4] = { {1,2,3,4}, {5,6,7,8} };,编译器根据初始化行数(2 行)确定第一维大小为 2。
      关键规则列数不能省略。编译器需知每行元素数,才能正确计算内存地址与访问元素。若省略列数(如选项 C 的 char x3[4][]),编译器无法确定每行长度,导致编译错误。
3. 二维数组的初始化
  • 按行初始化类型 数组名[行数][列数] = { {行 1 元素}, {行 2 元素},... };
    • 示例:int arr[2][3] = { {1,2,3}, {4,5,6} };,明确为每行赋值。
  • 不完整初始化:若初始化数据少于总元素数,剩余元素默认补 0(数值类型)或空字符(字符类型)。
    • 示例:int arr[2][3] = { {1}, {2} };,等价于 { {1,0,0}, {2,0,0} }
  • 省略行数初始化类型 数组名[][列数] = { 元素 1, 元素 2,... };,按行依次填充。
    • 示例:int arr[][3] = {1,2,3,4,5,6};,等价于 { {1,2,3}, {4,5,6} },共 2 行。
4. 二维数组的内存存储

二维数组在内存中 按行优先顺序连续存储。例如,int arr[2][3] = { {1,2,3}, {4,5,6} },先存第 1 行 1,2,3,再存第 2 行 4,5,6

  • 元素地址计算公式:&arr[i][j] = 数组首地址 + (i×列数 + j)×sizeof(类型)
5. 结合题目分析
    • 选项 A
      • char x1[][3] 省略了第一维大小,但明确指定了第二维大小为 3。
      • 初始化数据 { {'1'}, {'2'}, {'3','4','5'} } 中,每行元素个数都不超过 3,符合二维数组定义规则(第一维大小可根据初始化数据推断,第二维必须明确)。定义正确
      • 涉及知识点:二维数组定义时,第二维大小必须明确指定,第一维大小可根据初始化数据推断。
      • 知识点解析:在 C 语言中,二维数组的存储是按行连续存储的。当省略第一维大小时,编译器会根据初始化数据的行数来确定第一维的大小。例如,这里有 3 组初始化数据,所以第一维大小为 3。
    • 选项 B
      • char x2[4][3] 明确指定了第一维大小为 4,第二维大小为 3。
      • 初始化数据 { {'6'}, {'7','8'}, {'9'} } 中,不足 4 行时,剩余行默认补 0,符合定义规则。定义正确
      • 涉及知识点:二维数组初始化时,若初始化数据不足,剩余元素默认补 0。
      • 知识点解析:当定义二维数组并初始化时,如果提供的初始化数据少于数组的总元素个数,那么未初始化的元素会被自动初始化为 0(对于数值类型)或空字符(对于字符类型)。这里 x2 数组定义为 4 行 3 列,但只提供了 3 组初始化数据,所以第 4 行的 3 个元素会被初始化为 0。
    • 选项 C
      • char x3[4][] 中,第二维大小被省略。
      • 在 C 语言中,二维数组定义时,第二维大小必须明确指定,第一维大小可以省略(通过初始化数据推断)。此定义违反了这一规则。定义错误
      • 涉及知识点:二维数组定义时第二维大小不可省略。
      • 知识点解析:这是 C 语言的规定,因为编译器需要知道每行有多少个元素,才能正确地为数组分配内存和进行访问。如果省略第二维大小,编译器无法确定每行的长度,就无法正确处理数组。
    • 选项 D
      • char x4[][3] 省略了第一维大小,第二维大小为 3。
      • 初始化数据共 18 个元素,18 ÷ 3 = 6,可推断第一维大小为 6,符合定义规则。定义正确
      • 涉及知识点:根据初始化数据计算二维数组第一维大小。
      • 知识点解析:当省略第一维大小时,编译器会根据初始化数据的总个数和第二维大小来计算第一维大小。这里总共有 18 个元素,第二维大小为 3,所以第一维大小为 18 ÷ 3 = 6。
  1. 查找值等于 0x38 的元素

    • 0x38 是十六进制数,转换为十进制为 56。
    • 分析选项 D 的数组 {22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39},按每行 3 个元素划分:
      • 第 1 行:22,23,24
      • 第 2 行:25,26,27
      • 第 3 行:28,29,30
      • 第 4 行:31,32,33
      • 第 5 行:34,35,36
      • 第 6 行:37,38,39
    • 数值 56 对应十六进制 0x38,在数组中为 38,位于第 6 行第 2 个位置(从 0 开始计数,即 x4 [5][1]),但题目问的是 “值等于 0x38 的元素”,直接看元素值,该元素为 38。
    • 涉及知识点:十六进制与十进制的转换,二维数组元素的定位。
    • 知识点解析:十六进制转换为十进制可以通过位权相加的方法。例如,0x38 = 3×16 + 8 = 56。在二维数组中定位元素时,需要根据数组的行数和列数来计算。这里 x4 数组是按行优先存储的,所以先根据元素个数和列数计算出行数,再确定列数。

总结:错误的定义语句是 C,值等于 0x38 的元素是 38(在选项 D 的数组中)。

本题要求深刻理解二维数组列数不可省略的规则、初始化方式及内存存储特点,这对嵌入式开发中数组的正确使用(如驱动开发的数据表格处理、应用层的矩阵运算等)至关重要。

3. STM32 宏定义与内存操作

题目:完成宏定义

#define SET_U32_VALUE(addr, value)  ______  
#define GET_U8_VALUE(addr)  ______  

使用上述宏定义将 0x11223344 写入绝对地址 0x20000123,检查是否会产生 hardfault,若不产生,value 的值是______。

int main(void) {  
    unsigned char value = 0;  
    SET_U32_VALUE(0x20000123, 0x11223344);  
    value = GET_U8_VALUE(0x20000123);  
    return 0;  
}
题目分析

本题主要考查了 STM32 中宏定义的使用以及对内存操作的理解,同时涉及到硬件错误(hardfault)的判断和字节序的知识。我们需要完成两个宏定义,分别用于向指定地址写入 32 位数据和从指定地址读取 8 位数据,然后分析将特定值写入特定地址后读取的结果。

详细解析
1. 完成宏定义
  • SET_U32_VALUE 宏定义
    • 这个宏的作用是将一个 32 位的值 value 写入到指定的地址 addr 中。
    • 要实现这个功能,我们需要将 addr 转换为一个指向 32 位无符号整数的指针,然后通过这个指针来写入 value。同时,为了确保编译器不会对这个内存操作进行优化,我们使用 volatile 关键字。
    • 宏定义代码
#define SET_U32_VALUE(addr, value) (*(volatile unsigned int*)(addr) = (value))

  • 解释

    • (volatile unsigned int*)(addr):将 addr 强制转换为一个指向 32 位无符号整数的指针,并且使用 volatile 关键字告诉编译器这个指针指向的内存可能会被意外修改,不要对其进行优化。
    • *(volatile unsigned int*)(addr):通过这个指针来访问对应的内存地址。
    • *(volatile unsigned int*)(addr) = (value):将 value 写入到这个内存地址中。
  • GET_U8_VALUE 宏定义

    • 这个宏的作用是从指定的地址 addr 中读取一个 8 位的值。
    • 同样,我们需要将 addr 转换为一个指向 8 位无符号整数的指针,然后通过这个指针来读取数据。
    • 宏定义代码
#define GET_U8_VALUE(addr) (*(volatile unsigned char*)(addr))

  • 解释
    • (volatile unsigned char*)(addr):将 addr 强制转换为一个指向 8 位无符号整数的指针,使用 volatile 关键字确保编译器不优化。
    • *(volatile unsigned char*)(addr):通过这个指针来访问对应的内存地址并读取一个字节的数据。
2. 分析是否会产生 hardfault
  • hardfault 简介:hardfault 是 STM32 中的一种严重错误,通常是由于访问了非法的内存地址(如未映射的地址、受保护的地址等)或者执行了非法的指令而引起的。
  • 本题情况分析:地址 0x20000123 通常是 STM32 的 SRAM 地址范围(SRAM 的起始地址一般是 0x20000000),在正常情况下,这个地址是可以进行读写操作的,所以一般不会产生 hardfault。但如果这个地址被硬件或者软件配置为受保护的区域,那么写入操作就可能会触发 hardfault。这里我们假设该地址是可读写的,不会产生 hardfault。
3. 确定 value 的值
  • 字节序知识:STM32 采用的是小端字节序(Little Endian),即在内存中,数据的低字节存储在低地址,高字节存储在高地址。
  • 写入数据分析:当我们将 0x11223344 写入地址 0x20000123 时,按照小端字节序,内存中的存储情况如下:
    • 地址 0x20000123 存储 0x44
    • 地址 0x20000124 存储 0x33
    • 地址 0x20000125 存储 0x22
    • 地址 0x20000126 存储 0x11
  • 读取数据分析:当我们使用 GET_U8_VALUE(0x20000123) 读取数据时,读取的是地址 0x20000123 处的一个字节,即 0x44。所以 value 的值是 0x44
总结

本题涉及到的知识点包括:

GET_U8_VALUE 宏定义实现了从指定地址读取 8 位值的功能,代码如下:

  • 宏定义的使用:通过宏定义来实现对内存的读写操作,提高代码的可复用性和可读性。
  • 内存操作
  • 如何将一个地址转换为指针,并通过指针来读写内存:

  • 在嵌入式系统中,内存操作是非常基础且重要的部分,它涉及到对硬件内存地址的直接读写访问。下面详细解释本题中涉及的内存操作相关知识。

    指针与内存地址

    在 C 语言里,指针是一个变量,其存储的是内存地址。通过指针,我们能够直接访问和操作内存中的数据。在本题中,要将一个 32 位的值写入指定地址,或者从指定地址读取一个 8 位的值,就需要借助指针来实现。

     

    例如,若有一个整数变量 int num = 10;,使用 &num 可获取该变量的内存地址。若要定义一个指针指向这个变量,可以这样写:int *p = #。这里的 p 就是一个指针,它存储着 num 的内存地址。

    强制类型转换

    在进行内存操作时,常常需要将一个普通的整数类型(如地址)转换为指针类型,这就需要用到强制类型转换。在本题中,addr 是一个表示内存地址的整数,要将其转换为指针类型才能进行内存读写操作。

     

    例如,若要将一个整数 address 转换为指向 32 位无符号整数的指针,可以这样写:(volatile unsigned int*)address。这里的 volatile 关键字是为了告诉编译器,这个指针指向的内存可能会被意外修改,所以不要对其进行优化。

    内存读写操作

    一旦将地址转换为指针类型,就可以通过指针来进行内存读写操作。对于写入操作,使用赋值语句;对于读取操作,直接使用指针。

     

    例如,若有一个指向 32 位无符号整数的指针 p,要将一个值 value 写入该指针指向的内存地址,可以这样写:*p = value;。若要从该指针指向的内存地址读取一个值,可以这样写:int read_value = *p;

     

    结合本题,SET_U32_VALUE 宏定义实现了向指定地址写入 32 位值的功能,代码如下:

     
    #define SET_U32_VALUE(addr, value) (*(volatile unsigned int*)(addr) = (value))
    
     

    解释:

  • (volatile unsigned int*)(addr):将 addr 强制转换为指向 32 位无符号整数的指针。
  • *(volatile unsigned int*)(addr):通过这个指针访问对应的内存地址。
  • *(volatile unsigned int*)(addr) = (value):将 value 写入该内存地址。
  • (volatile unsigned char*)(addr):将 addr 强制转换为指向 8 位无符号整数的指针。
  • *(volatile unsigned char*)(addr):通过这个指针访问对应的内存地址并读取一个字节的数据。
  • 字节序
  • 了解 STM32 的小端字节序,以及如何根据字节序来确定数据在内存中的存储方式:

    字节序是指多字节数据在内存中存储时字节的排列顺序,常见的字节序有小端字节序(Little Endian)和大端字节序(Big Endian)。

     

    小端字节序(Little Endian)

     

    在小端字节序中,数据的低字节存储在低地址,高字节存储在高地址。例如,对于一个 32 位整数 0x11223344,在小端字节序的内存中存储情况如下:

     
    内存地址存储内容
    0x200001230x44
    0x200001240x33
    0x200001250x22
    0x200001260x11
     

    可以看到,最低字节 0x44 存储在最低地址 0x20000123 处,随着地址的增加,依次存储较高的字节。

     

    大端字节序(Big Endian)

     

    在大端字节序中,数据的高字节存储在低地址,低字节存储在高地址。对于同样的 32 位整数 0x11223344,在大端字节序的内存中存储情况如下:

     
    内存地址存储内容
    0x200001230x11
    0x200001240x22
    0x200001250x33
    0x200001260x44
     

    可以看到,最高字节 0x11 存储在最低地址 0x20000123 处,随着地址的增加,依次存储较低的字节。

     

    STM32 的字节序

     

    STM32 采用的是小端字节序。在本题中,将 0x11223344 写入地址 0x20000123 时,按照小端字节序,内存中的存储情况为:地址 0x20000123 存储 0x44,地址 0x20000124 存储 0x33,地址 0x20000125 存储 0x22,地址 0x20000126 存储 0x11

     

    当使用 GET_U8_VALUE(0x20000123) 读取数据时,读取的是地址 0x20000123 处的一个字节,即 0x44。所以 value 的值是 0x44

  • 硬件错误:对 hardfault 有一定的了解,知道可能触发 hardfault 的原因。

在嵌入式开发中,这些知识是非常重要的,尤其是在进行底层驱动开发、内存管理等方面。通过本题的练习,新手可以更好地掌握这些知识点,为后续的开发打下坚实的基础。

综上所述,答案依次为:*(volatile unsigned int*)(addr) = (value)*(volatile unsigned char*)(addr)0x44

二、简答题:实现 strstr 功能

题目:编写程序实现 strstr 功能,从字符串 str1 中查找是否有字符串 str2,若有,从 str1 中匹配位置起返回指针;若无,返回 NULL

一、核心思路分析

strstr 函数的核心是 字符串匹配,即判断短字符串 str2 是否是长字符串 str1 的子串。具体步骤如下:

  1. 处理边界条件:若 str2 是空字符串(str2 == ""),直接返回 str1 的首地址(C 语言标准规定)。
  2. 逐个字符匹配:在 str1 中遍历每个字符,以当前字符为起点,与 str2 的第一个字符比较。若匹配,继续比较后续字符;若不匹配,str1 指针后移一位,重新开始匹配。
  3. 判断完全匹配:若 str2 的所有字符都匹配成功(即遍历到 str2 的结束符 '\0'),则返回当前 str1 的匹配起点;若 str1 遍历完毕仍未找到匹配,则返回 NULL

二、代码实现(C 语言)

char *strstr_custom(char *str1, char *str2) {  
    // 处理边界条件:str2 为空字符串时,直接返回 str1  
    if (*str2 == '\0') {  
        return str1;  
    }  

    // 外层循环:遍历 str1 的每个字符作为匹配起点  
    while (*str1 != '\0') {  
        char *p1 = str1;  // 记录 str1 当前匹配起点  
        char *p2 = str2;  // 指向 str2 的当前比较字符  

        // 内层循环:逐个字符比较 str1 和 str2  
        while (*p1 != '\0' && *p2 != '\0' && *p1 == *p2) {  
            p1++;  // str1 后移一位  
            p2++;  // str2 后移一位  
        }  

        // 若 str2 完全匹配(p2 指向 '\0'),返回匹配起点  
        if (*p2 == '\0') {  
            return str1;  
        }  

        // str1 未匹配,后移一位继续查找  
        str1++;  
    }  

    // 未找到匹配子串,返回 NULL  
    return NULL;  
}  

三、关键点解析

1. 边界条件处理
  • str2 为空字符串:根据 C 语言标准(如 man strstr),当 str2 是空字符串时,应返回 str1 的首地址。代码中通过 if (*str2 == '\0') 判断,直接返回 str1,避免后续无效比较。
  • str1 长度小于 str2:若 str1 比 str2 短,内层循环会因 *p1 先到达 '\0' 而退出,最终返回 NULL,无需额外判断。
2. 双重循环逻辑
  • 外层循环:遍历 str1 的每个字符,确定匹配起点(str1 指针每次后移一位)。
  • 内层循环:从当前起点开始,逐个字符比较 str1 和 str2
    • 若字符相等(*p1 == *p2),p1 和 p2 同时后移。
    • 若字符不等或任一字符串结束(*p1 == '\0' 或 *p2 == '\0'),内层循环退出。
3. 指针操作与字符串结束符
  • 字符串以 '\0' 作为结束标志,循环条件通过判断 *str1 != '\0' 确保不越界。
  • 匹配成功的条件是 *p2 == '\0'(即 str2 所有字符已匹配完毕),而非 p1 到达 str1 末尾。

四、易错点分析

  1. 忽略 str2 为空的情况

    • 错误代码:直接跳过空字符串处理,导致程序崩溃或逻辑错误。
    • 正确做法:必须优先判断 str2 是否为空,这是面试中高频考点。
  2. 内层循环条件错误

    • 错误写法:while (*p1 == *p2)(未检查 '\0'),可能导致越界访问内存。
    • 正确写法:需同时判断 *p1 和 *p2 不为 '\0',即 while (*p1 != '\0' && *p2 != '\0' && *p1 == *p2)
  3. 返回指针错误

    • 错误:内层循环中直接返回 p1 而非 str1str1 是匹配起点,p1 是内层循环后移后的指针)。
    • 正确:匹配成功时,应返回外层循环记录的起点 str1,而非内层循环中的临时指针 p1

五、拓展知识:字符串匹配算法

本题实现的是 暴力匹配法(Brute-Force),时间复杂度为 O(n∗m)(n 是 str1 长度,m 是 str2 长度)。实际开发中,若追求效率,可使用以下优化算法:

  1. KMP 算法:通过预处理 str2 生成部分匹配表,避免重复比较已匹配的字符,时间复杂度优化至 O(n+m)。
  2. Boyer-Moore 算法:从右往左比较字符,利用坏字符规则和好后缀规则跳跃式移动指针,适合在长文本中搜索短模式串。

对于嵌入式开发,若对性能要求不高,暴力匹配法已足够简洁高效;若处理大规模数据,需根据场景选择更优算法。

六、总结

实现 strstr 需掌握以下核心能力:

  1. 字符串基本操作:指针遍历、'\0' 结束符判断。
  2. 边界条件处理:空字符串、长度差异等特殊情况。
  3. 双重循环逻辑:外层确定起点,内层逐个匹配字符。

通过本题,新手可深入理解字符串匹配的底层逻辑,为嵌入式开发中的文本处理、协议解析等场景打下基础。实际编码时,需注意内存越界风险,确保输入参数的有效性(如指针非空),必要时添加错误检查代码。

通过以上题目解析,就可系统掌握嵌入式面试常考知识点,注意易错点,结合拓展内容深化理解,逐步提升嵌入式开发能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值