C语言一些编程思想


  由于笔者工作是属于嵌入式方面,所以文章的C的编程思想跟算法,数据结构无很大关系,主要是功能应用的使用。

常用的编程思想

模块化编程思想:

回主目录
 模块化编程思想将整个系统功能代码分割成小的、互不依赖的功能,通过定义函数和模块化的方式进行各个功能实现。这样做有助于复用和代码维护,可让多个人同时开发一个项目。

/*输入一个数,获取它的平方值*/
/*函数功能:计算平方*/
int square(int num) {
    return num * num;
}
/*函数功能:打印计算值*/
void printSquare(int num) {
    int result = square(num);
    printf("平方值:%d\n", result);
}
// 主函数
int main() {
    int number = 5;
    printSquare(number);
    return 0;
}

面向对象思想:

 尽管C语言中没有类和对象的概念,但你可以使用结构体来模拟对象,并通过函数指针实现类似于类的行为和接口。
 例如,假设你要实现一个简单的字符串处理库,包括创建字符串、拷贝字符串、连接字符串等操作。你可以使用结构体来表示字符串对象,并定义与之相关的操作函数,如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char *data;
    int length;
} String;

String* create_string(const char *data) {
    String* str = malloc(sizeof(String));
    str->length = strlen(data);
    str->data = malloc((str->length + 1) * sizeof(char));
    strcpy(str->data, data);
    return str;
}

void destroy_string(String* str) {
    free(str->data);
    free(str);
}

void print_string(const String* str) {
    printf("String: %s\n", str->data);
}

int main() {
    String* str = create_string("Hello, World!");
    print_string(str);
    destroy_string(str);
    return 0;
}

状态机思想

可以更好的描述系统的行为,更方便的理解和管理程序。

简介

回主目录
 状态机(State Machine)在嵌入式软件编程中是很常见的一种编程思想,它是一种描述和处理系统行为的模型,将系统划分为一组离散的状态,定义了状态之间的转换条件和操作。
 状态机由状态(State)状态转换条件(Transition)动作(Action)初始状态(Initial State)初始状态(Initial State)组成。

类型
 状态机可以分为两种类型:有限状态机(FSM)和无限状态机(USM)。

  • 有限状态机(FSM)是最常见的状态机实现方式。它由确定的状态、预定义的转移条件和对应的动作组成。程序根据当前状态和输入条件,进行状态转移和执行相应的动作。
  • 无限状态机(USM)是一种更灵活的状态机实现方式,状态和转移条件可以动态生成和修改。USM通常使用状态模式进行实现,状态之间通过递归调用实现转移,可以处理更复杂的状态转移逻辑。

实现方式:

  1. 条件逻辑编码法:这种方法通过编写代码来直接实现状态机的转换和动作。一般使用switch语句或、if-else语句或者函数指针等方式来处理不同的事件和状态转换。
  2. 表驱动法:这种方法利用数据结构来存储状态及其对应的转换条件和动作。通常使用状态转换表(State Transition Table)或状态转换图(State Transition Diagram)来描述状态机的行为。在C语言中,可以使用表格(如二维数组)来表示状态及其转换。通过查表即可根据当前状态和输入事件找到下一个状态,执行相应的动作。
  3. 状态模式:使用面向对象的方式,将状态抽象为类,每个状态类实现自己的行为和状态迁移逻辑。

基本步骤

  1. 定义状态:根据具体系统需求,确定状态的数据和含义。每个状态都有一个标识符来表示。

  2. 定义状态转换条件:确定触发状态转换的条件,即从一个状态转移到另一个状态所需的条件。如输入事件、定时器等。

  3. 定义动作:确定每个状态下需要执行的动作操作,如更新数据、发送消息等。

  4. 实现状态机逻辑:将状态、转换条件和动作组织起来,使用代码实现状态机逻辑。可以定义状态机的数据结构和函数,数据结构通常包含当前状态、状态转换表等信息。函数可以用来处理状态转换、执行状态转换动作等。

  5. 测试和调试:通过测试验证状态机的正确性和稳定性,修复存在的问题。

框架

回主目录

一、条件逻辑编码

模型一:

/*定义状态A,B,C,每个状态标识一个特定的行为或状态*/
typedef enum{
    STATE_A,
    STATE_B,
    STATE_C,
    // ...
} State;
/*定义状态转换条件,开始和停止*/
typedef enum{
    EVENT_START,
    EVENT_STOP
} Event;
/*实现状态机逻辑*/
void processEvent(Event event,State *currentState) 
{
	/*根据当前的状态执行对应的动作*/
    switch (*currentState) { 
        case STATE_A:
            if (event == EVENT_START)
            {
                // 状态转移到状态B,执行状态转移时会产生的动作
                *currentState = STATE_B;
                ……	
            }
            break;
        case STATE_B:
            if (event == EVENT_STOP)
            {
                //状态转移到状态C,执行状态转移时会产生的动作
                *currentState = STATE_C;
				……	
            }
            break;
        case STATE_C:
            // 停止
            break;
        default:
            // 错误处理
            printf("错误的状态\n");    /*根据语法规范,错误时最好有个日志输出,方便Debug*/
            break;
    }
}

