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;
}
4.2 指针:C语言的灵魂
指针,绝对是C语言里最让人又爱又恨的东西。爱它,因为它灵活、高效;恨它,因为一不小心就踩坑。
指针就是一个变量,它存的是另一个变量的地址。你可以通过指针间接访问那个变量。嗯,这里要注意:指针本身也有地址,指针也可以指向指针,这就是二级指针。
int a = 10;
int *p = &a; // p指向a的地址
printf("a的值:%d\n", a); // 直接访问
printf("a的值:%d\n", *p); // 通过指针间接访问
我在项目中遇到过最经典的坑:指针没有初始化就使用。野指针会指向一个随机的内存地址,轻则程序崩溃,重则系统死机。所以我的习惯是:定义指针时立即初始化为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,就像成双成对一样。
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");
}
我在项目中遇到过一个问题:用位运算操作寄存器时,忘了考虑寄存器是只读的。结果写操作被硬件忽略了,查了半天才发现。所以操作寄存器前,一定要看数据手册,确认哪些位是可读可写的。
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微控制器。我会从选型开始,带你一步步搭建开发环境。到时候见。