c基础13-嵌入式c-程序员宅基地

技术标签: c语言  各语言  嵌入式硬件  

一、STM32中这些C语言知识点

C语言是单片机开发中的必备基础知识,本文列举了部分STM32学习中会遇见的C语言基础知识点,希望能对大家有所帮助。

 位操作 

下面,我们先讲解几种位操作符,然后讲解位操作使用技巧。C语言支持如下6中位操作:

六种位操作

下面,我们想着重讲解位操作在单片机开发中的一些实用技巧。

1.1 在不改变其他位的值的状况下,对某几个位进行设值

这个场景在单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作,然后用 | 操作符设值。

比如我要改变GPIOA的状态,可以先对寄存器的值进行&清零操作:

1.2  移位操作提高代码的可读性

移位操作在单片机开发中非常重要,下面是delay_init函数的一行代码:

SysTick->CTRL |= 1 << 1;

这个操作就是将CTRL寄存器的第1位(从0开始算起)设置为1,为什么要通过左移而不是直接设置一个固定的值呢?

其实这是为了提高代码的可读性以及可重用性。这行代码可以很直观明了的知道,是将第1位设置为1。如果写成:

SysTick->CTRL |= 0X0002;

这个虽然也能实现同样的效果,但是可读性稍差,而且修改也比较麻烦。

1.3  按位取反操作使用技巧

按位取反在设置寄存器的时候经常被使用,常用于清除某一个/某几个位。下面是delay_us函数的一行代码:

SysTick->CTRL &= ~(1 << 0) ; /* 关闭SYSTICK */

该代码可以解读为 仅设置CTRL寄存器的第0位(最低位)为0,其他位的值保持不变。

同样我们也不使用按位取反,将代码写成:

SysTick->CTRL &= 0XFFFFFFFE; /* 关闭SYSTICK */

可见前者的可读性,及可维护性都要比后者好很多。

1.4  ^按位异或操作使用技巧

该功能非常适合用于控制某个位翻转,常见的应用场景就是控制LED闪烁,如:

GPIOB->ODR ^= 1 << 5;

执行一次该代码,就会使PB5的输出状态翻转一次,如果我们的LED接在PB5上,就可以看到LED闪烁了。

 define宏定义 

define是C语言中的预处理命令,它用于宏定义(定义的是常量),可以提高源代码的可读性,为编程提供方便。常见的格式:

定义标识符HSE_VALUE的值为8000000,数字后的U表示unsigned的意思。

至于define宏定义的其他一些知识,比如宏定义带参数这里我们就不多讲解。

 ifdef条件编译 

单片机程序开发过程中,经常会遇到一种情况,当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。

条件编译命令最常见的形式为:

#ifdef 标识符 程序段1#else 程序段2#endif

它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。其中#else部分也可以没有,即:

#ifdef 程序段1 #endif

条件编译在HAL库里面是用得很多,在stm32mp1xx_hal_conf.h这个头文件中经常会看到这样的语句:

#if !defined (HSE_VALUE) #define HSE_VALUE 24000000U #endif

如果没有定义HSE_VALUE这个宏,则定义HSE_VALUE宏,并且HSE_VALUE的值为24000000U。条件编译也是C语言的基础知识吧。

这里提一下,24000000U中的U表示无符号整型,常见的,UL表示无符号长整型,F表示浮点型。

这里加了U以后,系统编译时就不进行类型检查,直接以U的形式把值赋给某个对应的内存,如果超出定义变量的范围,则截取。

 extern变量申明 

C语言中extern可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。

这里面要注意,对于extern申明变量可以多次,但定义只有一次。在我们的代码中你会看到看到这样的语句:

extern uint16_t g_usart_rx_sta;

这个语句是申明g_usart_rx_sta变量在其他文件中已经定义了,在这里要使用到。

所以,你肯定可以找到在某个地方有变量定义的语句:

uint16_t g_usart_rx_sta;

extern的使用比较简单,但是也会经常用到,需要掌握。

 typedef类型别名 

typedef用于为现有类型创建一个新的名字,或称为类型别名,用来简化变量的定义。typedef在HAL库用得最多的就是定义结构体的类型别名和枚举类型了。

struct _GPIO
    {
        __IO uint32_t CRL;
        __IO uint32_t CRH;
        …
    };

定义了一个结构体GPIO,这样我们定义结构体变量的方式为:

struct _GPIO gpiox; /* 定义结构体变量gpiox */