int main()
{
    State currentState = STATE_A; 
    processEvent(EVENT_START, &currentState); //此时跳转到了状态B
    processEvent(EVENT_STOP,  &currentState); //此时跳转到了状态C
    return 0;
}

模型二:

/*定义状态A,B,C,每个状态标识一个特定的行为或状态*/
typedef enum {
    STATE_A,
    STATE_B,
    STATE_C,
    // ...
} State;
/*实现状态机逻辑*/
int main()
{
	while(1)
	{
	  switch (current_state)
	  {
	    case STATE_A:
	       // 处理状态A的逻辑
	       // ...
	       if(/* 满足某个条件 */)
	       {
	          /*触发状态转换条件,状态跳转*/
	          current_state = STATE_B;
	       } 
	       else if(/* 满足某个条件 */)
	       {
	          current_state = STATE_C;
	       }
	       break;
	    case STATE_B:
	        // 处理状态B的逻辑
	        // ...
	        current_state = STATE_C;
	        break;
	    case STATE_C:
	       // 处理状态C的逻辑
	       // ...
	       current_state = STATE_A;
	       break;
	       // ...
	  }
	}
}

二、表驱动法

回主目录
 表驱动法通过使用一张状态转换表来实现状态机。这张表保存了状态转换的信息,程序会根据当前状态和输入来查找对应的状态转换,并执行相应的操作。

#include <stdio.h>
/*定义状态A,B,C,每个状态标识一个特定的行为或状态*/
typedef enum {
    State_A,
    State_B,
    State_C
} State;
// 定义输入事件
typedef enum {
    Event_1,
    Event_2
} Event;

// 定义状态转换表
typedef struct {
    /*当前状态-当前事件-下个状态-动作*/
    State current_state;
    Event current_event;
    State next_state;
    void (*action)();
} Transition;

// 定义状态转换表数组
Transition transitions[] = {
    /*当前状态-当前事件-下个状态-动作*/
    {State_A, Event_1, State_B, action1},
    {State_A, Event_2, State_C, action2},
    // ... 其他状态转换
};

/*根据当前状态和输入事件在定义的表里面查找相应状态转换*/
Transition* findTransition(State current_state, Event current_event) {
    for (int i = 0; i < sizeof(transitions) / sizeof(transitions[0]); i++) {
        if (transitions[i].current_state == current_state &&
            transitions[i].current_event == current_event) {
            return &transitions[i];
        }
    }
    return NULL;
}

// 执行相应的操作
void action1() {
    printf("Performing action 1.\n");
}

void action2() {
    printf("Performing action 2.\n");
}

int main() {
    /*状态和事件初始化*/
    State current_state = State_A;
    Event current_event = Event_1;
    /*查找状态转换,并执行相应的操作*/
    Transition* transition = findTransition(current_state, current_event);
    if (transition != NULL) {
        /*如果表中存在对应的数据,则更新状当前状态*/
        current_state = transition->next_state;
        /*执行转换动作*/
        transition->action();      
    }
    return 0;
}

三、状态模式

回主目录
 使用面向对象的方式,将状态抽象为类,但在C语言中,并不能直接使用面向对象的语法来实现状态机。但是可以通过结构体和函数指针来模拟面向对象的思想,如下所示:

#include <stdio.h>

// 定义状态机结构体
typedef struct {
    void (*state)();
    void (*transition)(Event);
} StateMachine;

// 定义状态函数
void stateA(Event event);
void stateB(Event event);
void stateC(Event event);

// 定义状态机对象
StateMachine state_machine = {stateA, NULL};

// 定义状态转换函数
void transition(Event event) 
{
    state_machine.state(event);
}

// 定义状态函数的实现
void stateA(Event event) 
{
    if (event == Event_1) 
    {
        printf("Performing action 1.\n");
        state_machine.state = stateB;
    } 
    else if (event == Event_2) 
    {
        printf("Performing action 2.\n");
        state_machine.state = stateC;
    }
}

void stateB(Event event) 
{
    // 状态B的实现...
}
void stateC(Event event) 
{
    // 状态C的实现...
}
int main() 
{
    Event current_event = Event_1;
    // 执行状态转换
    transition(current_event);
    return 0;
}

寄存器组封装

回主目录
 为了程序在编写过程中更方便的驱动底层寄存器,可以将共通的部分用结构体组合起来。以STM32标准库里面的代码为例:GPIOB寄存器组封装
1.封装总线和外设基地址

