第2章_瑞萨MCU零基础入门系列教程之面向过程与面向对象

本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写,需要的同学可以在这里获取: https://item.taobao.com/item.htm?id=728461040949

配套资料获取:https://renesas-docs.100ask.net

瑞萨MCU零基础入门系列教程汇总: https://blog.csdn.net/qq_35181236/article/details/132779862


第2章 面向过程与面向对象

本章目标

  • 理解面向过程与面向对象编程方法的特点
  • 锻炼面向对象编程的能力

2.1 特性对比

2.1.1 概念介绍

学习单片机开发时,我们写的第 1 个程序通常是点灯,示例代码如下:

// 点灯
void main()
{while (1){led_on();delay_ms(500);led_off();delay_ms(500);}
}

在 main 函数中是一个 while 循环,首先调用 led_on()函数点亮 LED,然后延迟一阵子,接着调用 led_off()熄灭 LED,最后延迟一阵子。

这个程序就是面向过程的,它的编程方法是:先分析出解决问题所需的步骤,然后针对每一步骤编写函数,最后依次调用这些函数。

对于面向对象,最初级的理解就是使用结构体或类来封装硬件的操作(C 语言中没有类,就使用结构体),示例代码如下:

// 点灯
struct led
{void (*on)(void);void (*off)(void);
};static ra_led_on(void)
{// 点亮 LED
}static ra_led_off(void)
{// 熄灭 LED
}static struct led g_led = {.on = ra_led_on,.off = ra_led_off,
};void main()
{while (1){g_led.on();delay_ms(500);g_led.off();delay_ms(500);}
}
  • 第 2~5 行:对于 LED,它是一个“对象”,抽象出一个结构体“struct led”,里面有 2 个函数指针。
  • 第 18~19 行,定义了一个结构体变量 g_led,填充了结构体里的 2 个函数指针,让它们指向具体的函数。
  • 第 23 行开始的 main 函数,跟面向过程的 main 函数类似,只不过操作 LED 时是调用结构体 g_led 的函数指针。

上述程序就是面向对象的,它的编程方法是:把问题中的事务分解成各个对象,在对象里描述事务的属性、行为,最后使用对象来解决问题。

上述面向对象的程序过于粗浅,其实对于 main 函数里实现的功能也可以抽象为一个结构体。比如可以抽象出一个结构体“struct business”,表示“业务”。示例代码如下:

struct business{void (*run)(void);
};void led_blink(void)
{while (1){g_led.on();delay_ms(500);g_led.off();delay_ms(500);}
}struct business g_business = {.run = led_blink,
};void main()
{g_business.run();
}
  • 第 1~3 行,把业务抽象为结构体“struct business”,里面有一个函数指针 run。
  • 第 5~14 行,实现了 LED 的闪烁功能。
  • 第 16~18 行,定义了一个“struct business”结构体 g_business。
  • 第 20~23 行,在 main 函数里调用 g_business 结构的 run 函数指针。

我们把程序分解成了“业务”、“LED”,在实现“业务”时需要操作硬件“LED”。

2.1.2 优缺点对比

使用面向过程编写程序时,符合人类的认知过程,比较自然。它的效率也比较高,因为省去了结构体的初始化,调用函数时也比较直接,无需通过结构体进行调用。

使用面向过程编写程序时,更容易维护、复用、扩展。

怎么理解面向对象的优点呢?

假设有一个产品更新了 2 代,第 1 代使用 I2C 接口的屏幕,第 2 代使用 SPI 接口的屏幕。怎么使用一套代码支持这两代产品?有三种方法:宏开关、使用条件判断、使用结构体。

我们要实现在屏幕上显示字符串的函数 lcd_print,使用宏开关时示例代码如下:

// LCD
#ifdef LCD_VER1
void i2c_lcd_print(char *str)
{// 在 I2C 屏幕上显示字符串
}
#else
void spi_lcd_print(char *str)
{// 在 SPI 屏幕上显示字符串
}
#endifvoid lcd_print(char *str)
{
#ifdef LCD_VER1i2c_lcd_print(str);
#elsespi_lcd_print(str);
#endif
}

