一、填空选择题
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'
详细解析
-
分析函数核心操作
- 函数中
*b = *a
是 赋值操作,将a
指针指向的字符复制到b
指针指向的内存空间。 - 随后判断
(*b = *a)
的结果是否为'\0'
(字符串结束标志)。若不是,a
和b
指针同时后移,继续复制下一个字符。
- 函数中
-
逐项分析选项
- 选项 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:
总结:该函数的核心是 字符串复制,将 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。
- 选项 A:
-
查找值等于 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
关键字。 - 宏定义代码:
- 这个宏的作用是将一个 32 位的值
#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
,在小端字节序的内存中存储情况如下:内存地址 存储内容 0x20000123
0x44
0x20000124
0x33
0x20000125
0x22
0x20000126
0x11
可以看到,最低字节
0x44
存储在最低地址0x20000123
处,随着地址的增加,依次存储较高的字节。大端字节序(Big Endian)
在大端字节序中,数据的高字节存储在低地址,低字节存储在高地址。对于同样的 32 位整数
0x11223344
,在大端字节序的内存中存储情况如下:内存地址 存储内容 0x20000123
0x11
0x20000124
0x22
0x20000125
0x33
0x20000126
0x44
可以看到,最高字节
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
的子串。具体步骤如下:
- 处理边界条件:若
str2
是空字符串(str2 == ""
),直接返回str1
的首地址(C 语言标准规定)。 - 逐个字符匹配:在
str1
中遍历每个字符,以当前字符为起点,与str2
的第一个字符比较。若匹配,继续比较后续字符;若不匹配,str1
指针后移一位,重新开始匹配。 - 判断完全匹配:若
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
末尾。
四、易错点分析
-
忽略
str2
为空的情况:- 错误代码:直接跳过空字符串处理,导致程序崩溃或逻辑错误。
- 正确做法:必须优先判断
str2
是否为空,这是面试中高频考点。
-
内层循环条件错误:
- 错误写法:
while (*p1 == *p2)
(未检查'\0'
),可能导致越界访问内存。 - 正确写法:需同时判断
*p1
和*p2
不为'\0'
,即while (*p1 != '\0' && *p2 != '\0' && *p1 == *p2)
。
- 错误写法:
-
返回指针错误:
- 错误:内层循环中直接返回
p1
而非str1
(str1
是匹配起点,p1
是内层循环后移后的指针)。 - 正确:匹配成功时,应返回外层循环记录的起点
str1
,而非内层循环中的临时指针p1
。
- 错误:内层循环中直接返回
五、拓展知识:字符串匹配算法
本题实现的是 暴力匹配法(Brute-Force),时间复杂度为 O(n∗m)(n 是 str1
长度,m 是 str2
长度)。实际开发中,若追求效率,可使用以下优化算法:
- KMP 算法:通过预处理
str2
生成部分匹配表,避免重复比较已匹配的字符,时间复杂度优化至 O(n+m)。 - Boyer-Moore 算法:从右往左比较字符,利用坏字符规则和好后缀规则跳跃式移动指针,适合在长文本中搜索短模式串。
对于嵌入式开发,若对性能要求不高,暴力匹配法已足够简洁高效;若处理大规模数据,需根据场景选择更优算法。
六、总结
实现 strstr
需掌握以下核心能力:
- 字符串基本操作:指针遍历、
'\0'
结束符判断。 - 边界条件处理:空字符串、长度差异等特殊情况。
- 双重循环逻辑:外层确定起点,内层逐个匹配字符。
通过本题,新手可深入理解字符串匹配的底层逻辑,为嵌入式开发中的文本处理、协议解析等场景打下基础。实际编码时,需注意内存越界风险,确保输入参数的有效性(如指针非空),必要时添加错误检查代码。
通过以上题目解析,就可系统掌握嵌入式面试常考知识点,注意易错点,结合拓展内容深化理解,逐步提升嵌入式开发能力。