重要的内容写在前面:
- 该系列是以up主太极创客的零基础入门学用Arduino教程为基础制作的学习笔记。
- 个人把这个教程学完之后,整体感觉是很好的,如果有条件的可以先学习一些相关课程,学起来会更加轻松,相关课程有数字电路(强烈推荐先学数电,不然可能会有一些地方理解起来很困难)、模拟电路等,然后就是C++(注意C++是必学的)。
- 文章中的代码都是跟着老师边学边敲的,不过比起老师的版本我还把注释写得详细了些,并且个人认为重要的地方都有详细的分析。
- 一些函数的介绍有参考太极创客官网给出的中文翻译,为了便于现查现用,把个人认为重要的部分粘贴了过来并做了一些修改。
- 如有错漏欢迎指正。
视频链接:零基础入门学用Arduino-专项教程2(面向对象基础)1 初始面向对象_哔哩哔哩_bilibili
太极创客官网:太极创客 – Arduino, ESP8266物联网的应用、开发和学习资料
一、类和对象基础认识
1、面向对象概述
(1)C++面向对象的三大特性为:封装、继承、多态。
(2)C++认为万事万物都皆为对象,对象上有其属性和行为。例如:
①舵机可以作为对象,属性有控制引脚在Arduino上的编号,行为有转动输出轴至某一角度等。
②步进电机也可以作为对象,属性步进模式、输出轴转动一周所需步数、控制引脚在Arduino上的编号等,行为有转动、静止等。
2、封装
具有相同性质的对象,我们可以抽象称为类,例如舵机属于舵机类、步进电机属于步进电机类,设计者需要将类的属性和行为设计出来,使用者根据类创建对象,然后直接使用设计者封装好的属性和行为即可。
3、继承
(1)类的继承是新的类从已有类那里得到已有的特性,从已有的类产生新类的过程就是类的派生。在继承过程中,原有的类或已经存在的用来派生新类的类称为基类或父类,而由已经存在的类派生出的新类则称为派生类或子类。
(2)从派生类的角度,根据它所拥有的基类数目不同,可以分为单继承和多继承,一个类只有一个直接基类时称为单继承,一个类同时有多个直接基类时则称为多继承(在实际开发中不建议用多继承)。
(3)基类与派生类之间的关系如下:
①基类是对派生类的抽象,派生类是对基类的具体化。基类抽取了它的派生类的公共特征,而派生类通过增加信息将抽象的基类变为某种有用的类型,派生类是基类定义的延续。
②派生类是基类的组合。多继承可以看作是多个单继承的简单组合。
③公有派生类的对象可以作为基类的对象处理,这一点与类聚集(类的数据成员中有另一个类)是不同的,在类聚集中,一个类的对象只能拥有作为其成员的其它类的对象,但不能作为其它类对象而使用。
二、类和对象的基本语法
1、类的定义
(1)类的定义可以分为说明部分和实现部分两部分,其中说明部分说明类中包含的数据成员和成员函数,实现部分是对成员函数的定义。
(2)类定义的一般格式如下:
//类的说明部分
class <类名>
{
public:
<成员函数或数据成员的说明> //公有成员,外部接口
protected:
<成员函数或数据成员的说明> //保护成员
private:
<成员函数或数据成员的说明> //私有成员
}; //花括号括住的属于类内范围
//类的实现部分
<各个成员函数的实现>
①class是声明类的关键字,类名是要声明的类的名字,必须符合标识符定义规则。
②花括号表示类的声明范围,其后的分号表示类声明结束。
③类的成员包括数据成员和成员函数,分别描述类所表达的属性和行为。
④关键字public、private和protected称为访问权限修饰符,它们限制了类成员的访问控制范围。
(3)类的数据成员:
①类中的数据成员描述类所表达的问题的属性。
②数据成员在类体中进行定义,其定义方式与一般变量相同,在定义类的数据成员时需要注意以下几个问题:
[1]对数据成员的访问会受到访问权限修饰符的控制。
[2]类中的数据成员可以是任意类型,包括整型、浮点型、字符型、数组、指针和引用等,也可以是其它类的对象(自身类不行,但自身类的指针可以)。
[3]在类体中不允许对所定义的数据成员进行初始化。
(4)类的成员函数:
①类的成员函数描述类所表达的问题的行为。
②各个成员函数的实现既可以在类体内定义(在类体内定义的成员函数都是内联函数,类体内定义则不需要对函数进行声明),也可以在类体外定义(类体外定义的话,类体内需要对函数进行声明)。
[1]如果一个成员函数在类体内进行了定义,它将不出现在类的实现部分。如果所有的成员函数都在类体内进行了定义,则可以省略类的实现部分。
[2]如果要将定义在类体外的成员函数也作为内联函数处理,就必须在成员函数的定义前加上关键字inline。
//类的说明部分
class <类名>
{
public:
<返回类型> <成员函数名>(<参数表>) //类内实现
{
<函数体>
}
<返回类型> <成员函数名>(<参数表>); //类内声明,类外实现
}; //花括号括住的属于类内范围
//类的实现部分
<返回类型> <类名>::<成员函数名>(<参数表>){
<函数体>
}
③成员函数除了可以定义为内联函数以外,也可以进行重载,还可以对其形参设置默认值。
2、对象的定义
(1)对象是类的实例,一个对象必须属于一个已知的类,因此在定义对象之前,必须先定义该对象所属的类。
(2)对象的定义格式如下:
<类名> <对象名>(<参数表>);
①类名是待定义的对象所属的类的名字。
②可以有一个或多个对象名,多个对象名之间用逗号分隔。
③参数表是初始化对象时需要的,建立对象时可以根据给定的参数调用相应的构造函数对对象进行初始化。无参数时表示调用类的默认构造函数。
(3)一个对象的成员就是该对象的类所定义的成员,包括数据成员和成员函数。定义了对象后,可以用“.”运算符和“->”运算符访问对象的成员,其中“.”运算符适用于一般对象和引用对象,而“->”运算符适用于指针对象(即指向对象的指针)。
<对象名>.<数据成员名> //通过对象名访问对象的数据成员
<指针名>-><数据成员名> //通过指向对象的指针访问对象的数据成员
<对象名>.<成员函数名>(<参数表>) //通过对象名访问对象的成员函数
<指针名>-><成员函数名>(<参数表>) //通过指向对象的指针访问对象的成员函数
3、构造函数和析构函数
(1)定义一个类对象时,编译程序要为对象分配存储空间,进行必要的初始化,在C++中,这项工作是由构造函数来完成的,构造函数的作用是在对象被创建时利用特定的值构造对象,将对象初始化为一种特定的状态,使该对象具有区别于其它对象的特征。与构造函数对应的是析构函数,当撤销类对象时,析构函数负责回收存储空间,并做一些善后工作。
(2)构造函数也是类的成员函数,但它是一种特殊的成员函数,它除了具有一般成员函数的特性之外,还具有一些特殊的性质:
①构造函数的名字必须与类名相同。
②构造函数不指定返回类型,它隐含有返回值,由系统内部使用。
③构造函数可以有一个或多个参数(还可以设置默认参数),因此构造函数可以重载。
④在创建对象时,系统会自动调用构造函数。
(3)析构函数也是类中的一种特殊成员函数,它具有以下一些特性:
①析构函数名是在类名前加求反符号“~”。
②析构函数不指定返回类型,它不能有返回值。
③析构函数没有参数,因此析构函数不能重载,一个类中只能定义一个析构函数。
④在撤销对象时,系统会自动调用析构函数。
(4)构造函数和析构函数的定义:
①构造函数的定义(这里只展示类内定义):
class <类名>
{
public:
<类名>(<参数表>) : <数据成员名>(<表达式>), <数据成员名>(<表达式>), …
{
<构造函数体>
}
<成员函数或数据成员的说明>
};
[1]冒号后面是一个构造函数的初始化列表,用于初始化类中的各个数据成员。
[2]初始化列表位于构造函数的形参表之后,函数体代码之前,由一个冒号和逗号分隔的若干项构成。
[3]每一个构造函数的初始化列表项都由数据成员标识符和其后的括号表达式构成。
[4]在调用构造函数对类对象初始化时,先执行初始化列表对各个成员进行初始化,再执行构造函数体。
[5]初始化列表中各个初始化项的执行顺序取决于类成员在类中声明的顺序,而与初始化列表给出的初始化项的顺序无关。
[6]对于大多数数据成员而言,既可以使用初始化列表的方式获得显式初值,也可以在获得默认初值后再在构造函数体中使用赋值语句将表达式的值赋值给数据成员。
[7]当一个类的成员是另外一个类的对象时,该对象就称为成员对象,当类中出现了成员对象时,该类的构造函数要包含成员对象的初始化。
②析构函数的定义:
class <类名>
{
public:
~<类名>( )
{
<析构函数体>
}
<成员函数或数据成员的说明>
};
(5)默认构造函数和默认析构函数:
①构造和析构都是必须有的实现,否则编译器会自己提供空实现。
②构造函数的空实现:
class <类名>
{
public:
<类名>( ) {
}
};
③析构函数的空实现:
class <类名>
{
public:
~<类名>( ) {
}
};
4、拷贝构造函数
(1)类中有一种特殊的构造函数叫做拷贝构造函数(也叫复制构造函数),它用一个已知的对象初始化一个正在创建的同类对象。
(2)拷贝构造函数的一般格式如下(这里仅给出类内定义的构造函数):
class <类名>
{
public:
<类名>(const <类名>& <引用对象名>){
<拷贝构造函数体>
}
<成员函数或数据成员的说明>
};
(3)拷贝构造函数具有以下特点:
①拷贝构造函数也是一种构造函数,因此函数名与类名相同,并且不能指定函数返回类型。
②只有一个参数,是对同类的某个对象的引用。
③每一个类中都必须有一个拷贝构造函数,如果类中没有定义拷贝构造函数,编译器会自动生成一个具有上述形式的公有拷贝构造函数。
(4)拷贝构造函数在以下三种情况下会被调用:
①用类的一个已知的对象去初始化该类的另一个正在创建的对象。
②采用值传递调用方式时,对象作为函数实参传递给函数形参。
③对象作为函数返回值。
5、构造函数的分类及调用
(1)根据参数列表是否为空可将构造函数分为有参构造和无参构造,根据参数类型可将构造函数分为普通构造和拷贝构造。
(2)调用构造函数的三种方式:括号法、显示法、隐式转换法。
class Unknown_hardware
{
public:
Unknown_hardware(){ //构造函数
//Unknown_hardware无参构造函数的调用
}
Unknown_hardware(int a){ //构造函数
pin = a;
//Unknown_hardware有参构造函数的调用
}
Unknown_hardware(const Unknown_hardware &p){ //拷贝构造函数
pin = p.pin; //将传入的对象身上的所有属性拷贝到这里
//Unknown_hardware拷贝构造函数的调用
}
~Unknown_hardware(){ //析构函数
//Unknown_hardware析构函数的调用
}
int pin;
};
void setup()
{
//无内容
}
void loop()
{
//括号法(推荐)
Unknown_hardware p1; //默认构造函数调用(千万不要加小括号,否则编译器会认为是函数声明)
Unknown_hardware p2(10); //有参构造函数调用
Unknown_hardware p3(p2); //拷贝构造函数调用
//显示法
Unknown_hardware p4; //默认构造函数调用
Unknown_hardware p5 = Unknown_hardware(10); //有参构造函数调用
Unknown_hardware p6 = Unknown_hardware(p5); //拷贝构造函数调用(不要利用拷贝函数初始化匿名对象,即Unknown_hardware(p5);)
Unknown_hardware(10); //匿名对象,创建了一个没名字的对象,当前行执行结束后系统会立即回收它
//隐式转换法
Unknown_hardware p7 = 10; //相当于Unknown_hardware p7 = Unknown_hardware(10);
Unknown_hardware p8 = p7; //相当于Unknown_hardware p2 = Unknown_hardware(p7);
}
(3)默认情况下,C++编译器至少会给一个类添加3个函数,分别为默认构造函数(无参,函数体为空)、默认析构函数(无参,函数体为空)、默认拷贝构造函数。
①如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造。
②如果用户定义有拷贝构造函数,C++不会再提供其它构造函数。
(4)类对象作为类成员,定义类对象时,先调用类成员的构造函数,再调用类对象的构造函数;销毁类对象时,先调用类对象的析构函数,再调用类成员的析构函数。(类中有多个成员对象时,成员对象构造函数的执行顺序仅与成员对象在类中声明的顺序有关,而与成员初始化列表中给出的成员对象的顺序无关)
三、使用类和对象的思想点灯
1、点灯电路连接
2、示例程序1——蓝色LED闪烁3次
class Led { //LED灯类
public:
Led(); //构造函数声明
~Led(); //析构函数声明
void on(); //点灯操作函数声明
void off(); //闭灯操作函数声明
};
Led::Led(){
Serial.println("Led Object Created.");
pinMode(2, OUTPUT); //初始化2号引脚为输出模式
}
Led::~Led(){
Serial.println("Led Object Deleted.");
}
void Led::on(){
digitalWrite(2, HIGH); //蓝色LED被点亮
}
void Led::off(){
digitalWrite(2, LOW); //蓝色LED被熄灭
}
void setup()
{
Serial.begin(9600);
Led myLed; //根据led类创建LED对象
Serial.println("Hello, this is from Setup()");
for(int i = 0; i < 3; i++)
{
myLed.on(); //myLed做点灯行为
delay(1000);
myLed.off(); //myLed做闭灯行为
delay(1000);
}
}
void loop()
{
//无内容
}
3、示例程序2——两个LED闪烁3次
class Led { //LED灯类
public:
Led(); //无参构造函数声明
Led(int userLedPin); //有参构造函数声明
~Led(); //析构函数声明
void on();
void off();
int ledPin = 2; //属性——LED连接引脚2
};
Led::Led(){
Serial.println("Led Object Created.");
pinMode(2, OUTPUT); //初始化引脚2为输出模式
}
Led::Led(int userLedPin) {
Serial.println("Led Object Created.");
ledPin = userLedPin; //更改LED连接的引脚号为userLedPin
pinMode(ledPin, OUTPUT); //初始化userLedPin引脚为输出模式
}
Led::~Led(){
Serial.println("Led Object Deleted.");
}
void Led::on(){
digitalWrite(ledPin, HIGH); //点亮引脚ledPin连接的LED
}
void Led::off(){
digitalWrite(ledPin, LOW); //熄灭引脚ledPin连接的LED
}
void setup()
{
Serial.begin(9600);
Led myLed; //蓝色LED连接引脚2
Led myLed2(7); //绿色LED连接引脚7
Serial.println("Hello, this is from Setup()");
for(int i = 0; i < 3; i++){
myLed.on(); //蓝色LED点灯
myLed2.on(); //绿色LED点灯
delay(1000);
myLed.off(); //蓝色LED闭灯
myLed2.off(); //绿色LED闭灯
delay(1000);
}
}
四、程序的分文件编写
1、分文件编写的目的与步骤
(1)分文件编写的目的:在实际开发中,代码量往往不止几十行,为了便于管理和检查错
误,通常需要将不同功能的代码写在不同的文件中。
(2)分文件编写的步骤:
①在项目文件夹中创建.h格式以及.cpp格式的文件。
②将函数的实现以及类的实现写在其它源文件(也就是.cpp文件)中。
③将函数的声明写在与其源文件同名的头文件(也就是.h文件)中,使用“#include”预处理指令将函数实现需要包含的头文件添加在该头文件中,另外不管头文件的情况如何,都要包含Arduino.h文件。
④在函数实现所在的源文件中使用“#include”预处理指令将函数声明所在的头文件添加进
来。
⑤需要调用函数的文件中使用“#include”预处理指令将函数声明所在的头文件添加进来即
可。
2、示例项目(由上例改编)
(1)项目文件夹结构:(分文件不一定要与项目文件夹的名称相同)
(2)Led.ino文件:
#include "Led.h"
void setup()
{
Serial.begin(9600);
Led myLed; //蓝色LED连接引脚2
Led myLed2(7); //绿色LED连接引脚7
Serial.println("Hello, this is from Setup()");
for(int i = 0; i < 3; i++){
myLed.on(); //蓝色LED点灯
myLed2.on(); //绿色LED点灯
delay(1000);
myLed.off(); //蓝色LED闭灯
myLed2.off(); //绿色LED闭灯
delay(1000);
}
}
void loop()
{
//无内容
}
(3)Led.h文件:
#ifndef _LED_H_
#define _LED_H_
#include <Arduino.h>
class Led { //LED灯类
public:
Led(); //无参构造函数声明
Led(int userLedPin); //有参构造函数声明
~Led(); //析构函数声明
void on();
void off();
int ledPin = 2; //属性——LED连接引脚2
};
#endif
(4)Led.c(Led.cpp)文件:
#include "Led.h"
Led::Led(){
Serial.println("Led Object Created.");
pinMode(2, OUTPUT); //初始化引脚2为输出模式
}
Led::Led(int userLedPin) {
Serial.println("Led Object Created.");
ledPin = userLedPin; //更改LED连接的引脚号为userLedPin
pinMode(ledPin, OUTPUT); //初始化userLedPin引脚为输出模式
}
Led::~Led(){
Serial.println("Led Object Deleted.");
}
void Led::on(){
digitalWrite(ledPin, HIGH); //点亮引脚ledPin连接的LED
}
void Led::off(){
digitalWrite(ledPin, LOW); //熄灭引脚ledPin连接的LED
}
五、自建Arduino库
继续以上例做延伸,在.ino文件上一级的Arduino文件夹中有一个libraries文件夹,这个文件夹用于存放第三方库,打开这个文件夹,在其中建立新文件夹Led,然后把上例中的Led.h文件和Led.c(Led.cpp)移动至该文件夹中,即完成第三方库的创建,程序可以直接使用之。