现代嵌入式C++教程:快速的C语言复习PartB
完整的仓库地址在Tutorial_AwesomeModernCPP中,您也可以光顾一下,喜欢的话给一个Star激励一下作者
5. 指针
指针是C语言最强大也最容易出错的特性,在嵌入式编程中尤为重要。这里因为是快速的复习,只是带大家闪过以下C的指针。
5.1 指针基础
intvalue=42;int*ptr=&value;// ptr存储value的地址intderef=*ptr;// 解引用,deref = 42*ptr=100;// 通过指针修改value// 空指针int*null_ptr=NULL;// 应始终初始化指针// 指针算术intarray[5]={1,2,3,4,5};int*p=array;p++;// 指向array[1]intval=*(p+2);// 访问array[3],val = 45.2 指针与数组
数组名在大多数情况下会退化为指向首元素的指针,欸,这可是要注意的是——数组不是指针!!!
intnumbers[10];int*ptr=numbers;// 等价于 &numbers[0]// 数组访问的两种方式numbers[3]=42;// 下标方式*(ptr+3)=42;// 指针方式,等价// 指针遍历数组for(int*p=numbers;p<numbers+10;p++){*p=0;}5.3 多级指针
这个玩意让我想起来一个梗图了——一个人指着一个人指着一个人.jpg,对,就这个意思。一个指向了指向了指向了指向了一个变量的指针变量的指针变量的指针变量。嗯,头都绕晕了,笔者建议是非必须,别玩这出,你这是给你的同事埋大的。
intvalue=42;int*ptr=&value;int**ptr_ptr=&ptr;// 指向指针的指针// 解引用intval1=*ptr;// 42intval2=**ptr_ptr;// 42多级指针在动态分配二维数组时很有用,但在嵌入式系统中应谨慎使用动态内存分配。
5.4 指针与const
const和指针的组合有多种含义:
intvalue=42;// 指向常量的指针:不能通过ptr修改valueconstint*ptr1=&value;// *ptr1 = 100; // 错误ptr1=&other;// 可以,指针本身可以改变// 常量指针:指针本身不能改变int*constptr2=&value;*ptr2=100;// 可以,可以修改指向的值// ptr2 = &other; // 错误,指针不能改变// 指向常量的常量指针:都不能改变constint*constptr3=&value;// *ptr3 = 100; // 错误// ptr3 = &other; // 错误6. 数组与字符串
6.1 数组
数组是相同类型元素的连续集合:
// 一维数组intnumbers[10];// 声明intprimes[]={2,3,5,7,11};// 初始化,大小自动推导为5intmatrix[3][4];// 二维数组// 数组初始化intzeros[100]={0};// 全部初始化为0intpartial[10]={1,2};// 前两个元素为1和2,其余为0// 指定初始化器(C99)intsparse[100]={[5]=10,[20]=30};在嵌入式系统中,数组常用于缓冲区和查找表:
// 串口接收缓冲区uint8_tuart_rx_buffer[256];volatilesize_trx_head=0;volatilesize_trx_tail=0;// 查找表(节省计算资源)constuint8_tsin_table[360]={// 预计算的正弦值(0-255范围)128,130,133,135,// ...};6.2 字符串
C语言中的字符串是以空字符'\0'结尾的字符数组:
charstr1[10]="Hello";// 字符串字面量初始化charstr2[]="World";// 大小自动推导为6(包括'\0')charstr3[10];// 未初始化// 字符串操作(需要包含string.h)#include<string.h>strcpy(str3,str1);// 复制字符串strcat(str3,str2);// 连接字符串intlen=strlen(str1);// 获取长度intcmp=strcmp(str1,str2);// 比较字符串在嵌入式系统中,应优先使用带长度限制的安全函数版本:
charbuffer[32];strncpy(buffer,source,sizeof(buffer)-1);buffer[sizeof(buffer)-1]='\0';// 确保以空字符结尾// 更安全的做法snprintf(buffer,sizeof(buffer),"Value: %d",value);字符串处理的注意事项:
- 确保目标缓冲区足够大
- 始终确保字符串以
'\0'结尾 - 在资源受限的系统中,考虑使用固定大小的缓冲区避免动态分配
7. 结构体、联合体与枚举
7.1 结构体
结构体允许将不同类型的数据组合成一个单元:
// 定义结构体structPoint{intx;inty;};// 使用typedef简化typedefstruct{intx;inty;}Point;// 创建和初始化Point p1={10,20};// 顺序初始化Point p2={.y=30,.x=40};// 指定初始化器(C99)// 访问成员p1.x=100;inty_value=p1.y;// 指针访问Point*ptr=&p1;ptr->x=200;// 等价于 (*ptr).x = 200在嵌入式开发中,结构体广泛用于表示配置、状态和数据包:
// 传感器数据结构typedefstruct{uint32_ttimestamp;floattemperature;floathumidity;uint16_tlight_level;uint8_tstatus;}SensorReading;// 通信协议数据包typedefstruct{uint8_theader;uint8_tcommand;uint16_tlength;uint8_tdata[256];uint16_tchecksum;}__attribute__((packed))ProtocolPacket;// 禁用对齐填充7.2 位域
位域允许在结构体中以位为单位分配存储,这在处理硬件寄存器时极为有用:
// 寄存器位域定义typedefstruct{uint32_tEN:1;// 使能位uint32_tMODE:2;// 模式选择(2位)uint32_tRESERVED:5;// 保留位uint32_tPRIORITY:3;// 优先级(3位)uint32_t:21;// 未命名位域,填充}ControlRegister;// 使用volatileControlRegister*ctrl_reg=(ControlRegister*)0x40000000;ctrl_reg->EN=1;ctrl_reg->MODE=2;ctrl_reg->PRIORITY=7;注意:位域的实现依赖于编译器和平台,在需要精确控制时应谨慎使用。
7.3 联合体
联合体的所有成员共享同一块内存,用于节省空间或类型双关:
// 基本联合体unionData{inti;floatf;charbytes[4];};unionData d;d.i=0x12345678;printf("%02X",d.bytes[0]);// 访问字节表示在嵌入式编程中,联合体常用于数据类型转换和协议处理:
// 多类型数据容器typedefunion{uint32_tword;uint16_thalfword[2];uint8_tbyte[4];}DataConverter;DataConverter dc;dc.word=0x12345678;// 现在可以按字节访问:dc.byte[0], dc.byte[1], ...// 结构体与联合体结合typedefstruct{uint8_ttype;union{intint_value;floatfloat_value;charstring_value[16];}data;}Variant;7.4 枚举
枚举定义命名的整数常量集合,提高代码可读性:
// 基本枚举enumColor{RED,// 0GREEN,// 1BLUE// 2};// 指定值enumStatus{STATUS_OK=0,STATUS_ERROR=-1,STATUS_BUSY=1,STATUS_TIMEOUT=2};// 使用typedeftypedefenum{STATE_IDLE,STATE_RUNNING,STATE_PAUSED,STATE_ERROR}SystemState;枚举在嵌入式开发中常用于定义状态、命令码和配置选项:
// 命令定义typedefenum{CMD_NOOP=0x00,CMD_READ=0x01,CMD_WRITE=0x02,CMD_ERASE=0x03,CMD_RESET=0xFF}Command;// 错误码typedefenum{ERR_NONE=0,ERR_INVALID_PARAM=1,ERR_TIMEOUT=2,ERR_HARDWARE_FAULT=3,ERR_OUT_OF_MEMORY=4}ErrorCode;8. 预处理器
预处理器在编译之前处理源代码,它是C语言灵活性的重要来源,在嵌入式开发中尤为重要。
8.1 宏定义
// 对象宏#defineMAX_SIZE100#definePI3.14159f#defineLED_PIN13// 函数宏#defineMAX(a,b)((a)>(b)?(a):(b))#defineMIN(a,b)((a)<(b)?(a):(b))#defineABS(x)((x)<0?-(x):(x))// 多行宏#defineSWAP(a,b,type)do{\type temp=(a);\(a)=(b);\(b)=temp;\}while(0)宏的注意事项:
- 参数应该加括号以避免优先级问题
- 多行宏使用do-while(0)包装
- 宏不进行类型检查,使用时要小心
在嵌入式开发中的典型应用:
// 寄存器位操作宏#defineBIT(n)(1UL<<(n))#defineSET_BIT(reg,bit)((reg)|=BIT(bit))#defineCLEAR_BIT(reg,bit)((reg)&=~BIT(bit))#defineREAD_BIT(reg,bit)(((reg)>>(bit))&1UL)#defineTOGGLE_BIT(reg,bit)((reg)^=BIT(bit))// 数组大小#defineARRAY_SIZE(arr)(sizeof(arr)/sizeof((arr)[0]))// 范围检查#defineIN_RANGE(x,min,max)(((x)>=(min))&&((x)<=(max)))// 字节对齐#defineALIGN_UP(x,align)(((x)+(align)-1)&~((align)-1))8.2 条件编译
条件编译允许根据条件选择性地包含或排除代码,这个东西是跨平台实现的一个基本利器。
// 基本条件编译#ifdefDEBUG#defineDEBUG_PRINT(fmt,...)printf(fmt,##__VA_ARGS__)#else#defineDEBUG_PRINT(fmt,...)((void)0)#endif// 使用DEBUG_PRINT("Value: %d\n",value);// 仅在DEBUG定义时输出// 平台相关代码#ifdefined(STM32F4)||defined(STM32F7)#defineMCU_FAMILY_STM32F4_F7#include"stm32f4xx.h"#elifdefined(STM32L4)#defineMCU_FAMILY_STM32L4#include"stm32l4xx.h"#else#error"Unsupported MCU family"#endif// 功能开关#defineFEATURE_USB1#defineFEATURE_ETHERNET0#ifFEATURE_USBvoidusb_init(void);#endif#ifFEATURE_ETHERNETvoidethernet_init(void);#endif8.3 文件包含
// 系统头文件#include<stdio.h>#include<stdint.h>// 用户头文件#include"config.h"#include"hal.h"// 防止重复包含(头文件保护)#ifndefCONFIG_H#defineCONFIG_H// 头文件内容#endif// CONFIG_H// 或使用#pragma once(非标准但广泛支持)#pragmaonce8.4 预定义宏
编译器提供了一些有用的预定义宏:
// 文件和行号#defineLOG_ERROR(msg)\fprintf(stderr,"Error in %s:%d - %s\n",__FILE__,__LINE__,msg)// 函数名voidsome_function(void){DEBUG_PRINT("Entered %s\n",__func__);}// 日期和时间printf("Compiled on %s at %s\n",__DATE__,__TIME__);// 标准版本#if__STDC_VERSION__>=199901L// C99或更高版本#endif9. 存储类别与作用域
9.1 存储类别
C语言提供了几种存储类别说明符:
auto:局部变量的默认存储类别,很少显式使用:
voidfunction(void){autointx=10;// 等价于 int x = 10;}static:有两种主要用途
静态局部变量保持值在函数调用之间:
voidcounter(void){staticintcount=0;// 仅初始化一次count++;printf("Called %d times\n",count);}静态全局变量和函数限制作用域在当前文件:
staticintfile_scope_var=0;// 只在本文件可见staticvoidhelper_function(void){// 只能在本文件内调用}extern:声明变量或函数在其他文件中定义:
// file1.cintglobal_counter=0;// file2.cexternintglobal_counter;// 声明,不分配存储空间voidincrement(void){global_counter++;}register:建议编译器将变量存储在寄存器中(现代编译器通常忽略):
voidfast_loop(void){registerinti;for(i=0;i<1000000;i++){// 循环变量建议存储在寄存器}}9.2 作用域规则
C语言有四种作用域:文件作用域、函数作用域、块作用域和函数原型作用域。
在嵌入式开发中,合理使用作用域可以避免命名冲突和意外的副作用:
// 文件作用域(全局)intglobal_var=0;staticintfile_static_var=0;// 仅本文件可见voidfunction(void){// 函数作用域intlocal_var=0;if(condition){// 块作用域intblock_var=0;// local_var和block_var都可见}// block_var在这里不可见}10. 内存管理
10.1 动态内存分配
虽然在嵌入式系统中应尽量避免动态内存分配(因为内存碎片和不确定性),但了解这些函数仍然重要:
#include<stdlib.h>// 分配内存int*array=(int*)malloc(10*sizeof(int));if(array==NULL){// 分配失败处理}// 分配并清零int*zeros=(int*)calloc(10,sizeof(int));// 重新分配array=(int*)realloc(array,20*sizeof(int));// 释放内存free(array);array=NULL;// 良好的实践10.2 内存布局
理解程序的内存布局对嵌入式开发至关重要,这里我们放到后面更加专门的部分介绍,这里就是过一下。
+------------------+ 高地址 | 栈(Stack) | 向下增长,存放局部变量和函数调用 +------------------+ | ↓ | | | | 未分配 | | | | ↑ | +------------------+ | 堆(Heap) | 向上增长,动态分配内存 +------------------+ | BSS段 | 未初始化的全局变量和静态变量 +------------------+ | 数据段(Data) | 初始化的全局变量和静态变量 +------------------+ | 代码段(Text) | 程序代码(只读) +------------------+ 低地址在嵌入式系统中,通常需要精确控制变量的存储位置:
// 放置在特定内存区域(编译器扩展)__attribute__((section(".ccmram")))staticuint32_tfast_buffer[1024];// 对齐要求__attribute__((aligned(4)))uint8_tdma_buffer[256];// 禁止优化__attribute__((used))constuint32_tversion=0x01020304;