但这样很繁琐,HAL库中有很多这样的结构体变量需要定义。这里我们可以为结体定义一个别名GPIO_TypeDef,这样我们就可以在其他地方通过别名GPIO_TypeDef来定义结构体变量了,方法如下:

typedef struct
    {
            __IO uint32_t CRL;
            __IO uint32_t CRH;
            …
    } GPIO_TypeDef;

Typedef为结构体定义了一个别名GPIO_TypeDef,这样我们可以通过GPIO_TypeDef来定义结构体变量:GPIO_TypeDef gpiox;

这里的GPIO_TypeDef就跟struct _GPIO是等同的作用了,但是GPIO_TypeDef使用起来方便很多。 whaosoft aiot http://143ai.com 

二、在Keil中调试STM32硬件错误的方法

在用Keil对STM32的程序进行仿真时程序有时会跑飞,停止仿真程序会停在HardFault_Handler 函数里的死循环while(1)中。

这说明 STM32 出现了硬件错误。

STM32出现硬件错误可能有以下原因:

  1. 数组越界操作;

  2. 内存溢出,访问越界;

  3. 堆栈溢出,程序跑飞;

  4. 中断处理错误;

遇到这种情况,可以通过以下2种方式来定位到出错代码段。

方法1:

在硬件中断函数HardFault_Handler里的while(1)处打调试断点,程序执行到断点处时点击STOP停止仿真。

在Keil菜单栏点击View——Registers Window,在寄存器查看窗口查找R14(LR)的值。

如果R14(LR) = 0xFFFFFFE9,继续查看MSP(主堆栈指针)的值;如果R14(LR) = 0xFFFFFFFD,继续查看PSP(进程栈指针)的值;

我的程序R14(LR) = 0xFFFFFFF9,接下来以此为例。

在Keil菜单栏点击“View”——“Memory Windows”——“Memory1”;

在“Address”地址栏中输入MSP的值:0x20001288,然后在对应的行里找到地址。

地址一般以 0x08 开头的32位数。本例中,地址为0x08003CB9。

在Keil菜单栏点击View——Disassembly Window,在Disassembly窗口中右击,在下拉菜单中选择Show Disassemblyat Address...。

在弹出框Show Code atAdress的地址框中输入地址0x08003CB9进行搜索,然后就会找到相对应的代码。这里的代码就是进入循环中断之前的情况。

仔细查看附近区域的相关代码来排查错误具体原因。

方法2:

在硬件中断函数HardFault_Handler里的while(1)处打调试断点,程序执行到断点处时点击“STOP”停止仿真。

在Keil菜单栏点击View——Call Stack Window弹出Call Stack + Locals对话框。

然后在对话框中右键选择Show Caller Code,就会跳转到出错之前的函数处,仔细查看这部分函数被调用或者数组内存使用情况。

三、嵌入式C语言知识点

