(嵌入式八股)No.1 C语言(3)
3.1 指针(重点、重中之重)
指针是 C 语言中一个非常强大且灵活的特性,它允许程序直接操作内存地址。指针可以用于多种用途,包括动态内存分配、访问数组元素、实现数据结构(如链表和树)等。
指针是一个变量,其存储的内容是另一个变量的地址。(好了,你知道指针就是一个地址就学完了(bushi))哈哈哈哈哈开玩笑呢
推荐一本书《C和指针》!!!
int a = 10; int *p = &a; // p 中保存着 a 的地址 a:普通变量,存放的是值 10; p:指针变量,存放的是 a 的地址; *p:解引用操作,取出 p 指向的地址中保存的值(即 a 的值)。
指针的四层关系
表达式 | 含义 |
| 变量本身(值) |
| 变量的地址 |
| 指针变量本身(保存地址) |
| 指针指向的值(间接访问) |
指针声明和类型
int *p1; // 指向 int char *p2; // 指向 char float *p3; // 指向 float void *p4; // 通用指针(不能直接解引用) 指针类型决定了解引用后的数据解释方式(即:从内存中取多少字节、如何解释)。
必考:指针与数组(面试也会问)
指针与数组的区别?( easy)
- 数组名是常量(指向固定内存),不能修改;
- 指针是变量,可以指向别的地址;
- sizeof(arr) 是整个数组大小;
- sizeof(ptr) 是指针大小(通常 4 (32 位系统)或 8 字节(64 位系统))。
一维数组与指针
intarr[3] = {10, 20, 30};
int *p = arr; // arr 等价于 &arr[0]
printf("%d\n", *(p+1)); // 输出 20
- 关系:问的比较多的就是指针和数组的关系!
- arr → 数组首元素地址&arr[0] → 数组首元素地址
- *p → 等价于 arr[0]*(p+i) → 等价于 arr[i]
二维数组与指针
int a[2][3] = {{1,2,3},{4,5,6}};
int (*p)[3] = a; // p 指向含 3 个 int 的一维数组
printf("%d\n", *(*(p+1)+2)); // 输出 6
说明:p 是一个“指向长度为 3 的 int 数组”的指针;
p+1 指向下一行;*(p+1) 是该行的首地址;
*(p+1)+2 指向该行的第 3 个元素。
指针与函数
指针作函数参数(传址调用)
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
指针函数(这个面试会和函数指针一起考)
指针函数是一个返回值为指针的函数。它的返回值是一个指针类型。
int* func()
{
static int x = 10;
return &x;
}
函数指针
函数指针是一个指针变量,它指向一个函数的入口地址。通过函数指针,可以像调用普通函数一样调用被指向的函数。
int add(int a, int b) { return a + b; }int (*pf)(int, int) = add;printf("%d\n", pf(3,4)); // 输出 7
函数指针可以用于 回调函数、事件处理(动态选择函数:根据条件动态调用不同的函数)、函数表:将多个函数指针存储在数组中,通过索引调用不同的函数 等。
野指针问题
什么是野指针
(1)野指针就是指向未知位置的指针。
(2)指针局部变量未初始化会造成野指针,使用 free 后没有将指针置为 NULL 也会造成野指针。
野指针的危害
(1)指向不可访问的地址,譬如内核空间:触发段错误。
(2)指向一个可访问的、没什么意义的地址,譬如空闲栈空间:不会触发明显错误,但会留下安全隐患。
(3)指向一个访问的被程序使用的地址,譬如说一个变量:导致数据被损害或者程序崩溃,危害最大。
怎样避免野指针
(1)定义指针时初始化为 NULL 或者绑定一个可用地址空间。
(2)指针使用完之后,将其赋值为 NULL。
#include <stdio.h>
int main() {
int *ptr; // 未初始化,此时 ptr 是一个野指针(指向随机地址)
int *ptr = NULL; // 必须初始化
// 尝试访问野指针指向的内存(危险操作!)
printf("%d\n", *ptr); // 未定义行为:可能打印随机值、崩溃或产生其他不可预料的结果
return 0;
}
/*释放后未置空 */
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int*)malloc(sizeof(int));
*ptr = 42;
free(ptr); // 内存已释放,但 ptr 仍指向原地址(此时成为野指针)
// 错误:继续使用已释放的指针 应该先 ptr = NULL ;
printf("%d\n", *ptr); // 未定义行为
return 0;
}
3.2 位运算
(有的面试官会考察,或者笔试题里面会出现)(芯片厂最喜欢考了)
位操作符
1.位与&
(1)注意:位与是&,逻辑与是&&。举例 0xAA&0xF0=0xA0,0xAA && 0xF0=1
2.位或|
(1)注意:位或是 |,逻辑或是||。
3.位取反~
(1)注意:C 语言中位取反是~,逻辑取反是!.
4.位异或^
(1)0 或 1 与 1 位异或就会取反,0 或 1 与 0 位异或则不变。
5.位左移、位右移
(1)对于无符号数:
①左移时右侧补 0,相当于逻辑移位。
②右移时左侧补 0,相当于逻辑移位。
(2)对于有符号数:
①左移时右侧补 0,叫算术移位,相当于逻辑移位。
②有右移时左侧补符号位,正数补 0 负数补 1,叫算术移位。
(3)嵌入式中使用的移位都是无符号数移位。
&,|,^在操作寄存器时的特殊作用
1.寄存器的操作要求
(1)ARM 是内存与 IO 统一编制的(x86 则不是),读写寄存器就是操控硬件。
(2)如何做到设定特定位时不影响其它位?答案是:读-改-写三部曲。
2.特定位清零用&
3.特定位置 1 用|
4.特定位取反用^
#include <stdio.h>
#include <stdint.h> // 使用 uint32_t 类型
int main() {
// 模拟一个 32 位寄存器,初始值设为 0xFFFFFFFF(所有位为1)
uint32_t reg = 0xFFFFFFFF;
printf("初始寄存器值: 0x%08X\n", reg);
// 1. 特定位清零:将 bit3~bit5 清零(掩码 0x7 << 3 = 0x38)
printf("\n--- 清零 bit3~bit5 ---\n");
uint32_t val = reg; // 读
val &= ~(0x7 << 3); // 改:bit3~bit5 清零,其他位保持不变
reg = val; // 写
printf("操作后寄存器: 0x%08X\n", reg);
// 2. 特定位置1:将 bit7 和 bit9 置1
printf("\n--- 置1 bit7 和 bit9 ---\n");
reg |= (1 << 7) | (1 << 9); // 直接合并读-改-写
printf("操作后寄存器: 0x%08X\n", reg);
// 3. 特定位取反:将低4位取反(bit0~bit3)
printf("\n--- 取反低4位 ---\n");
reg ^= 0xF; // 异或掩码 0xF
printf("操作后寄存器: 0x%08X\n", reg);
// 4. 组合操作:先清零字段,再写入新值
printf("\n--- 设置 bit8~bit10 字段为 0x5 ---\n");
uint32_t field_value = 0x5; // 要写入的值(3位)
// 先清零 bit8~bit10,然后置入新值
reg = (reg & ~(0x7 << 8)) | ((field_value & 0x7) << 8);
printf("操作后寄存器: 0x%08X\n", reg);
// 5. 演示位带操作的思路(注释部分,实际位带需要硬件支持)
// 若在 Cortex-M 上,可定义宏原子操作单个位:
// #define BITBAND(addr, bit) ((volatile uint32_t*)((uint32_t)(addr) & 0xF0000000) + 0x02000000 + ((uint32_t)(addr) & 0xFFFFF) * 32 + (bit) * 4)
// *BITBAND(®, 5) = 1; // 原子置1 bit5
return 0;
}
如何用位运算构建特定二进制数
1.寄存器操作经常需要特定位给特定值(“改”的过程中)
(1)解法 1:用工具软件或自己大脑计算,直接给出 32 位特定数。
(2)解法 2:自己写代码用位操作符号来构建。
2.使用移位获取特定位为 1 的二进制数
(1)譬如需要一个 bit3~bit7 为 1 的二进制数:(0x1f << 3)。
(2)bit3~bit7 为 1,同时 bit23~25 也为 1:((0x1f << 3) | (7 << 23))。
3.结合取反获取特定位为 0 二进制数
(1)获取 bit4~bit10 为 0,其余为 1 的数:~(0x7f << 4)。
4.总结:“改”的过程用&、|、^结合特定二进制数即可完成
位运算实战演练
1.给定一个整形数 a,取出 a 的 bit3~bit8
(1)第一步:a &= (0x3f << 3);
(2)第二步:a >>= 3;
2.给定一个整形数 a,给 a 的 bit7~bit17 赋值 937
(1)第一步:a &= ~(0x7ff << 7); // 特定位置 0
(2)第二步:a |= (937 << 7); // 特定位赋值
3.给定一个整形数 a,给 a 的 bit7~bit17 中的值加 17
(1)第一步:b = a & (0x7ff << 7); b >>= 7; // 取出该值
(2)第二步:b += 17; // 加上 17
(3)第三步:a &= ~(0x7ff << 7); // 特定位清 0
(4)第四步:a |= (b << 7); // 特定位赋值
用宏定义来完成位运算
1. 得到指定地址上的一个字节或字
#define MEM_B(x) (*((char *)(x)))
#define MEM_W(x) (*((short *)(x)))
2. 将第 n 位置 1
#define BIT_SET(x, n) ((x) |= (1U << (n)))
3. 将第 n 位置 0
#define BIT_CLR(x, n) ((x) &= ~(1U << (n)))
4. 获取第 n 位的值
#define BIT_GET(x, n) (((x) >> (n)) & 1U)
5. 构造特定位掩码 (Mask)
// 例如:MASK(3) -> 000...0111 (即 7)
#define BIT_MASK(len) ((1U << (len)) - 1)
进阶
交换两个变量 (不使用临时变量) 笔试面试常考
利用异或的性质:A ^ A = 0 和 A ^ 0 = A。
void swap(int *a, int *b) {
*a ^= *b;
*b ^= *a; // 此时 b 变成了原来的 a
*a ^= *b; // 此时 a 变成了原来的 b
}
判断一个数是不是 2 的幂次方
2 的幂次方二进制只有一个 1 (如 0100, 1000)。 x - 1 会把最低位的 1 变成 0,并把后面的 0 变成 1。
// 如果 ((x & (x-1)) == 0) 且 x != 0,则 x 是 2 的幂
if ((x & (x - 1)) == 0) { ... }
统计二进制中 1 的个数 (Kernighan 算法)------小米手撕
x = x & (x - 1) 每执行一次,就会消除掉二进制中最右边的一个 1。
int count_set_bits(int n) {
int count = 0;
while (n > 0) {
n &= (n - 1); // 消除最右边的 1
count++;
}
return count;
}
3.3 由源码到可执行程序的过程
预处理(Preprocessing)
预处理是编译过程的第一步,由预处理器(如 cpp)完成。预处理器的主要任务是处理源码中的预处理指令(如 #include、#define、#ifdef 等)。
主要任务:
- 宏展开:将宏定义替换为实际内容。
- 文件包含:将 #include 指令指定的文件内容插入到当前文件中。
- 条件编译:根据 #ifdef、#ifndef、#if 等指令,决定是否包含某些代码段。
输出:预处理后的文件,通常以 .i 为扩展名。
编译(Compilation)
编译是将预处理后的文件转换为汇编语言的过程,由编译器(如 gcc)完成。编译器的主要任务是将高级语言转换为低级的汇编语言。
主要任务:
- 语法分析:检查代码的语法是否正确。
- 语义分析:检查代码的语义是否正确。
- 代码生成:生成汇编语言代码。
输出:汇编语言文件,通常以 .s 为扩展名。
汇编(Assembly)
汇编是将汇编语言文件转换为机器代码的过程,由汇编器(如 as)完成。汇编器的主要任务是将汇编语言转换为二进制形式的机器代码。
主要任务:
- 符号解析:解析汇编语言中的符号(如标签、变量名等)。
- 指令转换:将汇编指令转换为机器代码。
- 生成目标文件:生成目标文件,通常以 .o 为扩展名。
输出:目标文件(Object File),通常以 .o 为扩展名。
链接(Linking)
链接是将多个目标文件(.o 文件)和库文件(如标准库、第三方库等)合并为一个可执行文件的过程,由链接器(如 ld)完成。链接器的主要任务是解析符号引用,将多个目标文件中的代码和数据合并为一个完整的可执行文件。
主要任务:
- 符号解析:解析目标文件中的符号引用,确保符号的正确性。
- 地址分配:为每个目标文件分配内存地址。
- 生成可执行文件:生成最终的可执行文件,通常以 .exe(Windows)或无扩展名(Linux)。
输出:可执行文件(Executable File)。
总结
从源码到可执行程序的过程可以总结为以下步骤:
预处理:处理预处理指令,生成预处理后的文件(.i )。
编译:将预处理后的文件转换为汇编语言文件(.s)。
汇编:将汇编语言文件转换为目标文件(.o)。
链接:将多个目标文件和库文件合并为一个可执行文件。

从入门到上岸,一站式搞定求职! 本硕纯机械,无竞赛无论文,后转行嵌入式软件开发(因为课题组师哥转嵌入式拿到30Woffer之后狠狠心动),秋招最终收获35W+offer可以为27届或者28届的的UU们提供参考,可以关注一下!!!
