(嵌入式八股)No.1 C语言(3)

3.1 指针(重点、重中之重)

指针是 C 语言中一个非常强大且灵活的特性,它允许程序直接操作内存地址。指针可以用于多种用途,包括动态内存分配、访问数组元素、实现数据结构(如链表和树)等。

指针是一个变量,其存储的内容是另一个变量的地址。(好了,你知道指针就是一个地址就学完了(bushi))哈哈哈哈哈开玩笑呢

推荐一本书《C和指针》!!!

int a = 10;
int *p = &a;  // p 中保存着 a 的地址
a:普通变量,存放的是值 10;
p:指针变量,存放的是 a 的地址;
*p:解引用操作,取出 p 指向的地址中保存的值(即 a 的值)。

指针的四层关系

表达式

含义

a

变量本身(值)

&a

变量的地址

p

指针变量本身(保存地址)

*p

指针指向的值(间接访问)

指针声明和类型

int    *p1;   // 指向 int
char   *p2;   // 指向 char
float  *p3;   // 指向 float
void   *p4;   // 通用指针(不能直接解引用)
 指针类型决定了解引用后的数据解释方式(即:从内存中取多少字节、如何解释)。

必考:指针与数组(面试也会问)

指针与数组的区别?( easy)

  1. 数组名是常量(指向固定内存),不能修改;
  2. 指针是变量,可以指向别的地址;
  3. sizeof(arr) 是整个数组大小;
  4. sizeof(ptr) 是指针大小(通常 4 (32 位系统)或 8 字节(64 位系统))。

一维数组与指针

intarr[3] = {10, 20, 30};
int *p = arr;  // arr 等价于 &arr[0]
printf("%d\n", *(p+1)); // 输出 20
  1. 关系:问的比较多的就是指针和数组的关系!
  2. arr → 数组首元素地址&arr[0] → 数组首元素地址
  3. *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 = 0A ^ 0 = A

void swap(int *a, int *b) {
    *a ^= *b;
    *b ^= *a; // 此时 b 变成了原来的 a
    *a ^= *b; // 此时 a 变成了原来的 b
}

判断一个数是不是 2 的幂次方

2 的幂次方二进制只有一个 1 (如 01001000)。 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)。

链接:将多个目标文件和库文件合并为一个可执行文件。

#八股##开工第一帖#
泻湖花园嵌入式Offer指南 文章被收录于专栏

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

全部评论

相关推荐

评论
2
4
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务