C语言中的关键字

    C语言中的关键字按照功能分为:

  • 数据类型(常用char, short, int, long, unsigned, float, double)

  • 运算和表达式( =, +, -, *, while, do-while, if, goto, switch-case)

  • 数据存储(auto, static, extern,const, register,volatile,restricted),

  • 结构(struct, enum, union,typedef),

  • 位操作和逻辑运算(<<, >>, &, |, ~,^, &&),

  • 预处理(#define, #include, #error,#if...#elif...#else...#endif等),

  • 平台扩展关键字(__asm, __inline,__syscall)

    这些关键字共同构成了嵌入式平台的C语言语法。嵌入式的应用从逻辑上可以抽象为三个部分:

  • 数据的输入,如传感器,信号,接口输入

  • 数据的处理,如协议的解码和封包,AD采样值的转换等

  • 数据的输出,如GUI的显示,输出的引脚状态,DA的输出控制电压,PWM波的占空比等

    对于数据的管理就贯穿着整个嵌入式应用的开发,它包含数据类型,存储空间管理,位和逻辑操作,以及数据结构,C语言从语法上支撑上述功能的实现,并提供相应的优化机制,以应对嵌入式下更受限的资源环境。

数据类型

    C语言支持常用的字符型,整型,浮点型变量,有些编译器如keil还扩展支持bit(位)和sfr(寄存器)等数据类型来满足特殊的地址操作。C语言只规定了每种基本数据类型的最小取值范围,因此在不同芯片平台上相同类型可能占用不同长度的存储空间,这就需要在代码实现时考虑后续移植的兼容性,而C语言提供的typedef就是用于处理这种情况的关键字,在大部分支持跨平台的软件项目中被采用,典型的如下:

typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
......
typedef signed int int32_t;

   既然不同平台的基本数据宽度不同,那么如何确定当前平台的基础数据类型如int的宽度,这就需要C语言提供的接口sizeof,实现如下。

printf("int size:%d, short size:%d, char size:%d\n", sizeof(int), sizeof(char), sizeof(short));

    这里还有重要的知识点,就是指针的宽度,如:​​​​​​​

char *p;
printf("point p size:%d\n", sizeof(p));

    其实这就和芯片的可寻址宽度有关,如32位MCU的宽度就是4,64位MCU的宽度就是8,在有些时候这也是查看MCU位宽比较简单的方式。

内存管理和存储架构

    C语言允许程序变量在定义时就确定内存地址,通过作用域,以及关键字extern,static,实现了精细的处理机制,按照在硬件的区域不同,内存分配有三种方式(节选自C++高质量编程):

  • 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。

  • 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中 ,效率很高,但是分配的内存容量有限。

  • 从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意多少的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但同时遇到问题也最多。

    这里先看个简单的C语言实例。

//main.c#include <stdio.h>#include <stdlib.h>

static int st_val;                   //静态全局变量 -- 静态存储区
int ex_val;                           //全局变量 -- 静态存储区int main(void)
{
   int a = 0;                         //局部变量 -- 栈上申请
   int *ptr = NULL;                   //指针变量
   static int local_st_val = 0;       //静态变量
   local_st_val += 1;
   a = local_st_val;
   ptr = (int *)malloc(sizeof(int)); //从堆上申请空间
   if(ptr != NULL)
   {      
    printf("*p value:%d", *ptr);
    free(ptr);      
    ptr = NULL;      
    //free后需要将ptr置空,否则会导致后续ptr的校验失效,出现野指针   
    }            
}

   C语言的作用域不仅描述了标识符的可访问的区域,其实也规定了变量的存储区域,在文件作用域的变量st_val和ex_val被分配到静态存储区,其中static关键字主要限定变量能否被其它文件访问,而代码块作用域中的变量a, ptr和local_st_val则要根据类型的不同,分配到不同的区域,其中a是局部变量,被分配到栈中,ptr作为指针,由malloc分配空间,因此定义在堆中,而local_st_val则被关键字限定,表示分配到静态存储区,这里就涉及到重要知识点,static在文件作用域和代码块作用域的意义是不同的:在文件作用域用于限定函数和变量的外部链接性(能否被其它文件访问), 在代码块作用域则用于将变量分配到静态存储区。

    对于C语言,如果理解上述知识对于内存管理基本就足够,但对于嵌入式C来说,定义一个变量,它不一定在内存(SRAM)中,也有可能在FLASH空间,或直接由寄存器存储(register定义变量或者高优化等级下的部分局部变量),如定义为const的全局变量定义在FLASH中,定义为register的局部变量会被优化到直接放在通用寄存器中,在优化运行速度,或者存储受限时,理解这部分知识对于代码的维护就很有意义。此外,嵌入式C语言的编译器中会扩展内存管理机制,如支持分散加载机制和__attribute__((section("用户定义区域"))),允许指定变量存储在特殊的区域如(SDRAM, SQI FLASH), 这强化了对内存的管理,以适应复杂的应用环境场景和需求。

LD_ROM 0x00800000 0x10000 { ;load region size_region
    EX_ROM 0x00800000 0x10000 { ;load address = execution address
  *.o (RESET, +First)
  *(InRoot$$Sections)
  .ANY (+RO)
  }
  EX_RAM 0x20000000 0xC000 { ;rw Data
    .ANY (+RW +ZI)
  }
  EX_RAM1 0x2000C000 0x2000 {
    .ANY(MySection)
   }
  EX_RAM2 0x40000000 0x20000{
    .ANY(Sdram)
  }
}

int a[10] __attribute__((section("Mysection")));
int b[100] __attribute__((section("Sdram")));

​​​​​​​    采用这种方式,我们就可以将变量指定到需要的区域,这在某些情况下是必须的,如做GUI或者网页时因为要存储大量图片和文档,内部FLASH空间可能不足,这时就可以将变量声明到外部区域,另外内存中某些部分的数据比较重要,为了避免被其它内容覆盖,可能需要单独划分SRAM区域,避免被误修改导致致命性的错误,这些经验在实际的产品开发中是常用且重要,不过因为篇幅原因,这里只简略的提供例子,如果工作中遇到这种需求,建议详细去了解下。

    至于堆的使用,对于嵌入式Linux来说,使用起来和标准C语言一致,注意malloc后的检查,释放后记得置空,避免"野指针“,不过对于资源受限的单片机来说,使用malloc的场景一般较少,如果需要频繁申请内存块的场景,都会构建基于静态存储区和内存块分割的一套内存管理机制,一方面效率会更高(用固定大小的块提前分割,在使用时直接查找编号处理),另一方面对于内存块的使用可控,可以有效避免内存碎片的问题,常见的如RTOS和网络LWIP都是采用这种机制,我个人习惯也采用这种方式,所以关于堆的细节不在描述,如果希望了解,可以参考<C Primer Plus>中关于存储相关的说明。

指针和数组

    数组和指针往往是引起程序bug的主要原因,如数组越界,指针越界,非法地址访问,非对齐访问,这些问题背后往往都有指针和数组的影子,因此理解和掌握指针和数组,是成为合格C语言开发者的必经之路。

    数组是由相同类型元素构成,当它被声明时,编译器就根据内部元素的特性在内存中分配一段空间,另外C语言也提供多维数组,以应对特殊场景的需求,而指针则是提供使用地址的符号方法,只有指向具体的地址才有意义,C语言的指针具有最大的灵活性,在被访问前,可以指向任何地址,这大大方便了对硬件的操作,但同时也对开发者有了更高的要求。参考如下代码。

int main(void)
{
char cval[] = "hello";
int i;
int ival[] = {1, 2, 3, 4};
int arr_val[][2] = {
   {1, 2}, {3, 4}};
const char *pconst = "hello";
char *p;
int *pi;
int *pa;
int **par;

  p = cval;
  p++;            //addr增加1
  pi = ival;
  pi+=1;          //addr增加4
  pa = arr_val[0];
  pa+=1;          //addr增加4
  par = arr_val;
  par++;         //addr增加8
for(i=0; i<sizeof(cval); i++)
  {
printf("%d ", cval[i]);
  }
printf("\n");
printf("pconst:%s\n", pconst);
printf("addr:%d, %d\n", cval, p);
printf("addr:%d, %d\n", icval, pi);
printf("addr:%d, %d\n", arr_val, pa);
printf("addr:%d, %d\n", arr_val, par);
}

/* PC端64位系统下运行结果
0x68 0x65 0x6c 0x6c 0x6f 0x0
pconst:hello
addr:6421994, 6421995
addr:6421968, 6421972
addr:6421936, 6421940
addr:6421936, 6421944 */

  对于数组来说,一般从0开始获取值,以length-1作为结束,通过[0, length)半开半闭区间访问,这一般不会出问题,但是某些时候,我们需要倒着读取数组时,有可能错误的将length作为起始点,从而导致访问越界,另外在操作数组时,有时为了节省空间,将访问的下标变量i定义为unsigned char类型,而C语言中unsigned char类型的范围是0~255,如果数组较大,会导致数组超过时无法截止,从而陷入死循环,这种在最初代码构建时很容易避免,但后期如果更改需求,在加大数组后,在使用数组的其它地方都会有隐患,需要特别注意。

    由于,指针占有的空间与芯片的寻址宽度有关,32位平台为4字节,64位为8字节,而指针的加减运算中的长度又与它的类型相关,如char类型为1,int类型为4,如果你仔细观察上面的代码就会发现par的值增加了8,这是因为指向指针的指针,对应的变量是指针,也就是长度就是指针类型的长度,在64位平台下为8,如果在32位平台则为4,这些知识理解起来并不困难,但是这些特性在工程运用中稍有不慎,就会埋下不易察觉的问题。另外指针还支持强制转换,这在某些情况下相当有用,参考如下代码:

#include <stdio.h>

typedef struct
{
int b;
int a;
}STRUCT_VAL;
static __align(4) char arr[8] = {0x12, 0x23, 0x34, 0x45, 0x56, 0x12, 0x24, 0x53};
int main(void)
{
    STRUCT_VAL *pval;
int *ptr;
    pval = (STRUCT_VAL *)arr;
    ptr = (int *)&arr[4];
printf("val:%d, %d", pval->a, pval->b);
printf("val:%d,", *ptr);
}
//0x45342312 0x53241256
//0x53241256

    基于指针的强制转换,在协议解析,数据存储管理中高效快捷的解决了数据解析的问题,但是在处理过程中涉及的数据对齐,大小端,是常见且十分易错的问题,如上面arr字符数组,通过__align(4)强制定义为4字节对齐是必要的,这里可以保证后续转换成int指针访问时,不会触发非对齐访问异常,如果没有强制定义,char默认是1字节对齐的,当然这并不就是一定触发异常(由整个内存的布局决定arr的地址,也与实际使用的空间是否支持非对齐访问有关,如部分SDRAM使用非对齐访问时,会触发异常), 这就导致可能增减其它变量,就可能触发这种异常,而出异常的地方往往和添加的变量毫无关系,而且代码在某些平台运行正常,切换平台后触发异常,这种隐蔽的现象是嵌入式中很难查找解决的问题。另外,C语言指针还有特殊的用法就是通过强制转换给特定的物理地址访问,通过函数指针实现回调,如下:

 这里说明下,volatile易变的,可变的,一般用于以下几种状况:

  • 并行设备的硬件寄存器,如:状态寄存器)

  • 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

  • 多线程应用中被几个任务共享的变量

    volatile可以解决用户模式和异常中断访问同一个变量时,出现的不同步问题,另外在访问硬件地址时,volatile也阻止对地址访问的优化,从而确保访问的实际的地址,精通volatile的运用,在嵌入式底层中十分重要,也是嵌入式C从业者的基本要求之一。函数指针在一般嵌入式软件的开发中并不常见,但对许多重要的实现如异步回调,驱动模块,使用函数指针就可以利用简单的方式实现很多应用,当然我这里只能说是抛砖引玉,许多细节知识是值得详细去了解掌握的。