对于第 14 行的 lcd_print 函数,当定义宏 LCD_VER1 时,它调用 i2c_lcd_print 操作第1 代的屏幕;当没有定义宏 LCD_VER1 时,它调用 spi_lcd_print 操作第 2 代的屏幕。这种方法的优点是编译出来的可执行程序比较小,因为它只支持 1 种屏幕,要么支持第 1 代要么支持第 2 代。缺点是:同一个可执行程序,不能既支持第 1 代屏幕也支持第 2 代屏幕。

为了能同时支持两代屏幕,假设可以读到屏幕的型号,那么可以使用条件判断的方法,示例代码如下:

// LCD
void i2c_lcd_print(char *str)
{// 在 I2C 屏幕上显示字符串
}void spi_lcd_print(char *str)
{// 在 SPI 屏幕上显示字符串
}void lcd_print(char *str)
{if (read_lcd_type() == VER1)i2c_lcd_print(str);elsespi_lcd_print(str);
}

第 14 行的代码里,先读取屏幕的型号,后面根据型号决定调用 i2c_lcd_print 还是spi_lcd_print。

使用条件判断的方法可以同时支持两代屏幕,但是有一个缺点:效率低。如果需要实现多个屏幕的操作函数,比如 lcd_on、lcd_off、lcd_rotate 等等函数,这些函数内部都要进行型号的判断。如果要支持更多种屏幕,那么就需要更多的条件判断,比如:

void lcd_print(char *str)
{int type = read_lcd_type();if (type == VER1)i2c_lcd_print(str);else if (type == VER2)spi_lcd_print(str);else if (type == VER3)usb_lcd_print(str);......
}

这样写出来的代码,既低效又丑陋。最好的办法就是使用结构体,初始化时指定具体函数,以后直接调用,示例代码如下:

// 使用结构体操作 LCD
struct lcd{void (*on)(void);void (*off)(void);void (*print)(const char *str);
};// 根据 LCD 的型号设置 struct lcd 结构体的函数指针
void lcd_init(struct lcd *plcd)
{int type = read_lcd_type();if (type == VER1){plcd->on = i2c_lcd_on;plcd->off = i2c_lcd_off;plcd->print = i2c_lcd_print;}else if (type == VER2){plcd->on = spi_lcd_on;plcd->off = spi_lcd_off;plcd->print = spi_lcd_print;}else if (type == VER3){plcd->on = usb_lcd_on;plcd->off = usb_lcd_off;plcd->print = usb_lcd_print;}......
}// 使用
struct lcd g_lcd;
void main(void)
{lcd_init(&g_lcd);              // 初始化g_lcd.on();                    // 启动 LCDg_lcd.print("www.100ask.net"); // 显示文字
}
  • 第 2~6 行,把屏幕抽象为一个结构体“struct lcd”,里面有 on、off、print 等函数指针。
  • 第 9~31 行,根据屏幕的型号设置结构体,让它的函数指针指向具体屏幕的操作函数。
  • 第 35 行的 main 函数,在初始化 g_lcd 后就可以直接使用了,效率更高。

使用面向对象的思想,把硬件的操作封装在一个结构体里,甚至把一个业务的操作封装在一个结构体里。初次接触这种风格的代码时,不容易理解。特别是在比较复杂的系统里,这些封装的层次更多,层层封装和多次跳转之下程序更难理解。但是一旦理解之后,就会发现程序设计的精妙。一旦习惯了面向对象的编程方法,你就不会再使用面向过程的编程方法。

2.2 面向对象的程序设计方法

在《代码大全》第 5 章中,把程序设计分为这几个层次:

  • 第 1 层:软件系统,就是整个系统、整个程序
  • 第 2 层:分解为子系统,比如可以把整个程序拆分为:输入系统、显示系统、业务系统
  • 第 3 层:分解为类,在 C 语言里没有类,可以分解为结构体
  • 第 4 层:分解为子程序,就是实现那些结构体(实现里面的函数指针)

2.2.1 分解为子系统

一个系统,必定可以分为这 3 大子系统:输入系统、处理系统、输出系统。

image01

