4、C语言基础回顾(下):函数、指针、结构体与联合体、内存管理(malloc/free)、位运算

好,咱们接着聊。上一节我们把C语言的骨架搭起来了,变量、流程控制这些基本盘都过了一遍。这一节,咱们要啃的才是真正让C语言「硬核」起来的东西——函数、指针、结构体,还有内存管理和位运算。

说实话,指针和内存管理是很多初学者的噩梦。但我个人觉得,只要你理解了「地址」这个概念,一切就豁然开朗了。你想想看,计算机里所有的数据都存在内存里,而内存就像一排带编号的柜子。指针,就是那个柜子的编号。

4.1 函数:把代码拆成积木

函数这东西,说白了就是把一段逻辑封装起来,给它起个名字,方便反复调用。我刚开始写代码时,喜欢把所有逻辑都塞在main函数里,结果代码又臭又长,自己都看不懂。

后来我养成了一个习惯:一个函数只做一件事。比如读取传感器数据、计算平均值、发送到串口,各写一个函数。这样调试起来特别方便。

// 一个简单的加法函数
int add(int a, int b) {
    return a + b;
}

int main() {
    int result = add(3, 5);
    printf("结果是:%d\n", result);
    return 0;
}
我的小技巧:函数命名要见名知意。别用f1、f2这种名字,三个月后你自己都看不懂。

4.2 指针:C语言的灵魂

指针,绝对是C语言里最让人又爱又恨的东西。爱它,因为它灵活、高效;恨它,因为一不小心就踩坑。

指针就是一个变量,它存的是另一个变量的地址。你可以通过指针间接访问那个变量。嗯,这里要注意:指针本身也有地址,指针也可以指向指针,这就是二级指针。

int a = 10;
int *p = &a;  // p指向a的地址

printf("a的值:%d\n", a);   // 直接访问
printf("a的值:%d\n", *p);  // 通过指针间接访问

我在项目中遇到过最经典的坑:指针没有初始化就使用。野指针会指向一个随机的内存地址,轻则程序崩溃,重则系统死机。所以我的习惯是:定义指针时立即初始化为NULL。

避坑指南:我曾经在一个电机控制项目里,因为一个野指针导致PWM输出异常,电机直接飞车了。从那以后,我每次用完指针都会把它置为NULL。

4.3 数组与指针的暧昧关系

数组名其实就是一个指针常量,指向数组的第一个元素。你想想看,为什么arr[2]和*(arr+2)是等价的?因为编译器会把arr[2]翻译成*(arr+2)。

int arr[] = {10, 20, 30, 40};
int *p = arr;  // 数组名就是首地址

printf("%d\n", arr[2]);   // 30
printf("%d\n", *(p+2));   // 30

但要注意,数组名不是普通的指针。你不能做arr++这种操作,因为数组名是常量。但p可以,因为p是变量。

4.4 结构体与联合体:打包数据

结构体,就是把不同类型的数据打包在一起。比如你要描述一个学生,有姓名、年龄、成绩,用结构体就很方便。

struct Student {
    char name[20];
    int age;
    float score;
};

struct Student stu1 = {"张三", 18, 95.5};
printf("姓名:%s,年龄:%d,成绩:%.1f\n", stu1.name, stu1.age, stu1.score);

联合体就更有意思了。它和结构体很像,但所有成员共享同一块内存。说白了,联合体的大小取决于最大的那个成员。我在做协议解析时经常用联合体,比如把一个int拆成四个字节。

union Data {
    int i;
    char c[4];
};

union Data d;
d.i = 0x12345678;
printf("第一个字节:0x%x\n", d.c[0]);  // 小端模式下是0x78
重点:结构体成员各自独立,联合体成员共享内存。用联合体可以节省内存,但一次只能用一个成员。

4.5 内存管理:malloc和free

静态数组的大小在编译时就确定了,但很多时候我们不知道需要多少内存。这时候就需要动态内存分配。malloc从堆上申请内存,free释放内存。

int *p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
    printf("内存分配失败!\n");
    return -1;
}

// 使用内存
for (int i = 0; i < 10; i++) {
    p[i] = i * 2;
}

// 用完释放
free(p);
p = NULL;  // 避免野指针

我曾经犯过一个低级错误:malloc之后忘了free,导致内存泄漏。在嵌入式系统里,内存本来就有限,泄漏几次系统就挂了。所以我现在每次malloc,都会立刻写对应的free,就像成双成对一样。

警告:malloc返回的指针一定要检查是否为NULL。free之后一定要把指针置为NULL。这两个习惯能救你的命。

4.6 位运算:嵌入式工程师的瑞士军刀

位运算在嵌入式开发里太常用了。设置寄存器的某一位、清除某一位、判断某一位的状态,都离不开位运算。

常用的位运算符:&(与)、|(或)、^(异或)、~(取反)、<<(左移)、>>(右移)。

unsigned char reg = 0b00001111;

// 设置第4位(从0开始)
reg |= (1 << 4);  // reg = 0b00011111

// 清除第0位
reg &= ~(1 << 0); // reg = 0b00011110

// 判断第2位是否为1
if (reg & (1 << 2)) {
    printf("第2位是1\n");
}

我在项目中遇到过一个问题:用位运算操作寄存器时,忘了考虑寄存器是只读的。结果写操作被硬件忽略了,查了半天才发现。所以操作寄存器前,一定要看数据手册,确认哪些位是可读可写的。

我的习惯:用宏定义把常用的位操作封装起来,比如SET_BIT(reg, bit)、CLR_BIT(reg, bit)、GET_BIT(reg, bit)。这样代码可读性高,也不容易出错。

4.7 综合示例:用结构体和位运算管理GPIO

咱们来个实战例子。假设有一个GPIO寄存器,低8位是数据位,高8位是控制位。我们用结构体和位运算来管理它。

typedef struct {
    unsigned char data;   // 数据位
    unsigned char ctrl;   // 控制位
} GPIO_Reg;

GPIO_Reg gpio;

// 设置数据位第3位为1
gpio.data |= (1 << 3);

// 清除控制位第1位
gpio.ctrl &= ~(1 << 1);

// 读取数据位第5位
if (gpio.data & (1 << 5)) {
    // 做点什么
}

你看,结构体把相关的寄存器打包在一起,位运算精确控制每一位。这就是嵌入式开发的日常。

好了,这一节的内容就到这里。函数、指针、结构体、内存管理、位运算,这些都是C语言的硬核内容。别指望一次就全记住,多写代码,多踩坑,慢慢就熟练了。

下一节,咱们要进入嵌入式开发的核心——STM32微控制器。我会从选型开始,带你一步步搭建开发环境。到时候见。