结构类型和对齐

    C语言提供自定义数据类型来描述一类具有相同特征点的事务,主要支持的有结构体,枚举和联合体。其中枚举通过别名限制数据的访问,可以让数据更直观,易读,实现如下:

typedef enum {spring=1, summer, autumn, winter }season;
season s1 = summer;

    联合体的是能在同一个存储空间里存储不同类型数据的数据类型,对于联合体的占用空间,则是以其中占用空间最大的变量为准,如下:

联合体的用途主要通过共享内存地址的方式,实现对数据内部段的访问,这在解析某些变量时,提供了更为简便的方式,此外测试芯片的大小端模式也是联合体的常见应用,当然利用指针强制转换,也能实现该目的,实现如下:


int data = 0x12345678; 
short *pdata = (short *)&data; 
if(*pdata = 0x5678)     
  printf("%s\n", "小端模式"); 
else   
  printf("%s\n", "大端模式");

 可以看出使用联合体在某些情况下可以避免对指针的滥用。结构体则是将具有共通特征的变量组成的集合,比起C++的类来说,它没有安全访问的限制,不支持直接内部带函数,但通过自定义数据类型,函数指针,仍然能够实现很多类似于类的操作,对于大部分嵌入式项目来说,结构化处理数据对于优化整体架构以及后期维护大有便利。

    C语言的结构体支持指针和变量的方式访问,通过转换可以解析任意内存的数据,如我们之前提到的通过指针强制转换解析协议。另外通过将数据和函数指针打包,在通过指针传递,是实现驱动层实接口切换的重要基础,有着重要的实践意义,另外基于位域,联合体,结构体,可以实现另一种位操作,这对于封装底层硬件寄存器具有重要意义。通过联合体和位域操作,可以实现对数据内bit的访问,这在寄存器以及内存受限的平台,提供了简便且直观的处理方式,另外对于结构体的另一个重要知识点就是对齐了,通过对齐访问,可以大幅度提高运行效率,但是因为对齐引入的存储长度问题,也是容易出错的问题,对于对齐的理解,可以分类为如下说明。

  • 基础数据类型:以默认的的长度对齐,如char以1字节对齐,short以2字节对齐等

  • 数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。

  • 联合体 :按其包含的长度最大的数据类型对齐。

  • 结构体:结构体中每个数据类型都要对齐,结构体本身以内部最大数据类型长度对齐

  其中union联合体的大小与内部最大的变量int一致,为4字节,根据读取的值,就知道实际内存布局和填充的位置是一致,事实上学会通过填充来理解C语言的对齐机制,是有效且快捷的方式。