这种拆分方法过于粗糙,在复杂场景里需要进一步拆分。比如对于输入子系统,如果产品有用户输入、传感器、远程控制等功能,那么输入子系统可以拆分为:用户输入子系统、传感器子系统、远程控制子系统,如下:

image02

比如对于输出子系统,可能会涉及显示、操作各类设备、数据保存,那么就可以细分为:显示子系统、设备操作、数据子系统,如下:

image03

对于中间的处理子系统,它根据输入,提供输出。不同的产品有不同的处理逻辑,对于智能家居产品,它根据用户按键、或者根据预设时间、或者根据远程控制命令,去操作各类家居产品;对于工业机器人,它根据各类信号,执行特定的动作。处理子系统是一个集成者:根据业务逻辑,采集左侧各类输入子系统的数据,操作右侧各类输出子系统。以智能家居产品为例,处理子系统又可以细分为:用户关系处理子系统、设备控制子系统、配置子系统等

等,如下:

image-20230815214356726

2.2.2 分解为结构体

怎么实现某个子系统?关键在于抽象出合适的结构体。比如对于用户输入子系统,可以对外提供两个接口函数:初始化、获得按键数据。关键就在于怎么表示“按键数据”。对于用户输入,来源有多种:按键、鼠标、触摸屏。使用怎样的结构体,能统一地描述这些不同来源的输入事件?可以参考 Linux 中的代码,在 Linux 中有如下结构体:

struct input_event{struct timeval time;__u16 type;__u16 code;__s32 value;
};
  • 第 2 行的 time,表示事件发生的时间。
  • 第 3 行 type 表示事件类型,有下面这些取值,EV_KEY 表示按键,EV_REL 表示相对位移比如鼠标,EV_ABS 表示绝对位移比如触摸屏。
#define EV_KEY 0x01
#define EV_REL 0x02
#define EV_ABS 0x03
  • 第 4 行 code 表示编码,比如按键有 A、B、C 等按键的编码值,鼠标有 X、Y 等方向的编码值,触摸屏有 X、Y、Z 等方向的编码值。
  • 第 5 行 value 表示取值,比如对于按键:0 表示松开,1 表示按下;对于鼠标,它就表示偏移值;对于触摸屏,它就表示触点坐标。

举几个例子,比如按下键盘上的 A 键时,怎么使用 input_event 结构体表示它?如下:

{time, EV_KEY, KEY_A, 1}

往右边移动鼠标 100 个单位,往下移动鼠标 200 个单位,怎么使用 input_event 结构体表示它?需要用 2 个 input_event 结构体,如下:

{time, EV_REL, REL_X, 100}{time, EV_REL, REL_Y, 200}

在触摸屏(100, 200)的位置点击、松开,怎么使用 input_event 结构体表示它?需要用3 个 input_event 结构体,如下:

{time, EV_ABS, ABS_X, 100}{time, EV_ABS, ABS_Y, 200}{time, EV_ABS, ABS_Z, 1} // 表示按下

考虑完善的结构体,有助于涉及实现子系统,有了 input_event 结构体后,用户输入子系统对外提供的接口就可以如下定义:

int Init_User_Input_System(void);int Get_Input_Event(struct input_event *ptEvent);

假设需要同时支持多种用户输入设备,那么还可以继续抽象出其他结构体,比如用来描述输入设备的“struct input_dev”,如下:

struct input_dev {const char *name;int (*Init)(void);};

对于每一个输入设备,都会为它构建一个“struct input_dev”结构体。

2.2.3 分解为子程序

还是以用户输入系统为例,假设需要支持按键、鼠标、触摸屏,那么可以构造对应的 3个“struct input_dev”结构体,给里面的 Init 函数指针提供按键、鼠标、触摸屏的初始化函数,最重要的是:分别注册中断处理函数。

  1. 在按键中断处理函数里,消除抖动、构造“struct input_event”结构体,并把它写入一个环形缓冲区。
  2. 在鼠标中断处理函数里,解读鼠标数据、构造“struct input_event”结构体,并把它写入一个环形缓冲区。
  3. 在触摸屏中断处理函数里,通过 I2C 等接口读取触点数据、构造“struct input_event”结构体,并把它写入一个环形缓冲区。

