目前国内外Arduino教程普遍采用C (面向过程语言) 来教授Arduino编程,因此大多数Arduino使用者也是用C语言来进行Arduino开发。然而Arduino的魅力很大一部分是来自于丰富的开源库资源。可是这些库都是用C++开发的。这就给很多Arduino使用者造成了困扰。在接触到一个新的Arduino库时,很多人因为不懂C++而对Arduino库的使用感到不知所措
在本章中,我们将使用Arduino讲解面向对象编程的基本知识和概念。通过本章教程学习您将学会如何编写自己的Arduino库以及更好的使用和借鉴他人开发好的Arduino库。
阶段一:类与对象
先贴一段Arduino环境下典型的LED_Blink代码面向对象
// 定义 LED 引脚
const int LED_PIN = 13;
// 定义 LED 类
class LED {
private:
int pin;
//类函数定义的第一种方式:在类中直接写出函数功能代码
public:
LED(int ledPin) {
pin = ledPin;
pinMode(pin, OUTPUT);
}
//类函数也需要在类中先声明
void on();
void off();
};
//类函数定义的第二种方式:在类外通过域作用运算符(::)定义函数功能代码
void LED::on() {
digitalWrite(pin, HIGH);
}
void LED::off() {
digitalWrite(pin, LOW);
}
// 创建 LED 对象
LED led(LED_PIN);
void setup() {
// 无需在此处编写任何代码
}
void loop() {
led.on();
delay(1000);
led.off();
delay(1000);
}
这段代码实现了一个简单的LED闪烁功能。下面是对代码进行分析的逐行解释:
-
定义一个名为LED_PIN的常量,并赋值为13,表示LED连接到Arduino的13号引脚。
-
定义一个名为LED的类,类中包含私有成员变量pin用于存储LED连接的引脚号。类中还包含公有成员函数on()和off(),分别用于打开和关闭LED。在类的构造函数中,将传入的引脚号设置给私有成员变量pin,并使用pinMode()函数将该引脚设置为输出模式。
-
创建一个LED对象led,使用LED_PIN作为构造函数的参数,即将LED对象连接到13号引脚。
-
setup()函数,Arduino环境会在程序初始化时自动调用该函数。在这个例子中,setup()函数为空,因为没有需要初始化的内容。
-
loop()函数,在Arduino环境下会不断重复执行。在这个例子中,我们将LED先打开(调用led.on()),然后使用delay()函数延迟1000毫秒,再将LED关闭(调用led.off())并再次延迟1000毫秒。这样LED就会以1秒的间隔不断闪烁。
总结:这段代码使用了面向对象的方式实现了LED的闪烁效果。通过定义LED类和创建LED对象,简洁了控制LED的方式,我们熟悉了C++语言中关于类和对象的基本样式。
阶段二:多构造函数类
-建立带有多个构造函数的类
-建立带有参数的构造函数
-建立析构函数
#include <Arduino.h>
class Led {
public:
Led();
Led(int userLedPin);
~Led();
void on();
void off();
private:
int ledpin = 2;
};
/*构造函数
* LED对象刚被创建时调用此函数(不带参数)
*/
Led::Led(){
pinMode(2, OUTPUT);
Serial.println("Led对象被创建(不带参数)");
}
/*构造函数
* LED对象刚被创建时调用此函数(带参数)
*/
Led::Led(int userLedPin) {
ledPin = userLedPin;
pinMode(ledPin, OUTPUT);
Serial.println("Led对象被创建(带参数)");
}
/*析构函数
* LED对象被销毁前调用此函数
*/
Led::~Led(){
Serial.println("Led对象被删除");
}
void Led::on(){
digitalWrite(ledPin, HIGH);
}
void Led::off(){
digitalWrite(ledPin, LOW);
}
/*
* 知识点:函数重载
* 两种创建Led对象的方式:不带参数 & 带参数
* 编译器通过传入参数的类型和个数判断该调用哪个构造函数,这是C++的语言特性
*/
Led myLed;
Led myLed2(7);
void setup() {
Serial.begin(115200);
}
void loop() {
myLed.on();
myLed2.on();
delay(1000);
myLed.off();
myLed2.off();
delay(1000);
}
重点学习这段代码中构造函数的内容
在C++中,构造函数是一种特殊的成员函数,用于初始化类的对象。构造函数的名字必须与类的名称相同,且没有返回类型(包括void类型),在类的声明中声明,在类的定义中定义。
构造函数可以有多个重载版本,每个版本接受不同的参数或参数列表,因此可以根据传入的参数的类型和个数来决定具体调用哪个构造函数。
以下是构造函数的一些特点和用法:
-
构造函数在创建对象时自动调用,用于初始化对象的成员变量。
-
构造函数可以有默认参数值,这样在创建对象时可以省略一部分参数。
-
如果没有显式地定义构造函数,编译器会生成一个默认构造函数(无参构造函数),该构造函数将对类的成员变量执行默认初始化操作。
-
如果显式地定义了构造函数,则默认构造函数将被覆盖。
以下是一个例子,展示了一个具有多个构造函数的类的示例:
class MyClass {
public:
// 默认构造函数
MyClass() {
// 进行默认初始化操作
}
// 带参数的构造函数
MyClass(int value) {
// 使用传入的参数进行初始化操作
}
// 带多个参数的构造函数
MyClass(int value1, int value2) {
// 使用传入的参数进行初始化操作
}
// 析构函数
~MyClass(){
}
};
在上面的例子中,MyClass类有三个构造函数:一个无参构造函数、一个带一个参数的构造函数、一个带两个参数的构造函数和一个析构函数。
通过定义不同版本的构造函数,可以根据需要来创建对象,并为对象的成员变量提供不同的初始值。
阶段三:类的封装
– 类的封装操作
– 类的私有成员
– 使用公有成员函数访问私有成员变量
#include <Arduino.h>
class Led {
public:
Led();
Led(int userLedPin);
~Led();
void on();
void off();
int getLedPin();
void setLedPin(int userLedPin);
private:
int ledpin = 2;
};
Led::Led(){
pinMode(2, OUTPUT);
Serial.println("Led对象被创建(不带参数)");
}
Led::Led(int userLedPin) {
ledPin = userLedPin;
pinMode(ledPin, OUTPUT);
Serial.println("Led对象被创建(带参数)");
}
Led::~Led(){
Serial.println("Led对象被删除");
}
void Led::on(){
digitalWrite(ledPin, HIGH);
}
void Led::off(){
digitalWrite(ledPin, LOW);
}
/*
* 通过公有成员函数getLedPin得知私有成员变量ledPin的值
*/
int Led::getLedPin(){
return ledPin;
}
/*
* 通过公有成员函数setLedPin设置私有成员变量ledPin的值
*/
void Led::setLedPin(int userLedPin){
ledPin = userLedPin;
pinMode(ledPin, OUTPUT);
}
Led myLed;
Led myLed2(7);
void setup() {
Serial.begin(115200);
}
void loop() {
myLed.on();
myLed2.on();
delay(1000);
myLed.off();
myLed2.off();
delay(1000);
}
封装是面向对象编程中的一项重要原则,它对于代码的可维护性和可复用性有着重要影响。以下是关于封装的好处,结合你提供的代码进行分析:
-
隐藏内部实现细节:通过将数据和相关的方法封装在类中,可以隐藏实现细节,只向外部提供必要的接口。在你的代码中,LED类的成员变量和成员函数都是私有的,外部无法直接访问和修改,只能通过公有方法进行操作,这样可以确保对象的状态和行为的一致性,避免了外部对内部实现的依赖。
-
提供良好的抽象层次:通过封装,可以将复杂的内部逻辑抽象成简单易用的公共接口。在你的代码中,通过定义公有方法
on()
、off()
、getLedPin()
和setLedPin()
,使得外部用户可以通过简单和直观的命令来控制LED的状态和访问私有成员变量。这种抽象层次使得代码易于理解和使用。 -
提高代码的可维护性:封装可以将代码和数据进行组织和结构化,使得代码更易于维护。通过隐藏实现细节,当需要修改内部逻辑时,只需关注类的内部,而不会影响外部代码。这种解耦的设计使得代码的修改更加安全和可控。
-
增加代码的可复用性:封装可以将功能性的代码进行封装,形成可重用的模块。在你的代码中,通过定义一个LED类,可以在不同的项目中重复使用。其他项目只需要创建LED对象并调用相应的方法即可实现LED的控制,而不需要重新编写相同的代码。
-
提供更好的安全性:通过封装,可以限制对数据的直接访问,从而提高数据的安全性。只有通过类提供的公共接口来访问和修改数据,确保了数据的有效性和一致性。
综上所述,封装的好处包括隐藏实现细节、提供良好的抽象层次、提高代码的可维护性、增加代码的可复用性和提供更好的安全性。这些优点使得代码更加可靠、灵活和易于扩展。通过良好的封装,可以提高代码的质量并提升开发效率。
阶段四:.h文件和.cpp文件
– 头文件(.h文件)和源文件(.cpp文件)建立及用途
/*
* main.cpp
*/
#include "Led.h"
void setup() {
Serial.begin(9600);
Led myLed; //建立Led类对象myLed
myLed.setLedPin(3);
int myLedPin = myLed.getLedPin();
Serial.print("int myLedPin = ");
Serial.println(myLedPin);
Led myLed2(7); //建立Led类对象myLed2
int myLed2Pin = myLed2.getLedPin();
Serial.print("int myLed2Pin = ");
Serial.println(myLed2Pin);
Serial.println("Hello, this is from Setup()");
for(int i = 0; i < 3; i++){
myLed.on();
myLed2.on();
delay(1000);
myLed.off();
myLed2.off();
delay(1000);
}
}
void loop() {
}
/*
* Led.h
*/
#ifndef _LED_H_
#define _LED_H_
#include <Arduino.h>
class Led {
public:
Led();
Led(int userLedPin);
~Led();
void on();
void off();
int getLedPin();
void setLedPin(int userLedPin);
private:
int ledPin = 2 ;
};
#endif
/*
* Led.cpp
*/
#include "Led.h"
Led::Led(){
Serial.println("Led Object Created.");
pinMode(2, OUTPUT);
}
Led::Led(int userLedPin) {
Serial.println("Led Object Created.");
ledPin = userLedPin;
pinMode(ledPin, OUTPUT);
}
Led::~Led(){
Serial.println("Led Object Deleted.");
}
void Led::on(){
digitalWrite(ledPin, HIGH);
}
void Led::off(){
digitalWrite(ledPin, LOW);
}
int Led::getLedPin(){
return ledPin;
}
void Led::setLedPin(int userLedPin){
ledPin = userLedPin;
pinMode(ledPin, OUTPUT);
}
头文件(.h文件)和源文件(.cpp文件)是C++编程中常用的两种文件类型,它们具有不同的作用。
头文件(.h文件):
-
头文件通常包含在C++程序的开头部分,并由
#include
预处理指令引入到源文件中。 -
头文件主要用于声明和定义类、函数、常量、宏等的接口和声明。
-
头文件通常包含类的定义、成员函数的原型、常量的声明、类型的定义等内容,但不包含实际的实现代码。
-
头文件的作用是提供给其他源文件引用,使得其他源文件可以访问并使用头文件中定义的接口和声明,而无需了解具体的实现细节。
-
头文件的命名通常与对应的源文件的名称相对应,但使用 .h 扩展名。
源文件(.cpp文件):
-
源文件包含了可执行代码的实际实现。
-
源文件包含类的成员函数的具体实现、全局函数的实现、以及其他代码逻辑的具体实现。
-
源文件中的代码将实现头文件中声明的接口,并定义函数、变量、类的成员函数等。
-
源文件的命名通常与对应的头文件的名称相对应,但使用 .cpp 或 .cxx 等源代码扩展名。
头文件和源文件的分离有助于代码的组织和管理,使代码更清晰、可维护性更高。头文件的使用可以提供接口声明,方便多个源文件的共享和重用。源文件则包含了具体的实现代码,通过与头文件的配合,使得程序结构更清晰,并且可以实现模块化和分层设计。
需要注意的是,头文件和源文件之间的一致性是非常重要的,头文件中的声明和源文件中的实现必须相符,以确保代码的正确性和一致性。
阶段五:类的继承
– 子类继承
– public继承类型
/*
* main.c
*/
#include "Led.h"
PwmLed myPwmLed;
void setup() {
Serial.begin(9600);
myPwmLed.setLedPin(3);
int myPwmLedPin = myPwmLed.getLedPin();
Serial.print("int myPwmLedPin = ");
Serial.println(myPwmLedPin);
}
void loop() {
for(int i = 0; i < 255; i++){
myPwmLed.on(i);
Serial.print("myPwmLed.getPwmVal() = ");
Serial.println(myPwmLed.getPwmVal());
delay(10);
}
}
/*
* Led.h
*/
#ifndef _LED_H_
#define _LED_H_
#include <Arduino.h>
class Led {
public:
Led();
Led(int userLedPin);
~Led();
void on();
void off();
int getLedPin();
void setLedPin(int userLedPin);
private:
int ledPin = 2 ;
};
/*
* 类Pwmled继承自类Led
* 因为public Led(),所以类Led中的所有公有成员在类Pwmled中是公有的
* 如果是private Led(),则类Led中的所有公有成员在类Pwmled中是私有的
*/
class PwmLed : public Led{
public:
void on(int userPwmVal);
int getPwmVal();
private:
int pwmVal = 0;
};
#endif
/*
* Led.cpp
*/
#include "Led.h"
Led::Led(){
Serial.println("Led Object Created.");
pinMode(2, OUTPUT);
}
Led::Led(int userLedPin) {
Serial.println("Led Object Created.");
ledPin = userLedPin;
pinMode(ledPin, OUTPUT);
}
Led::~Led(){
Serial.println("Led Object Deleted.");
}
void Led::on(){
digitalWrite(ledPin, HIGH);
}
void Led::off(){
digitalWrite(ledPin, LOW);
}
int Led::getLedPin(){
return ledPin;
}
void Led::setLedPin(int userLedPin){
ledPin = userLedPin;
pinMode(ledPin, OUTPUT);
}
void PwmLed::on(int userPwmVal){
pwmVal = userPwmVal;
analogWrite(getLedPin(), pwmVal);
}
int PwmLed::getPwmVal(){
return pwmVal;
}
阶段六:建立Arduino库
– 自建Arduino库的方法
– 使用 keywords.txt
文件让程序关键字改变颜色
-
库位置:
Arduino项目目录下的librarys文件夹
-
自建库基本结构:
--libarys
--Led
--Led.cpp
--Led.h
--keywords.txt
--examples
--example1
--example1.ino
--example2.ino
--example2.ino
......
--其他库的文件夹......
-
keywords.txt文件解析
#######################################
# Arduino LED库 关键字
# 太极创客 / www.taichi-maker.com
#######################################
#######################################
# 数据类型/Datatypes (KEYWORD1)
#######################################
Led KEYWORD1
PwmLed KEYWORD1
#######################################
# 函数/Functions (KEYWORD2)
#######################################
on KEYWORD2
off KEYWORD2
getLedPin KEYWORD2
setLedPin KEYWORD2
#######################################
# 其它关键字测试演示
#######################################
testKEYWORD1 KEYWORD1
testKEYWORD2 KEYWORD2
testKEYWORD3 KEYWORD3
testLITERAL1 LITERAL1
testLITERAL2 LITERAL2
testRESERVED_WORD RESERVED_WORD
testRESERVED_WORD_2 RESERVED_WORD_2
testDATA_TYPE DATA_TYPE
testPREPROCESSOR PREPROCESSOR
在Arduino开发环境中,keywords.txt
文件是一个用于代码提示和高亮显示关键字的配置文件。它主要用于扩展Arduino IDE(集成开发环境)的关键字识别功能,以提供更好的代码编写体验。
具体而言,keywords.txt
文件的作用如下:
-
代码提示:
keywords.txt
文件定义了一组关键字,这些关键字是Arduino IDE用来进行代码提示(自动补全)的依据。当你在编辑Arduino代码时,开始键入某个关键字时,IDE会自动显示与该关键字相关的选项列表,帮助你快速选择并补全代码。 -
关键字高亮显示:
keywords.txt
文件还用于指示关键字的高亮显示。在Arduino IDE中,关键字会以特殊的颜色或样式进行突出显示,以与其他代码区分开来,提高代码的可读性和可理解性。 -
用户自定义关键字:除了Arduino核心关键字外,
keywords.txt
文件还允许用户定义自己的关键字。用户可以将特定标识符或符号添加到keywords.txt
文件中,以使它们被IDE识别为关键字,并按照定义的样式进行高亮显示和代码提示。
总而言之,keywords.txt
文件是Arduino IDE的一个重要配置文件,用于定义关键字,以实现代码提示和高亮显示的功能。它提供了更好的编码体验和代码可读性,简化了Arduino项目的开发过程。