预处理机制

    C语言提供了丰富的预处理机制,方便了跨平台的代码的实现,此外C语言通过宏机制实现的数据和代码块替换,字符串格式化,代码段切换,对于工程应用具有重

要意义,下面按照功能需求,描述在C语言运用中的常用预处理机制。

    #include 包含文件命令,在C语言中,它执行的效果是将包含文件中的所有内容插入到当前位置,这不只包含头文件,一些参数文件,配置文件,也可以使用该文件插入到当前代码的指定位置。其中<>和""分别表示从标准库路径还是用户自定义路径开始检索。

    #define宏定义,常见的用法包含定义常量或者代码段别名,当然某些情况下配合##格式化字符串,可以实现接口的统一化处理,实例如下:

#define MAX_SIZE  10
#define MODULE_ON  1
#define ERROR_LOOP() do{\
                     printf("error loop\n");\
                   }while(0);
#define global(val) g_##val
int global(v) = 10;
int global(add)(int a, int b)
{
return a+b;
}

   #if..#elif...#else...#endif, #ifdef..#endif, #ifndef...#endif条件选择判断,条件选择主要用于切换代码块,这种综合性项目和跨平台项目中为了满足多种情况下的需求往往会被使用。

    #undef 取消定义的参数,避免重定义问题。

    #error,#warning用于用户自定义的告警信息,配合#if,#ifdef使用,可以限制错误的预定义配置。

    #pragma 带参数的预定义处理,常见的#pragma pack(1), 不过使用后会导致后续的整个文件都以设置的字节对齐,配合push和pop可以解决这种问题,代码如下:


#pragma pack(push)
#pragma pack(1)
struct TestA
{
char i;
int b;
}A;
#pragma pack(pop); //注意要调用pop,否则会导致后续文件都以pack定义值对齐,执行不符合预期
//等同于
struct _TestB{
char i;
int b;
 }__attribute__((packed))A;

总结​​​​​​​

    嵌入式C语言在处理硬件物理地址、位操作、内存访问方面都给予开发者了充分的自由。通过数组,指针以及强制转换的技巧,可以有效减少数据处理中的复制过程,这对于底层是必要的,也方便了整个架构的开发。对于任何嵌入式C语言开发的从业者,清晰的掌握这些基础的知识是必要的。

四、在嵌入式中解析JSON数据,CSON+CJSON

json是目前最为流行的文本数据传输格式,特别是在网络通信上广泛应用,随着物联网的兴起,在嵌入式设备上,也需要开始使用json进行数据传输,那么,如何快速简洁地用C语言进行json的序列化和反序列化呢?

当前,应用最广泛的C语言json解析库当属cJSON,但是,使用cJSON读json进行序列化和反序列化,需要根据key一个一个进行处理,会导致代码冗余,逻辑性不强,哪有没有更好的方法呢?

思路

在Android平台,一般会使用gson等工具解析json,这些工具将json直接映射成对象,在C语言上使用对象的概念,我们需要借助结构体,然而,最大的问题在于,C语言没有高级语言具有的反射机制,直接从json映射到结构体对象几乎是不可能的。

怎么解决呢,既然C语言没有反射机制,那么我们可以自己定义一套类似于反射的机制,这里我将其称之为结构体数据模型,在数据模型中,我们需要准确地描述结构体的特征,包括结构体各成员的名称,类型,在结构体中的偏移。

有了这些,我们可以在解析josn的时候,将解析得到的数据直接写入到对应的内存里面去,或者是在序列化的时候,直接从对应的内存中读取数据,进行处理。

实现

CSON正是采用上面说到的思路,使用数据模型对结构体进行描述,然后基于cJSON,根据数据模型进行解析,将解析得到的数据直接写入到对应的内存区域,从而实现从json到结构体对象的映射。

CSON最基本的数据模型定义如下:​​​​​​​

typedef struct cson_model{
       CsonType type;                      /**< 数据类型 */    char *key;                          /**< 元素键值 */    short offset;                       /**< 元素偏移 */} CsonModel;

通过type描述结构体成员的数据类型,key描述该成员在json中对应的字段,offset描述该结构体成员在结构体中的偏移,CSON在解析json的时候,根据type调用相应的cJSON API并传递key作为参数,得到解析出的数据,然后根据offset将数据写入到对应的内存空间。

比如说这样一个结构体:​​​​​​​

struct project{
       int id;    char *name;}

该结构体包含两个成员,对于成员id,我们使用数据模型对其进行描述

{.type=CSON_TYPE_CHAR, key="id", offset=0}

对于结构体的每个成员,都进行数据模型的定义,就可以得到一个完整的结构体数据模型,CSON会根据这个模型,进行解析。

因为是通过直接写内存的方式,所以在写不同类型的量到内存中时,会多次用到强制转型,导致CSON中赋值的代码都类似于:

*(int *)((int)obj + model[i].offset) = (int)csonDecodeNumber(json, model[i].key);

当然,上面说到的数据模型,只适用于基本数据类型的数据,对于子结构体,链表,数组等,需要对数据模型的定义进行扩充,有兴趣的朋友可以直接阅读CSON源码。

CSON使用实例

声明结构体:​​​​​​​

/** 项目结构体 */
struct project
{
    int id;
    char *name;
};

/** 仓库结构体 */
struct hub
{
    int id;
    char *user;
    struct project *cson;
};

定义数据模型:

对每一个需要使用cson的结构体,都需要定义相对应的数据模型​​​​​​​

/** 项目结构体数据模型 */
CsonModel projectModel[] =
{
    CSON_MODEL_OBJ(struct project),
    CSON_MODEL_INT(struct project, id),
    CSON_MODEL_STRING(struct project, name),
};

/** 仓库结构体数据模型 */
CsonModel hubModel[] =
{
    CSON_MODEL_OBJ(struct hub),
    CSON_MODEL_INT(struct hub, id),
    CSON_MODEL_STRING(struct hub, user),
    CSON_MODEL_STRUCT(struct hub, cson, projectModel, sizeof(projectModel)/sizeof(CsonModel))
};

使用CSON解析:

只需要定义好数据模型,就可以使用CSON读json进行序列化和反序列化。​​​​​​​

void csonDemo(void)
{
    char *jsonDemo = "{\"id\": 1, \"user\": \"Letter\", \"cson\": {\"id\": 2, \"name\": \"cson\"}}";

    /** 解析json */
    struct hub *pHub = csonDecode(jsonDemo, hubModel, sizeof(hubModel)/sizeof(CsonModel));
    printf("hub: id: %d, user: %s, project id: %d, project name: %s\r\n",
        pHub->id, pHub->user, pHub->cson->id, pHub->cson->name);

    /** 序列化对象 */
    char *formatJson = csonEncodeFormatted(pHub, hubModel, sizeof(hubModel)/sizeof(CsonModel));
    printf("format json: %s\r\n", formatJson);

    /** 释放结构体对象 */
    csonFree(pHub, hubModel, sizeof(hubModel)/sizeof(CsonModel));

    /** 释放序列化生成的json字符串 */
    csonFreeJson(formatJson);
}

运行结果:​​​​​​​

hub: id: 1, user: Letter, project id: 2, project name: csonformat json: {
           "id":   1,        "user": "Letter",        "cson": {
                   "id":   2,                "name": "cson"        }}

可以看到,无论是解析json,还是序列化结构体到json,在使用CSON的情况下,都只需要一行代码就可以解决,同样的操作,在使用原生cJSON的情况下,你可能需要多次判断,解析元素。

CSON地址:https://github.com/NevermindZZT/cson

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_29788741/article/details/132746396

智能推荐

unity3d 实现简单水墨画效果_unity3d shader 水墨动画-程序员宅基地

文章浏览阅读6.6k次,点赞2次,收藏8次。水墨效果,素材简陋:文章参考http://gad.qq.com/article/detail/18724,原理可以看这位大佬的文章,我就贴上自己工程源码:https://download.csdn.net/my_unity3d shader 水墨动画

(Java毕业设计)招聘信息管理系统(基于java+springboot)附源码_java人力资源管理系统招聘管理-程序员宅基地

文章浏览阅读1.1k次,点赞2次,收藏17次。在Internet高速发展的今天,我们生活的各个领域都涉及到计算机的应用,其中包括招聘信息管理系统的网络应用,在外国招聘信息管理系统已经是很普遍的方式,不过国内的线上管理系统可能还处于起步阶段。招聘信息管理系统具有招聘信息管理功能的选择。_java人力资源管理系统招聘管理

冯诺依曼、哈佛、RISC、CISC_冯诺依曼曼指令和数据区分-程序员宅基地