程序框图如下:

image04

如果使用多任务系统,那么可以把上图的环形缓冲区替换为消息队列,各类中断程序写消息队列时可以唤醒等待输入事件的任务。

2.2.4 结构体的继承

在单片机软件开发中,基本使用的是 C 语言,很少使用 C++。C 语言是面向过程的语言,但是我们可以使用结构体实现面向对象的开发。但是结构体跟类,有很大的差别。

在 C++中实现类时,可以有继承关系。在 C 语言中使用结构体时,也可以使用继承(在RT-Thread 中大量使用到结构体的继承)。比如人类、狗都属于生物,生物都有性别、体重等属性,那么可以如下定义生物、人类、狗三个结构体,人类和狗都继承了生物的特性:

struct organism
{int sex;    // 性别int weight; // 体重
};struct person
{struct organism parent; // 父类const char *school;     // 毕业学校
};struct dog
{struct organism parent; // 父类const char *species;    // 种类
};

使用结构体的继承时,“父结构体”必须是“子结构体”的第一个成员。这样有一些好处,比如要实现一个打印体重的函数,可以只实现一个版本,示例代码如下:

void PrintWeight(struct organism *p)
{printf("weight : %d\n", p->weight);
}void main(void)
{struct person per = {{1, 100}, "USTC"};struct person dog = {{1, 10}, "Husky"};PrintWeight(&per);PrintWeight(&dog);
}
  • 第 1 行实现了一个“打印生物体重”的函数。
  • 第 8、9 行分别定义了一个“人”、“狗”的结构体。
  • 第 11、12 行,可以使用同一个函数“PrintWeight”来打印人的体重、打印狗的体重。如果不使用结构体的继承,那么就需要分别实现人、狗的打印函数,示例代码如下:
struct person{int sex;            // 性别int weight;         // 体重const char *school; // 毕业学校
};struct dog{int sex;             // 性别int weight;          // 体重const char *species; // 种类
};void PrintPersonWeight(struct person *p)
{printf("weight : %d\n", p->weight);
}void PrintDogWeight(struct person *p)
{printf("weight : %d\n", p->weight);
}
void main(void)
{struct person per = {{1, 100}, "USTC"};struct person dog = {{1, 10}, "Husky"};PrintPersonWeight(&per);PrintDogWeight(&dog);
}

2.2.5 结构体的函数指针

结构体的成员里,可以使用变量来描述属性,使用函数指针来描述方法,比如 LED 可以抽象出如下结构体:

struct led {void (*on)(void);void (*off)(void);
};

对于使用芯片引脚控制的 LED,即使引脚不同,操作函数也是类似的。那怎么分辨使用哪个引脚呢?需要在“struct led”里添加属性以描述引脚,如下:

struct led {int pin; // 引脚void (*on)(void);void (*off)(void);
};

但是,在 on、off 指针对应的函数里,怎么引用到属性 pin?C 语言的结构体里不能想C++的类那样使用 this 指针来引用自己,所以结构体还需要改进,在 on、off 函数指针里增加一个参数:增加结构体本身的指针,如下:

struct led{int pin; // 引脚void (*on)(struct led *p);void (*off)(struct led *p);
};

使用“struct led”的示例代码如下:

// 点灯
struct led{int pin; // 引脚void (*on)(struct led *p);void (*off)(struct led *p);
};static ra_led_on(struct led *p)
{// 点亮 LED// 操作引脚 p->pin
}static ra_led_off(struct led *p)
{// 熄灭 LED// 操作引脚 p->pin
}static struct led g_led = {.pin = 100,.on = ra_led_on,.off = ra_led_off,
};void main()
{while (1){g_led.on(&g_led);delay_ms(500);g_led.off(&g_led);delay_ms(500);}
}
  • 第 8、11 行实现的函数,需要从参数“struct led *p”获得要操作哪个引脚:p->pin。
  • 第 21~25 行实现的结构体,需要指定属性 pin:使用哪个引脚。
  • 第 31、33 行,调用 g_led.on、g_led.off 函数指针时,需要传输 g_led 的地址。

2.2.6 程序设计原则

