C语言

本文最后更新于 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;
//__attribute__((aligned(1),packed))

枚举

用来代替常量不解其意的手段

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); //等于2,只有len划分了内存。
Test_t *a = (Test_t *)malloc(sizeof(Test_t) + 10); //这里给Data分配了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);
}

//在main函数中实现带参的函数回调
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" //printf
#include "stdarg.h" //va_list相关
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下载


C语言
https://blog.kala.love/posts/7379d433/
作者
久远·卡拉
发布于
2021年4月15日
许可协议