文章浏览阅读1.3w次,点赞5次,收藏20次。几个基础材料(源:http://jwc.seu.edu.cn/zq/signal/new/importent/zhang5_6/feng.htmhttp://jwc.seu.edu.cn/zq/signal/new/importent/zhang5_6/harvard.htmhttp://dingjun.net/dingjun/html/14/84FB43402106C575.html_冯诺依曼曼指令和数据区分

Netty——深入解析心跳检测机制_netty 服务端心跳检测-程序员宅基地

文章浏览阅读428次。客户端定时每X秒(推荐小于60秒)向服务端发送特定数据(任意数据都可),服务端设定为X秒没有收到客户端心跳则认为客户端掉线,并关闭连接触发onClose回调。当需要服务端定时给客户端发送心跳数据时, $gateway->pingData设置为服务端要发送的心跳请求数据,心跳数据是任意的,只要客户端能识别即可。当设置为服务端主动发送心跳时,如果客户端最近有发来数据,那么证明客户端存活,服务端会省略一个心跳,下个心跳大约1.5*$gateway->pingInterval秒后发送。心跳检测时间间隔 单位:秒。_netty 服务端心跳检测

linux内核SPI总线驱动分析(一)_linux spi驱动总线分析-程序员宅基地

文章浏览阅读535次。下面有两个大的模块:一个是SPI总线驱动的分析 (研究了具体实现的过程)另一个是SPI总线驱动的编写(不用研究具体的实现过程)SPI总线驱动分析 1 SPI概述 SPI是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口,是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应_linux spi驱动总线分析

Nginx,nginx-rtmp-module-master搭建直播平台-程序员宅基地

文章浏览阅读511次。Nginx,nginx-rtmp-module-master搭建直播平台_nginx-rtmp-module-master

随便推点

Google Earth影像数据破解之旅-程序员宅基地

文章浏览阅读890次。“Zed, you are so excellent.” 为什么要写这句英文?容我卖个关子稍后再解释。 相信大多数人都体验过Google Earth(简称GE),我对GE最初的印象是在大学宿舍,当时Google刚刚推出GE时,舍友们喜欢在上面找自己的家,惊讶于它的精细程度,并担心是否对中国有一定的安全隐患。 坦白说,当时我对GE上找家没一点兴趣,但不能否认..._googleearth galil算法加密

oracle 11 ex 版本图文安装-程序员宅基地

文章浏览阅读354次。Oracle 11g XE 是 Oracle 数据库的免费版本,支持标准版的大部分功能,11g XE 提供 Windows 和 Linux 版本。做为免费的 Oracle 数据库版本,XE 的限制是:最大数据库大小为 11 GB可使用的最大内存是 1G一台机器上只能安装一个 XE 实例XE 只能使用单 CPU,无法在多CPU上进行分布处理..._oracle 11 xe

iPhone 14 Plus与iPhone 14 Pro:你应该买哪一款-程序员宅基地

文章浏览阅读1k次。iPhone 14 Plus与iPhone 14 Pro:你应该买哪一款?

单片机毕设 stm32智能疫情防控门禁系统_单片机stm32f103c6智能安防系统设计-程序员宅基地

文章浏览阅读336次。 这两年开始毕业设计和毕业答辩的要求和难度不断提升,传统的毕设题目缺少创新和亮点,往往达不到毕业答辩的要求,这两年不断有学弟学妹告诉学长自己做的项目系统达不到老师的要求。为了大家能够顺利以及最少的精力通过毕设,学长分享优质毕业设计项目,今天要分享的是基于Stm32的智能疫情防控门禁系统学长这里给一个题目综合评分(每项满分5分)难度系数:3分工作量:3分创新点:5分针对当前的疫情形势,设计一款智能防疫门禁系统。_单片机stm32f103c6智能安防系统设计

PTA 求给定精度的简单交错序列部分和_pta求给定精度的简单交错序列部分和c语言-程序员宅基地

文章浏览阅读1.1k次。本题要求编写程序,计算序列部分和 1 - 1/4 + 1/7 - 1/10 + ... 直到最后一项的绝对值不大于给定精度eps。输入格式:输入在一行中给出一个正实数eps。输出格式:在一行中按照“sum = S”的格式输出部分和的值S,精确到小数点后六位。题目保证计算结果不超过双精度范围。输入样例1:4E-2输出样例1:sum = 0.854457输入..._pta求给定精度的简单交错序列部分和c语言

从梯度场重建图像_根据梯度重建二值图像-程序员宅基地

文章浏览阅读7.2k次,点赞3次,收藏18次。转自:http://jiashu.blog.163.com/blog/static/1765790352011411222152/很多图像处理的算法是在图像的梯度域完成的。比如图像增强、图像的融合、图像的边缘检测和分割等。在这些算法中或者一部分或者全部在梯度域中完成。 图像梯度的概念很简单。图像梯度可以把图像看成二维离散函数,图像梯度其实就是这个二维离散函数的求导:_根据梯度重建二值图像

推荐文章

热门文章

相关标签