怎样把整个系统拆分多个子系统?在实现子系统时怎样抽象出各个结构体?原则是:高内聚,低耦合。

  • 高内聚:一个子系统(或者一个结构体),尽可能只完成一个功能,即最大限度地耦合。
  • 低耦合:一个子系统(或者一个结构体)的实现,尽可能少地调用到另一个子系统(或另一个结构体)的功能。

简单地说,就是尽可能让一个子系统(或一个结构体)的功能比较单一,减少对其他子系统(或其他结构体)的依赖。增强内聚度、降低耦合度的方法:

  • 基于接口编程,隐藏内部实现的细节
  • 模块只对外暴露最小限度的接口,形成最低的依赖关系
  • 只要对外接口不变,模块内部的修改不得影响其他模块
  • 删除一个模块,应当只影响有依赖关系的其他模块,而不应该影响其他无关部分
  • 模块的功能尽可能的单一
  • 接口函数在头文件中声明,内部函数不要放在头文件里声明
  • 接口函数在 C 文件中实现,内部函数定义为 static 函数,内部变量定义为 static
  • 尽量少用全局变量,要使用全局变量的话不要直接访问,使用函数来访问
  • 调用者只需要包含头文件
  • 函数代码不要太长,功能太复杂的话拆分为多个子函数

本章完

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/132535.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用C#开发163邮件发送功能

创建SMTP服务器(发送邮件需要SMTP服务器代发) 这里介绍创建网易SMTP(SMTP是邮件通讯格式)服务器: 1.先注册一个163网易邮箱 2.注册成功后登陆该邮箱 3.在该邮箱中找到设置>POP3/SMTP/IMAP点击进入,如下…

【C语言】每日一题(半月斩)——day2

目录 一.选择题 1、以下程序段的输出结果是( ) 2、若有以下程序,则运行后的输出结果是( ) 3、如下函数的 f(1) 的值为( ) 4、下面3段程序代码的效果一样吗( ) 5、对于下面的说法,正确的是&#xf…

OPENCV实现人类识别(包括眼睛、鼻子、嘴巴)

人脸识别步骤 # -*- coding:utf-8 -*- """ 作者:794919561 日期:2023/9/14 """ import cv2 import numpy as np # load xml face_xml = cv2.CascadeClassifier(F:\\learnOpenCV\\opencv\\data\\haarcascades\\haarcascade_frontalface_defaul…

怎么获取别人店铺的商品呢?

jd.item_search_shop(获得店铺的所有商品) 为了进行电商平台 的API开发,首先我们需要做下面几件事情。 1)开发者注册一个账号 2)然后为每个JD应用注册一个应用程序键(App Key) 。 3)下载JDAPI的SDK并掌握基本的API…

使用“vue init mpvue/mpvue-quickstart“初始化mpvue项目时出现的错误及解决办法

当使用"vue init mpvue/mpvue-quickstart"初始化 mpvue 项目时出现 "vue-cli Failed to download repo mpvue/mpvue-quickstart: connect ETIMEDOUT IP地址"原因是 github 的 IP 解析失败,连接超时 解决办法:更改最新的 github 的 …

NoSQL之 Redis介绍与配置

目录 一、关系数据库和非关系数据库概述 1、关系型数据库 2、非关系型数据库 二、关系数据库和非关系数据库的区别 1、数据存储格式不同 2、扩展方式不同 3、对事务的支持不同 三、非关系数据库产生背景 1、总结 四、Redis简介 1、 Redis的单线程模式 2、Redis优点…

图的基本知识

