嵌入式C语言笔面试题
·
1. const 关键字
// 这两种写法完全等效,都表示a是一个不可修改的整型常量。通常推荐使用第一种写法(const int)。
const int a; // a是一个整形常量
int const a; // a是一个整形常量
// 指针a可以修改(指向其他地址)
// 但不能通过a修改它指向的值(值是不可变的)
const int *a; // a是一个指向整型常量的常量指针
// 指针a本身不可修改(必须初始化且不能改变指向)
// 但可以通过a修改它指向的值
int * const a; // a是一个指向整型变量的指针常量
// 指针a本身不可修改
// 也不能通过a修改它指向的值
// 必须初始化(因为指针本身是const)
int const * const a = &b; // a是一个指向整型常量的指针常量
// 技巧:
// const在*左侧:指向的内容是常量(常量指针)
// const在*右侧:指针本身是常量(指针常量)
// 两边都有const:指针和指向的内容都是常量
2. 一个指针可以是volatile吗?
// ptr 本身是 volatile,意味着指针的地址可能被外部修改(例如硬件或中断)。
// 每次访问 ptr 时,编译器必须重新读取它的值,不能优化掉访问。
// 适用场景:
// 在多线程环境中,指针可能被其他线程修改。
// 在嵌入式系统中,指针可能指向内存映射的硬件寄存器,其地址可能被硬件改变。
3. 计算机底层运算
// 几乎所有的现代CPU和编程语言都基于补码进行整数运算
// 统一加减法:补码的加减法可以直接使用相同的硬件电路(无需区分正负数),简化CPU设计。
例如:A - B 可以转换为 A + (-B),其中 -B 直接用补码表示。
// 零的唯一性:补码中只有一个零(000...0),避免了原码中“+0”和“-0”的问题。
// 溢出处理自然:计算结果超出位数时,直接截断高位即可得到正确结果(符合模运算规则)。
// 正数:与原码相同(最高位为0,其余为二进制值),最高位为 0 是符号位
// 负数:
1. 取绝对值的二进制表示(原码);
2. 按位取反(反码),其中最高位是符号位为 1
3. 加1(得到补码)。
加法:直接按位相加,忽略最高位进位。
例如:5 + (-3)(8位补码):
5 → 00000101
-3 → 11111101
相加:00000101 + 11111101 = 00000010(结果为2,正确)。
减法:转换为加法(A - B = A + (-B))。
例如:7 - 5 → 7 + (-5):
-5 的补码:11111011
计算:00000111 + 11111011 = 00000010(结果为2)。
// 注意!!!!!!!!!
// 当运算表达式中存在有符号数和无符号数时,有符号数隐式转换成了无符号数(即底层的补码不变,但是此数从有符号数变成了无符号数)
// 将有符号数转换为无符号数时,并不是简单地“将最高位置0”,而是直接复用补码的二进制位模式,将其重新解释为无符号数。
// 有符号解释:0xFFFFFFFF → -1(最高位是符号位,表示负数)。
// 无符号解释:0xFFFFFFFF → 4294967295(最高位是数值的一部分)。
4. 内存对齐
// 对齐访问:数据存储在内存地址上,其地址是数据大小的整数倍。
例如,int32_t(4字节)对齐访问时,地址必须是 0, 4, 8, 12, ...(即 addr % 4 == 0)。
// 未对齐访问:数据跨越两个对齐的内存块。
例如,int32_t 存储在地址 3,则它占用了 3-6,跨越了 0-3 和 4-7 两个 4 字节对齐块。
5. 结构体对齐
// 1. 结构体变量的起始地址必须是其最宽基本类型成员大小的整数倍
// 2. 每个成员相对结构体起始地址的偏移量必须是该成员大小的整数倍
// 3. 结构体的总大小必须是其最宽基本类型成员大小的整数倍
// (1) 成员对齐规则
每个成员的起始地址 必须是其自身大小(或编译器指定的对齐值)的整数倍。
例如:int(4字节)的地址必须是 0, 4, 8, ...(即 addr % 4 == 0)。
double(8字节)的地址必须是 0, 8, 16, ...(addr % 8 == 0)。
// (2) 结构体总大小
必须是所有成员中最严格对齐值的整数倍(即最大对齐值的倍数)。
例如:若结构体包含 char(1字节)和 double(8字节),则总大小需对齐到 8 的倍数。
// (3) 填充字节(Padding)
编译器在成员之间或结构体末尾插入空白字节,以满足对齐要求。
struct Example1 {
char a; // 1字节 (地址 0)
int b; // 4字节 (需对齐到4的倍数,地址 4)
char c; // 1字节 (地址 8)
}; // 总大小:9字节 → 填充到12(对齐到4)
// 内存布局(假设起始地址为 0):
Offset 0: [a] (1字节)
Offset 1-3: [padding] (填充3字节)
Offset 4-7: [b] (4字节)
Offset 8: [c] (1字节)
Offset 9-11: [padding] (填充3字节,使总大小为12)
// 总大小:12 字节(对齐到 4)。
// 嵌套结构体
struct Inner {
double d; // 8字节 (需对齐到8)
char e; // 1字节 (地址 8)
}; // 总大小:16字节(对齐到8)
struct Outer {
int f; // 4字节 (地址 0)
Inner inner; // 需对齐到8 → 起始地址8
char g; // 1字节 (地址 24)
}; // 总大小:32字节(对齐到8)
// 内存布局:
Offset 0-3: [f] (4字节)
Offset 4-7: [padding] (填充4字节)
Offset 8-15: [inner.d] (8字节)
Offset 16: [inner.e] (1字节)
Offset 17-23: [padding] (填充7字节,使Inner对齐到8)
Offset 24: [g] (1字节)
Offset 25-31: [padding] (填充7字节,使Outer对齐到8)
// 总大小:32 字节(Inner 本身对齐到 8,Outer 也需对齐到 8)。
// 控制对齐的方式
// (1) 编译器指令
// #pragma pack(n):强制按 n 字节对齐(可能牺牲性能)。
#pragma pack(1) // 取消填充,紧密排列
struct Tight {
char a;
int b;
}; // 大小:5字节(无填充)
#pragma pack() // 恢复默认对齐
// (2) C++11 alignas
struct Aligned {
alignas(16) int a; // 强制16字节对齐
char b;
}; // 总大小:32字节(对齐到16)
6. 运算顺序
// c = a + b > 0 ? 1 : 2;
// 运算符优先级(从高到低)
+(加法)
>(大于比较)
?:(三元条件运算符)
=(赋值)
在C/C++中,表达式 c = a + b > 0 ? 1 : 2; 的运算顺序由运算符优先级和结合性决定。以下是逐步解析:
1. 运算符优先级(从高到低)
+(加法)
>(大于比较)
?:(三元条件运算符)
=(赋值)
// 运算顺序分解
步骤1:计算 a + b
步骤2:比较 (a + b) > 0
步骤3:执行三元运算符 ?:
步骤4:赋值给 c
// c = ((a + b) > 0) ? 1 : 2;
7. 返回值
// 1. 可以返回局部变量
return value; 会将 value 的值复制到调用者的接收变量中,value 的生命周期仅限于函数执行期间,但它的 值 在 return 时已经被复制出去,因此调用者可以安全使用。
// 2. 不能返回局部变量的地址
如果函数返回的是 局部变量的地址(指针),则会导致 未定义行为(Undefined Behavior),因为函数结束后局部变量的内存会被回收。
// 调用者获取的指针指向已释放的栈内存,访问会导致崩溃或数据错误。
unsigned char* invalid_reverse(unsigned char input) {
unsigned char result = 0;
// ... 计算逻辑 ...
return &result; // 错误!返回局部变量的地址
}
8. 位域
// 1. 位域的定义与作用
位域:允许将结构体成员定义为特定位数(如 a:1 表示 a 占 1 比特),用于节省内存。
语法:type member_name : bit_width;
例如:unsigned int a:1; 表示 a 是无符号整型,仅占 1 比特。
// 2. 结构体 A 的位域布局
struct A {
unsigned int a:1; // 1 bit
unsigned int b:3; // 3 bits
unsigned int c:2; // 2 bits
};
总位数:1 + 3 + 2 = 6 bits。
内存对齐规则:
位域按声明顺序紧密排列,但不能跨越其底层类型(unsigned int)的边界。
unsigned int 的大小通常是 4 字节(32 位),因此所有位域必须容纳在一个 unsigned int 内。
// 3. 内存布局示意图
假设从低地址到高地址排列:
| a (1 bit) | b (3 bits) | c (2 bits) | 未使用的填充位 (26 bits) |
总占用:6 比特有效数据 + 26 比特填充 = 32 比特(4 字节)。
// 4.1 位域总位数超过底层类型
// sizeof(B):8(两个 unsigned int,共 8 字节)。
struct B {
unsigned int a:20;
unsigned int b:20; // 超出 32 位,b 必须另起一个 unsigned int
};
// 4.2 混合类型位域
struct A {
char t:4; // 4位位域
char k:4; // 4位位域
unsigned short i:8; // 8位位域
unsigned long m; // 普通变量
};
// 位域必须存储在 同一类型 的存储单元中(如 char 位域不能和 int 位域混用)。
// 必须是 所有成员对齐值的最大值的整数倍。
// 位域(Bit-field)的存储起始地址不需要保证是该位域类型的整数倍
1字节(t;k) | 2字节(i) | 1字节(空闲) 4字节(m)
struct C {
unsigned int a:1;
unsigned char b:3; // 底层类型变为 unsigned char
unsigned int c:2;
};
// 严格编译器(如 GCC/Clang):不同底层类型的位域 不能共享同一存储单元,即使总位数看似可压缩。
// a:1(unsigned int)和 b:3(unsigned char)属于不同类型,必须分开存储。
// c:2(unsigned int)需另起一个 unsigned int 单元。
9. 数组指针 & 函数声明分析
// 请指出如下语句的含义
void *(*(p1)(int))[10];
float (*(*p2)(int,int,float))(int);
typedef double (*(*(*p3)())[10])();
int (*(*p4())[10])();
// 重点: “从内到外” 和 “右左法则”(The Right-Left Rule)
// 参考:
int (*p1)[10]; // p1 是指向含10个int的数组的指针
int *p2[10]; // p2 是含10个int指针的数组
int (*fp1)(int); // fp1 是指向函数的指针,函数参数为int
int *(*fp2)(int*); // fp2 是指向函数的指针,该函数参数为int*,返回int*
// 1. void *(*(p1)(int))[10];
内:从 p2 开始:p2 是一个标识符。
向右看 (int, int, float):p2 是一个函数指针,接受 (int, int, float) 参数。
向左看 *:该函数返回指针。
向右看 (int):返回的指针指向一个函数(接受 int 参数)。
向左看 float:该函数返回 float。
结论:p1 是一个函数:参数:int,返回值:指针,指向 void *[10](即 void* 数组,长度为 10)。
// 2. float (*(*p2)(int,int,float))(int);
内:从 p2 开始:p2 是一个标识符。
向右看 (int, int, float):p2 是一个函数指针,接受 (int, int, float) 参数。
向左看 *:该函数返回指针。
向右看 (int):返回的指针指向一个函数(接受 int 参数)。
向左看 float:该函数返回 float。
结论:p2 是一个函数指针:参数:(int, int, float),返回值:另一个函数指针(该函数指针接受 int 参数,返回 float)。
// 3. typedef double (*(*(*p3)())[10])();
内:从 p3 开始:p3 是一个标识符。 *p3
向右看 ():p3 是一个函数指针,无参数。 (*p3)()
向左看 *:该函数返回指针。 *(*p3)()
向右看 [10]:返回的指针指向 [10] 数组。 (*(*p3)())[10]
向左看 *:数组的每个元素是指针。 *(*(*p3)())[10]
向右看 ():每个元素是函数指针,无参数。 (*(*(*p3)())[10])()
向左看 double:函数返回 double。 double (*(*(*p3)())[10])()
结论:p3 是一个函数指针:参数:void(无参数)返回值:指针,指向 [10] 数组,数组的每个元素是 函数指针(返回 double)。
// 4. int (*(*p4())[10])();
内:从 p4 开始:p4 是一个标识符。
向右看 ():p4 是一个函数,无参数。
向左看 *:该函数返回指针。
向右看 [10]:返回的指针指向 [10] 数组。
向左看 *:数组的每个元素是指针。
向右看 ():每个元素是函数指针,无参数。
向左看 int:函数返回 int。
结论:p4 是一个函数:参数:void(无参数),返回值:指针,指向 [10] 数组,数组的每个元素是函数指针(返回 int)。
10. 单精度浮点数在内存中的十六进制表示
// 步骤 1:转换为二进制科学计数法
确定符号位(Sign):
负数 → 符号位 S = 1。
转换绝对值为二进制:
12.75 的二进制:
整数部分:12 → 1100
小数部分:0.75 → 0.11(因为 0.5 + 0.25 = 0.75)
合并:1100.11
规范化科学计数法:
移动小数点:1100.11 → 1.10011 × 2³
有效数(Mantissa):1.10011(隐含前导1)
指数(Exponent):3(需加上偏移量 127 → 130)
尾数:10011
/*
重点!小数部分的二进制转换方法:乘2取整法
1. 将小数部分 F 乘以2,得到乘积 P。
2. 记录 P 的整数部分(0或1),作为二进制的一位。
3. 用 P 的小数部分作为新的 F。
4. 重复上述过程,直到 F=0 或达到指定精度。
示例:将 0.625 转换为二进制
0.625×2=1.25 → 整数部分 1,小数部分 0.25,二进制:0.1
0.25×2=0.5 → 整数部分 0,小数部分 0.5,二进制:0.10
0.5×2=1.0 → 整数部分 1,小数部分 0.0(终止),二进制:0.101
结果:0.625=0.101
*/
// 步骤 2:计算IEEE 754字段
指数计算:实际指数要在指数的基础上加127
实际指数:3 → 130(3 + 127) → 二进制:10000010
尾数计算:尾数就是小数部分
规范化后的有效数:1.10011(忽略前导1,保留小数部分) → 10011
补零到23位:10011000000000000000000
// 步骤 3:合并为32位二进制
1 10000010 10011000000000000000000
// 步骤 4:转换为十六进制
每4位一组:1100 0001 0100 1100 0000 0000 0000 0000
十六进制:0xC14C0000
11. 数组
// 1. 普通指针 +1(跳过单个元素)
void main(void) {
char a[] = "SF-TECH";
a++;// 错误:数组名是常量指针,不能修改,编译器会报错:error: lvalue required as increment operand
printf("%s", a);
}
// a 是数组名,代表数组的首地址,类型为 char *const(常量指针),不能进行 a++ 这样的修改。
// 不仅是字符串数组不允许 a++,整型数组同样不允许!!!
// 改用指针(如 char *p = a; p++;printf("%s", p);)或直接偏移(printf("%s", a + 1);)
// 输出 F-TECH(跳过首字符 'S'),p + 1 移动 sizeof(char) 字节
// 2. 数组指针 +1(跳过整个数组)
int arr[3][2] = {{1, 2}, {3, 4}, {5, 6}};
int (*p)[2] = arr; // p 是「指向含 2 个 int 的数组」的指针,数组指针
p++; // p 跳过整个子数组 {1, 2},指向 {3, 4}
// p + 1 移动 sizeof(int[2]) 字节(即 8 字节,假设 int 为 4 字节)。
// 3. 二维数组的指针跳跃
int arr[3][2] = {{1, 2}, {3, 4}, {5, 6}};
int (*p)[2] = arr; // p 指向 arr[0](即 {1, 2})
// 在 C/C++ 中,[](数组下标)的优先级高于 *(解引用)。因此:
// *p[0] 等价于 *(p[0]),即先取 p[0],再解引用。
printf("初始指向: %d\n", p[0]); // 输出 -522695216(地址),相差8个字节两个int类型
printf("初始指向: %d\n", p[1]); // 输出 -522695208(地址)
printf("初始指向: %d\n", p[2]); // 输出 -522695200(地址)
printf("初始指向: %d\n", *p); // 输出 -522695216(地址)
printf("初始指向: %d\n", *p[0]); // 输出 1
printf("初始指向: %d\n", *p[1]); // 输出 3
printf("初始指向: %d\n", *p[2]); // 输出 5
printf("初始指向: %d\n", **p); // 输出 1(arr[0][0])
p++; // 跳过 {1, 2}
printf("跳过整个子数组后: %d\n", **p); // 输出 3(arr[1][0])
// 4. 一维数组的指针跳跃
int arr[5] = {10, 20, 30, 40, 50};
int (*p)[5] = &arr; // p 是「指向整个数组的指针」
printf("初始指向: %d\n", arr); // 输出 -1359123840(地址)
printf("初始指向: %d\n", p); // 输出 -1359123840(地址)
printf("初始指向: %d\n", p[0]); // 输出 -1359123840(地址)
printf("初始指向: %d\n", (*p)[0]); // 输出 10
printf("初始指向: %d\n", *p[0]); // 输出 10
printf("初始指向: %d\n", *p[0]); // 输出 10
printf("初始指向: %d\n", *p[1]); // 跳过整个 arr[5],越界访问,输出 32765
p++; // 跳过整个 arr[5](危险操作!)
printf("跳过整个数组后: %d\n", (*p)[0]); // 未定义行为(越界访问),输出 32765
12. 字符串数组指针
[]优先级要高于*:数组优先级高于解引用
char *a[] = {"BEIJING", "SHENZHEN", "SHANGHAI", "GUANGZHOU"};
char **pa[] = {a+3, a+2, a+1, a};// GUANGZHOU, SHANGHAI, SHENZHEN, BEIJING
char ***ppa = pa;
int main(void) {
printf("%s, %s, %s, %s \n", **(ppa), **(ppa+1), **(ppa+2), **(ppa+3));// GUANGZHOU, SHANGHAI, SHENZHEN, BEIJING
printf("%s \n", a[0]);// BEIJING
printf("%x, %x, %x \n", ppa, pa, &pa[0]);// b1c44040, b1c44040, b1c44040
printf("\n");
printf("%s, ", **++ppa); // SHANGHAI
printf("%s, ", *--*++ppa+3);// JING
printf("%s, ", *ppa[-2]+3);// NGZHOU
printf("%s", ppa[-1][-1]+1);// HENZHEN
printf("\n");
内存示意图

解析
// 最重要的是参考上图内存示意图!!!!!!!!!!
// 可知,指针存放的是数组的地址;数组名即为数组地址,第 0 个元素取址也为数组地址!!!
printf("%x, %x, %x \n", ppa, pa, &pa[0]);// b1c44040, b1c44040, b1c44040
// 可知,字符串数组中的各元素为字符串
printf("%s \n", a[0]);// BEIJING
// 由ppa = pa = &pa[0] -> ++ppa == &pa[1] -> *++ppa = *&pa[1] == pa[1] ->
// **++ppa = *pa[1] = *&a[2] == a[2] == SHANGHAI
printf("%s, ", **++ppa); // SHANGHAI
// 由上,此时ppa == &pa[1] -> ++ppa == &pa[2] -> *++ppa = pa[2] == &a[1] ->
// --*++ppa == &a[0] -> *--*++ppa == a[0] -> *--*++ppa+3 == a[0]+3 == JING
// 注意a[0]等存放的是字符指针地址
printf("%s, ", *--*++ppa+3);// JING
// 数组访问 arr[i] 本质上是 指针算术 的语法糖,等价于 *(arr + i)。因此:
arr[-1] → *(arr - 1)
arr[-n] → *(arr - n)
// 由上,此时ppa == &pa[2] -> ppa[-2] == *(ppa-2) == *(&pa[2]-2) == *&pa[0] == pa[0]
// -> *ppa[-2] == *pa[0] == a[3] -> a[3]+3 == NGZHOU
printf("%s, ", *ppa[-2]+3);// NGZHOU
// 由上,此时ppa == &pa[2] -> ppa[-1] == *(ppa-1) == *(&pa[2]-1) == *&pa[1] == pa[1]
// ->ppa[-1][-1] == pa[1][-1] == *(pa[1]-1) == *(&a[2]-1) == *&a[1] == a[1]
// ppa[-1][-1]+1 == a[1]+1 == HENZHEN
// 注意:在 pa[1][-1] == *(pa[1]-1)中,不要想当然pa[1][-1] == *(&pa[1]-1),就按照上述规则来!!!!
printf("%s", ppa[-1][-1]+1);// HENZHEN
注意区分:

// 1. 内存布局:
// 每个字符串常量(如 "Hello")存储在只读内存的独立位置。
// strs 数组的每个元素(strs[0]、strs[1] 等)是指向这些字符串的指针。
// 2. 访问方式:
// strs[i] 返回第 i 个字符串的地址(char* 类型)。
// strs[i][j] 访问第 i 个字符串的第 j 个字符。
char *strs[] = {"Hello", "World", "C"};
// 1.内存布局:
// 所有字符串按行紧密排列在连续内存中,每行固定 6 字节(包括 '\0')。
// 不足部分用 '\0' 填充(如 "C" 存储为 'C', '\0', '\0', ...)。
// 2. 访问方式:
// strs[i] 返回第 i 行的首地址(类型为 char[6],可退化为 char*)。
// strs[i][j] 访问第 i 行第 j 列的字符。
char strs[3][6] = {"Hello", "World", "C"};
13. 函数指针
// 函数位于0x20000000处,输入参数为int类型,输出为void类型,调用该函数代码正确的是?
// * 解引用操作符在此场景是多余的,但语法上允许((*ptr)(arg) 和 ptr(arg) 等效)。
typedef void (*func)(int); func a = (func)0x20000000; a(); (*a)();//解引用
((void (*)(int))0x20000000)()
(*(void (*)(int))0x20000000)(123); // 解引用后调用,语法正确但冗余
14. sizeof
#include <stdio.h>
#include <stdlib.h>
void Foo(char str[100]) {
printf("%ld\n", sizeof(str)); // 输出指针大小
}
int main() {
char str[] = "http://localhost"; // 长度 16
char *p1 = str;
void *p2 = malloc(100);
int n = 10;
struct A {
int n;
char str[10];
} struct_a;
printf("%ld\n", sizeof(str)); // 17(字符串常量初始化,包含 \0)
printf("%ld\n", sizeof(p1)); // 4(指针大小,32 位系统
printf("%ld\n", sizeof(p2)); // 4(void 指针大小)
printf("%ld\n", sizeof(n)); // 4(int 类型)
printf("%ld\n", sizeof(struct_a)); // 16(结构体包含对齐)
Foo(str); // 4(函数参数退化为指针)
free(p2);
return 0;
}
// sizeof 是编译时运算符,返回对象或类型的大小(字节数)。
// 编译时运算符(Compile-Time Operator),这意味着它的值在 代码编译期间 就已经被计算确定,而不是在程序运行时动态计算。
// 在 C/C++ 中,当数组作为函数参数传递时,它会自动退化为指向其首元素的指针。
// 即使你声明为 char str[100],函数内部实际接收到的是 char* 类型的指针,而非真正的数组。
// 内存效率:避免复制大数组(如 int arr[10000])带来的性能开销。
// 以下三种声明完全等价(编译器视为同一函数):
void Foo(char str[100]); // 看似数组,实为指针
void Foo(char str[]); // 不指定大小,仍是指针
void Foo(char *str); // 直接声明为指针
15. 存储位置
注意!在main函数中的变量也是局部变量,只有在函数之外(包括主函数)定义的变量才是全局变量!

char str1[] = "abc";
char str2[] = "abc";
const char str3[] = "abc";
const char str4[] = "abc";
const char* str5 = "abc";
const char* str6 = "abc";
// false abc abc
std::cout << boolalpha << ( str1 == str2 ) <<' ' << str1 << ' ' << str2 << std::endl;
// false abc abc
std::cout << boolalpha << ( str3 == str4 ) <<' ' << str3 << ' ' << str4 << std::endl;
// true abc abc
std::cout << boolalpha << ( str5 == str6 ) <<' ' << str5 << ' ' << str6 << std::endl;
// 1. const char[](栈内存):
// 定义 const char str3[] = "abc"; 时,编译器会在栈上创建一个长度为 4 的数组(含 \0),并复制字符串 "abc" 到该数组。
// 2. const char*(常量区)
// 定义 const char* str5 = "abc"; 时,字符串字面量 "abc" 存储在只读数据段(.rodata)。所有相同的字符串字面量可能被编译器优化为同一地址(字符串池化)。
// 3. 关于str为何输出内容而不是地址?
// C 语言中,char* 通常表示字符串,printf("%s", str) 会输出内容而非地址。
// C++ 继承了这一行为,使 std::cout << str 也输出字符串内容。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)