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;  
}
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
高质量C++/C编程指南 目 录 前 言 6 第1章 文件结构 11 1.1 版权和版本的声明 11 1.2 头文件的结构 12 1.3 定义文件的结构 13 1.4 头文件的作用 13 1.5 目录结构 14 第2章 程序的版式 15 2.1 空行 15 2.2 代码行 16 2.3 代码行内的空格 17 2.4 对齐 18 2.5 长行拆分 19 2.6 修饰符的位置 19 2.7 注释 20 2.8 类的版式 21 第3章 命名规则 22 3.1 共性规则 22 3.2 简单的Windows应用程序命名规则 23 3.3 简单的Unix应用程序命名规则 25 第4章 表达式和基本语句 26 4.1 运算符的优先级 26 4.2 复合表达式 27 4.3 if 语句 27 4.4 循环语句的效率 29 4.5 for 语句的循环控制变量 30 4.6 switch语句 30 4.7 goto语句 31 第5章 常量 33 5.1 为什么需要常量 33 5.2 const 与 #define的比较 33 5.3 常量定义规则 33 5.4 类中的常量 34 第6章 函数设计 36 6.1 参数的规则 36 6.2 返回值的规则 37 6.3 函数内部实现的规则 39 6.4 其它建议 40 6.5 使用断言 41 6.6 引用与指针的比较 42 第7章 内存管理 44 7.1内存分配方式 44 7.2常见的内存错误及其对策 44 7.3指针与数组的对比 45 7.4指针参数是如何传递内存的? 47 7.5 free和delete把指针怎么啦? 50 7.6 动态内存会被自动释放吗? 50 7.7 杜绝“野指针” 51 7.8 有了malloc/free为什么还要new/delete ? 52 7.9 内存耗尽怎么办? 53 7.10 malloc/free 的使用要点 54 7.11 new/delete 的使用要点 55 7.12 一些心得体会 56 第8章 C++函数的高级特性 57 8.1 函数重载的概念 57 8.2 成员函数的重载、覆盖与隐藏 60 8.3 参数的缺省值 63 8.4 运算符重载 64 8.5 函数内联 65 8.6 一些心得体会 68 第9章 类的构造函数、析构函数与赋值函数 69 9.1 构造函数与析构函数的起源 69 9.2 构造函数的初始化表 70 9.3 构造和析构的次序 72 9.4 示例:类String的构造函数与析构函数 72 9.5 不要轻视拷贝构造函数与赋值函数 73 9.6 示例:类String的拷贝构造函数与赋值函数 73 9.7 偷懒的办法处理拷贝构造函数与赋值函数 75 9.8 如何在派生类中实现类的基本函数 75 9.9 一些心得体会 77 第10章 类的继承与组合 78 10.1 继承 78 10.2 组合 80 第11章 其它编程经验 82 11.1 使用const提高函数的健壮性 82 11.2 提高程序的效率 84 11.3 一些有益的建议 85

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值