图 一、图的定义和基本术语二、图的存储结构(1)数组(邻接矩阵表示法)(2)数组(邻接矩阵)的实现(3)邻接表(链式表示法)(4&am…

释放潜能!RunnerGo:性能测试的全新视角

在数字化时代,性能测试已成为企业持续发展的关键一环。但面对繁杂的工具和流程,很多企业却陷入了无从选择的困境。现在,一款名为RunnerGo的全新性能测试工具正悄然崭露头角。 RunnerGo,一款由国内开发者自主研发的全栈式性能测试…

模拟实现C语言--strlen函数

模拟实现C语言–strlen函数 模拟实现C语言--strlen函数一、strlen函数是什么?二、strlen函数的模拟实现2.1 计数器方式实现strlen函数2.2 不创建临时变量计数器方式实现strlen函数2.3 指针-指针方式实现strlen函数 三、strlen函数的返回类型 一、strlen函数是什么&a…

使用Spring Gateway为对象存储系统MinIo和kkFileView文档预览增加登录验证

文章目录 1、kkfileview下载部署1.1、安装包部署运行1.1.1、物理机或虚拟机上运行1.1.2、Docker容器环境环境运行 1.2、接入说明 2、使用Spring Gateway增加登录认证2.1、网关实现代码2.2、文件服务实现代码2.3、Demo运行效果 官网介绍:kkFileView为文件文档在线预览…

易点易动固定资产管理系统:为企业提供高效资产管理的必备利器

在如今竞争激烈的商业环境中,企业对于固定资产的管理显得尤为重要。然而,传统的手工管理方式已经无法满足企业对于高效、准确、智能化管理的需求。因此,引入一款先进的固定资产管理系统是必不可少的。易点易动固定资产管理系统作为一款领先的…

C#自定义控件组件实现Chart图表(多Y轴,选择图例加粗,选择放大,缩放,点击查看信息等功能)

先看看ECharts的效果 C# 工具箱里的Chart控件就不演示了,很多效果没办法做出来,做出来效果也很不理想。所以,需要自己去手动实现工具箱里的Chart没办法实现的效果; 先看看实现后的效果 绑定数据 点击图表 点击右侧图例加粗 选择放大 右键 点击缩小,恢复

重建大师提交空三后引擎状态是等待,怎么开启?

答:图片中这是在自由网空三阶段,整个AT都是等待中,可以修改任务目录和监控目录看一下,先设置引擎,再提交空三。

建站系列(三)--- 网络协议

目录 相关系列文章前言一、定义二、术语简介三、协议的组成要素四、网络层次划分五、常见网络协议划分六、常用协议介绍(一)TCP/IP(二)HTTP协议(超文本传输协议)(三)SSH协议 相关系列…

DC/DC开关电源学习笔记(五)开关电源的主要技术指标

(五)开关电源的主要技术指标 1.输入参数2.输出参数3.效率4.电压调整率和负载调整率5.动态特性:负载突变时输出电压的变化6.电源启动时间(Set-Up Time)与保持时间(Hold-Up Time)1.输入参数 输入电压大小,交流还是直流,相数,频率等。 2.输出参数 输出功率,输出电压,输出…

算法-26. 删除有序数组中的重复项-⭐

给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。 考虑 nums 的唯一元素的数量为 k ,你需要做…

2023-9-10 集合-Nim游戏

题目链接&#xff1a;集合-Nim游戏 #include <iostream> #include <cstring> #include <algorithm> #include <unordered_set>using namespace std;const int N 110, M 10010;int n, m; int s[N], f[M];int sg(int x) {if(f[x] ! -1) return f[x];//…

NeuroFlash:AI文章写作与生成工具

【产品介绍 ​ 】 名称 NeuroFlash 上线时间 2015 具体描述 Neuroflash是一款基于人工智能的文本和图像生成器&#xff0c;可以帮助用户快速创建高质量的内容。Neuroflash拥有超过100种短文和长文的文本类型&#xff0c;涵盖了各种营销场景和需求。只需要输入简单的指示&#…

云计算与虚拟化

一、概念 什么是云计算&#xff1f; 云计算&#xff08;cloud computing&#xff09;是分布式计算的一种&#xff0c;指的是通过网络“云”将巨大的数据计算处理程序分解成无数个小程序&#xff0c;然后&#xff0c;通过多部服务器组成的系统进行处理和分析这些小程序得到结果…

【LeetCode】210. 课程表 II——拓扑排序

题目链接&#xff1a;210. 课程表 II 题目描述&#xff1a; 现在你总共有 numCourses 门课需要选&#xff0c;记为 0 到 numCourses - 1。给你一个数组 prerequisites &#xff0c;其中 prerequisites[i] [ai, bi] &#xff0c;表示在选修课程 ai 前 必须 先选修 bi 。 例如…