题目:分析下列函数的功能
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
指向的字符串到 b
指向的空间,直到遇到 '\0'
。例如,若 a
指向 "hello"
,b
指向一块足够大的空闲内存,函数执行后 b
指向的空间内容会变为 "hello"
。因此,选项 A 正确。b
指针的指向,而是修改 b
指针指向的内存空间的内容。假设 b
初始指向地址 0x1000
,函数执行过程中 b
会逐步后移(如 0x1000
→ 0x1001
→ …),但这是为了复制内容,而非让 b
指向 a
的字符串。选项 B 错误。*b = *a
是赋值(=
),而比较操作应为 *b == *a
(==
)。因此,函数不是在比较两个字符串。选项 C 错误。'\0'
,但目的是通过判断复制的字符是否为 '\0'
来结束循环(即复制完整个字符串),而非检查 a
和 b
中是否有 '\0'
。选项 D 错误。总结:该函数的核心是 字符串复制,将 a
所指字符串赋给 b
所指空间,正确答案为 A。
通过这道题,需掌握:
*
取值、++
指针移动)。'\0'
的作用。=
)和比较(==
)操作的区别。后续遇到类似题目,可先分析代码核心操作(如赋值、比较、移动等),再结合选项逐一排除。
题目:以下错误的定义语句是( ),值等于 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};
二维数组可视为 “表格” 或 “矩阵” 形式的数据结构,由多个一维数组组成。例如,一个 m
行 n
列的二维数组,类似有 m
行、每行含 n
个元素的表格。在 C 语言中,常用于存储需按行和列组织的数据,如矩阵、表格或字符网格等。
类型 数组名[行数][列数]
。
int arr[3][4];
,定义 3 行 4 列的整型二维数组,共 3×4 = 12
个元素。类型 数组名[][列数]
,通过初始化数据确定行数。
int arr[][4] = { {1,2,3,4}, {5,6,7,8} };
,编译器根据初始化行数(2 行)确定第一维大小为 2。char x3[4][]
),编译器无法确定每行长度,导致编译错误。类型 数组名[行数][列数] = { {行 1 元素}, {行 2 元素},... };
。
int arr[2][3] = { {1,2,3}, {4,5,6} };
,明确为每行赋值。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 行。二维数组在内存中 按行优先顺序连续存储。例如,int arr[2][3] = { {1,2,3}, {4,5,6} }
,先存第 1 行 1,2,3
,再存第 2 行 4,5,6
。
&arr[i][j] = 数组首地址 + (i×列数 + j)×sizeof(类型)
。char x1[][3]
省略了第一维大小,但明确指定了第二维大小为 3。{ {'1'}, {'2'}, {'3','4','5'} }
中,每行元素个数都不超过 3,符合二维数组定义规则(第一维大小可根据初始化数据推断,第二维必须明确)。定义正确。char x2[4][3]
明确指定了第一维大小为 4,第二维大小为 3。{ {'6'}, {'7','8'}, {'9'} }
中,不足 4 行时,剩余行默认补 0,符合定义规则。定义正确。char x3[4][]
中,第二维大小被省略。char x4[][3]
省略了第一维大小,第二维大小为 3。查找值等于 0x38 的元素
{22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39}
,按每行 3 个元素划分:
总结:错误的定义语句是 C,值等于 0x38 的元素是 38(在选项 D 的数组中)。
本题要求深刻理解二维数组列数不可省略的规则、初始化方式及内存存储特点,这对嵌入式开发中数组的正确使用(如驱动开发的数据表格处理、应用层的矩阵运算等)至关重要。
题目:完成宏定义
#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 位数据,然后分析将特定值写入特定地址后读取的结果。
SET_U32_VALUE
宏定义
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)
:通过这个指针来访问对应的内存地址并读取一个字节的数据。0x20000123
通常是 STM32 的 SRAM 地址范围(SRAM 的起始地址一般是 0x20000000
),在正常情况下,这个地址是可以进行读写操作的,所以一般不会产生 hardfault。但如果这个地址被硬件或者软件配置为受保护的区域,那么写入操作就可能会触发 hardfault。这里我们假设该地址是可读写的,不会产生 hardfault。value
的值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)
:通过这个指针访问对应的内存地址并读取一个字节的数据。字节序是指多字节数据在内存中存储时字节的排列顺序,常见的字节序有小端字节序(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
。
在嵌入式开发中,这些知识是非常重要的,尤其是在进行底层驱动开发、内存管理等方面。通过本题的练习,新手可以更好地掌握这些知识点,为后续的开发打下坚实的基础。
综上所述,答案依次为:*(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
。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;
}
str2
为空字符串:根据 C 语言标准(如 man strstr
),当 str2
是空字符串时,应返回 str1
的首地址。代码中通过 if (*str2 == '\0')
判断,直接返回 str1
,避免后续无效比较。str1
长度小于 str2
:若 str1
比 str2
短,内层循环会因 *p1
先到达 '\0'
而退出,最终返回 NULL
,无需额外判断。str1
的每个字符,确定匹配起点(str1
指针每次后移一位)。str1
和 str2
:
*p1 == *p2
),p1
和 p2
同时后移。*p1 == '\0'
或 *p2 == '\0'
),内层循环退出。'\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
长度)。实际开发中,若追求效率,可使用以下优化算法:
str2
生成部分匹配表,避免重复比较已匹配的字符,时间复杂度优化至 O(n+m)。对于嵌入式开发,若对性能要求不高,暴力匹配法已足够简洁高效;若处理大规模数据,需根据场景选择更优算法。
实现 strstr
需掌握以下核心能力:
'\0'
结束符判断。通过本题,新手可深入理解字符串匹配的底层逻辑,为嵌入式开发中的文本处理、协议解析等场景打下基础。实际编码时,需注意内存越界风险,确保输入参数的有效性(如指针非空),必要时添加错误检查代码。
通过以上题目解析,就可系统掌握嵌入式面试常考知识点,注意易错点,结合拓展内容深化理解,逐步提升嵌入式开发能力。