本文最后更新于 2024年6月25日 早上
本文主要汇总碰到的一些能提高代码质量的C语言的使用方式。可能文本结构混乱,以右下角目录为导航。初版将之前的各处的笔记转移过来,之后则是碰到新玩意再更新。
数据结构
结构体位域
我之前主要用在传感器开关上,一般开关就0和1两个状态,下列代码中:1则表示此变量只占用一位
1 2 3 4 5 6 7 8 9 10 11
| typedef struct sSensorMag { char s1:1; char s2:1; char s3:1; char s4:1; char s5:1; char s6:1; char s7:1; char s8:1; }sensorMag_t;
|
然后使用sizeof就可以知道,实际上只使用了一个字节。
联合体和位域
这是在lorawan协议上学到的写法,算是上面的升级版,用一个变量集中管理,使用上方便不少。通信协议通常使用这种方式,例如下面是对lorawan协议头部的定义:
1 2 3 4 5 6 7 8 9 10
| typedef union uLoRaMacHeader { uint8_t Value; struct sHdrBits { uint8_t Major : 2; uint8_t RFU : 3; uint8_t MType : 3; }Bits; }LoRaMacHeader_t;
|
结构体的初始化
emm 总所周知的一种初始化方式是
1 2 3 4 5 6 7
| typedef struct sSensorMag { char s1; char s2; }sensorMag_t;
sensorMag_t test={1,2};
|
但是当结构体成员多了后,就会发现,想改其中一个值的时候,不知道是哪个,得去看看结构体定义。于是结构体初始化可以使用下列方式
1 2 3 4 5 6 7
| sensor_t test={.s1=2,.s2=2}; //以上文uLoRaMacHeader结构体为例则是 LoRaMacHeader_t test={ .Bits.Major=1, .Bits.MType=2, .Bits.RFU=3 };
|
联合体+位域+枚举
以上文的uLoRaMacHeader进行修改,例如已知MType的选择只有MType_AAA和MType_BBB两种,可以将其修改为枚举类型,以此进行限制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| typedef union uLoRaMacHeader { char Value; struct sHdrBits { char Major : 2; char RFU : 3; enum { MType_AAA =0, MType_BBB =1, }MType:3; }Bits; }LoRaMacHeader_t;
|
字节对齐
在结构体中字节对齐默认是结构体成员分配空间最大的哪一个,例如3个成员,分别是char,int,short,则默认以int进行对齐。
1 2 3 4 5 6
| typedef struct stest { char a; int b; short c; }test_t;
|
以上结构体为例,最大的为int,4字节,则开始分配a的时候就使用4字节,然后分别b的时候发现剩余不够,则新索取4字节,c同理,由于b已经用完新分配的,则仍然继续索取4字节,于是上结构体将会使用12字节大小。将b和c换个位置则变成了8字节大小。所以通常将大类型的成员放后面就是这个原因。
但是有的时候并不想使用默认,而是手动设置规则,就可以使用到GNU的一个关键字__attribute__来进行属性设置。
例如添加packed参数,则是将成员进行1字节对齐,他和aligned参数不同,后者是告诉编译器内存的分配规则,前者是改变结构体变量的布局使其压缩内存空间。
1 2 3 4 5 6 7
| typedef struct stest { char a; int b; short c; }__attribute__ ((__packed__)) test_t;
|
枚举
用来代替常量不解其意的手段
1 2 3 4
| typedef enum{ INIT_OK, INIT_ERROR, }SVLBInitStatus;
|
可变长结构体
1 2 3 4 5 6 7 8 9 10
| typedef struct sTest { unsigned short len; unsigned char data[0]; }Test_t;
{ sizeof(Test_t); Test_t *a = (Test_t *)malloc(sizeof(Test_t) + 10); }
|
使用示例
宏
定义类型
1
| typedef unsigned char uint8;
|
得到结构体成员在结构体中的偏移量
1
| #define STRUCT_POS(type ,member) ( (unsigned long)&((type *)0)->member)
|
将一个字母转换为大写
1
| #define UPCASE( c ) ( ((c) >= 'a' && (c) <= 'z') ? ((c) - 0x20) : (c) )
|
判断字符是不是10进制的数字
1
| #define DECCHK( c ) ((c) >= '0' && (c) <= '9')
|
判断字符是不是16进制的数字
1 2 3
| #define HEXCHK( c ) ( ((c) >= '0' && (c) <= '9') ||\ ((c) >= 'A' && (c) <= 'F') ||\ ((c) >= 'a' && (c) <= 'f') )
|
返回数组元素的个数
1
| #define ARR_SIZE( a ) ( sizeof( (a) ) / sizeof( (a[0]) ) )
|
得到指定地址上一个数据类型值
1
| #define MEM_P( p,type ) ( *( (type *) (x) ) )
|
得到一个结构体中field所占用的字节数
1
| #define FSIZ( type, field ) sizeof( ((type *) 0)->field )
|
函数
弱函数
当外部没有定义的时候,使用该处的定义,如果外部定义了则函数实现的是外部定义的
1 2 3 4
| int __attribute__((weak)) func(......) { return 0 }
|
回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void PrintfText(char* s) { printf(s); }
void CallPrintfText(void (*callfuct)(char*),char* s) { callfuct(s); }
int main(int argc,char* argv[]) { CallPrintfText(PrintfText,"Hello World!\n"); return 0; }
|
内联函数
就是直接将该函数内容放置到使用函数的地方,不用对函数进行压栈等处理,适合于频繁调用,且函数中没有啥内存消耗的,下列是例子
1 2 3 4
| static __inline void list_init(list_t *l) { l->next = l->prev = l; }
|
函数指针
1 2 3 4 5 6 7 8 9 10 11 12
| typedef struct sExternalFunction { u8 (*getBCStatus)(void); }ExternalFunction;
static ExternalFunction SVLexternalFuction;
void solenoidValveLeakInit(ExternalFunction ex) { SVLexternalFuction = ex; }
|
指向指针的指针
解决问题:修改对指针的修改。
简书原文
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <stdio.h> #include <stdbool.h> #include <stdint.h>
typedef struct{ int data; }temp_s; static temp_s temp={20};
void read(void *p) { *((uint64_t*)p)=(uint64_t)&temp; }
int main() { temp_s* b; read(&b); printf("data:%d\n",b->data); return 0; }
|
其他
关键字volatile
用于修饰随时可能被改变的变量,使其每次读取都从对应的寄存器取出,而不是内存中。例如在STM32的ADC+DMA数据读取时就会用到volatile。下列例子说明什么情况下会使用到
1 2 3 4
| int test =10 int a = test /*一堆其他代码*/ int b = test
|
某些编译器在优化后即是在中间一堆代码运行期间test被其他中断修改,但是由于赋值b时并未重新读取test的寄存器值,导致b仍然为10。但是给test加上volatile修饰,便能解决这个问题。
可变参数函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include "stdio.h" #include "stdarg.h" void func(int num,...) { va_list valist; va_start(valist,num); for(int i=0;i<num;i++) { printf("%d\n",va_arg(valist,int)); } va_end(valist); } void main(void) { func(3,1,2,3); }
|
main函数的参数
main函数有三个参数,分别是argc,argv和envp。
第一个参数argc表示的是传入参数的个数。
第二个参数char* argv[] ,是字符串数组,用来存放指向的字符串参数的指针数组,每一个元素指向一个参数,argv[ 0 ]指向程序运行的全路径名,argv[ 1 ]指向执行程序名后的第一个字符串 ,表示真正传入的第一个参数,一次类推。argv[ argc ]为NULL则表示参数结尾。
第三个参数char* envp[] 是用于保存用户环境变量。
测试代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| int main(int argc,char *argv[],char *envp[]) { int i=0; while (argv[i]) { printf("argv[%d]:%s\n",i,argv[i]); i++; } i=0; printf("\n"); while (envp[i]) { printf("%s\n", envp[i]); i++; } }
|
输出结果如下:
用switch来比对字符串的一种方式
最近在接收json的时候遇到要匹配下行命令,而这些命令都是字符串,根据不同命令执行不同的动作。以前传输是字节表示,直接枚举后用switch来罗列。但现在switch可没法比对字符串,全部写出if+else,感觉太难看了。于是有了下面的写法。
思路是将字符串替换为唯一的数字,然后继续用switch来比对数字即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| typedef enum eCmdTyep{ GetLog=0, SetWiFi, GetOfflineData, DownloadFirmware, UpdateFirmware, }eCmdTyep_t;
const char * const type[] ={"GetLog","SetWiFi","GetOfflineData","DownloadFirmware","UpdateFirmware"};
int getPos(const char * s) { for(int i=0; i<UpdateFirmware+1; i++) if( strcmp(s,type[i]) ==0 ) return i; return -1; } void main() { char *test = "UpdateFirmware"; switch (getPos(test)) { case GetLog: printf("GetLog"); break; } }
|
文件可从Github下载