/*外设基地址*/
#define PERIPH_BASE ((unsigned int) 0x40000000)
/*总线基地址*/
#define APB1PERIPH_BASE    PERIPH_BASE 
#define APB2PERIPH_BASE    (PERIPH_BASE + 0x00010000)
/*GPIO外设基地址*/
#define GPIOB_BASE  (APB2PERIPH_BASE + 0x0C00)
/*寄存器基地址,以GPIOB为例*/
#define GPIOB_CRL   (GPIOB_BASE + 0x00)
#define GPIOB_CRH   (GPIOB_BASE + 0x04)
#define GPIOB_IDR   (GPIOB_BASE + 0x08)
#define GPIOB_ODR   (GPIOB_BASE + 0x0C)
#define GPIOB_BSRR  (GPIOB_BASE + 0x10)
#define GPIOB_BRR   (GPIOB_BASE + 0x14)
#define GPIOB_LCKR  (GPIOB_BASE + 0x18)
/*对单个寄存器进行操作,如GPIOB_BSRR*/
*(unsigned int *)GPIOB_BSRR = (0x01<<(16+0));  /*将第0位输出低电平*/

2.封装寄存器列表
由于GPIOA~GPIOE都各有一组功能相同的寄存器,
只是地址不一样,可以将上述通过基地址用结构体整合起来。

/*使用结构体对GPIO寄存器组的封装*/
typedef unsigned       int uint32_t; /*无符号32位变量*/
/*GPIO寄存器列表*/
/*因为C语言语法规定,结构体变量的存储空间是连续的,其中32位占4个字节,
  GPIO寄存器列表的个寄存器也是连续的,所以可以如下编写*/
typedef struct{         
    uint32_t CRL;
    uint32_t CRH;
    uint32_t IDR;
    uint32_t ODR;
    uint32_t BSRR;
    uint32_t BRR;
    uint32_t LCKR;
}GPIO_TypeDef;

3.通过结构体指针访问寄存器
 设置完后,直接调用GPIOB即可设置其相关的寄存器组。

GPIO_TypeDef * GPIOx;        /*定义一个GPIO_TypeDef型结构体GPIOx*/
GPIOx = GPIOB_BASE;          /*把指针地址设置为宏GPIOB_BASE地址*/
GPIOx->IDR = 0xFFFF;         /*设置GPIOB寄存器列表中的IDR寄存器*/
uint32_t temp = GPIOx->IDR; /*读取GPIOB寄存器列表中IDR寄存器的值*/
/*再合并结构体指针*/
#define GPIOB   ((GPIO_TypeDef *) GPIOB_BASE)
GPIOB->IDR = 0xFFFF;        /*设置GPIOB寄存器列表中的IDR寄存器*/
uint32_t temp = GPIOB->IDR; /*读取GPIOB寄存器列表中IDR寄存器的值*/

函数参数校验

回主目录
 在C语言中,使用函数进行模块化代码设计是最常用的操作,可以让整个函数看起来更加的清晰明了,但函数的使用,就会存在函数参数调用的值与设想的值不同,从而导致细致的问题,而这种参数值问题,IDE是不怎么会报错的,就像判断语句中,常量在右边,==写成了=,就不会报错,但是逻辑存在问题。这样在函数编写时可用来参数检查,而在正式发布时就不需要这个功能。

1.定义USE_FULL_ASSERT宏来设置检验功能,打开检测功能

#define USE_FULL_ASSERT    1
/**
  * @brief  The assert_param macro is used for function's parameters check.
  * @param  expr: If expr is false, it calls assert_failed function which reports 
  *         the name of the source file and the source line number of the call 
  *         that failed. If expr is true, it returns no value.
  * @retval None
  */
#ifdef  USE_FULL_ASSERT
  #define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------- */
  void assert_failed(uint8_t* file, uint32_t line);
#else
  #define assert_param(expr) ((void)0)
#endif /* USE_FULL_ASSERT */

2.声明参数范围

/** @defgroup GPIO_Exported_Types
  * @{
  */
#define IS_GPIO_ALL_PERIPH(PERIPH) (((PERIPH) == GPIOA) || \
                                    ((PERIPH) == GPIOB) || \
                                    ((PERIPH) == GPIOC) || \
                                    ((PERIPH) == GPIOD) || \
                                    ((PERIPH) == GPIOE) || \
                                    ((PERIPH) == GPIOF) || \
                                    ((PERIPH) == GPIOG))

3.参数检验
 当函数输入的参数不是GPIOA~GPIOG时,会执行“assert_failed”函数,可在该函数中设置特定的错误打印,从而更快的定位错误位置。

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{
	assert_param(IS_GPIO_ALL_PERIPH(GPIOx));      
}

使用void强转类型实现共通

#include <stdio.h>  
  
// 定义结构体  
struct Student {  
    char name[20];  
    int age;  
};  
  
// 定义函数,接受void类型参数  
void printStudent(void* student) {  
    // 将void指针转换为结构体指针  
    struct Student* s = (struct Student*)student;  
    printf("Name: %s, Age: %d\n", s->name, s->age);  
}  
  
int main() {  
    // 创建结构体变量并初始化  
    struct Student stu = {"Alice", 20};  
    // 调用函数并传递结构体变量作为参数  
    printStudent(&stu);  
    return 0;  
}
  • 28
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值