# Object-Oriented Programming 物件導向程式設計
物件導向是一種程式設計方法,主要是利用類別的變數型態把相關的變數和函式包在一起,相對於程序式程式設計(procedural programming)有低偶合度、使主程式較簡潔、好擴充等功能。
在ROS中大部分的package都是使用物件導向的架構寫成的,而之後創建package時,也可以使用物件將程式模組化及提高可維護性,所以我們需要學習如何編寫物件,及物件的相關概念。
* [物件導向與程序式導向更詳細的解釋](https://zxuanhong.medium.com/%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91-vs-%E7%A8%8B%E5%BA%8F%E5%BC%8F%E5%B0%8E%E5%90%91-%E5%B9%B3%E5%BA%B8%E8%88%87%E9%AB%98%E7%B4%9A%E5%B7%A5%E7%A8%8B%E5%B8%AB%E9%96%8B%E7%99%BC%E9%80%9F%E5%BA%A6%E5%B7%AE%E7%95%B0%E7%9A%84%E7%A7%98%E5%AF%86-19e6357b54e6)
## 類別宣告
下面是以差動輪底盤程式為例寫成的物件:
```cpp=
#include <iostream>
#include <math.h>
class Chassis{
public:
Chassis(float x_0, float y_0, float theta_0, float v_wheels_0[2])
:x(x_0), y(y_0), theta(theta_0){
v_wheels[0]=v_wheels_0[0];
v_wheels[1]=v_wheels_0[1];
}
void forwardKinematics(){
v_x=(v_wheels[0]+v_wheels[1])*cos(theta)/2;
v_y=(v_wheels[0]+v_wheels[1])*sin(theta)/2;
omega=(v_wheels[0]+v_wheels[1])/(radius*2);
}
void poseUpdate(){
time+=dt;
x+=v_x*dt;
y+=v_y*dt;
theta+=omega*dt;
}
void output(){
std::cout<<"at "<<time<<"s, ";
std::cout<<"x: "<<x<<", y: "<<y<<", theta: "<<theta<<std::endl;
}
private:
float x;
float y;
float theta;
float v_x=0.0;
float v_y=0.0;
float omega=0.0;
float v_wheels[2]={0.0, 0.0};
float radius=100;
float dt=0.01;
float time=0.0;
};
int main(){
float time=0.0;
float wheels[2]={3.0, 3.0};
Chassis diff(10, 10, 0, wheels);
for(int i=0;i<5;i++){
diff.forwardKinematics();
diff.poseUpdate();
diff.output();
}
return 0;
}
```
output:
>at 0.01s, x: 10.03, y: 10, theta: 0.0003
at 0.02s, x: 10.06, y: 10, theta: 0.0006
at 0.03s, x: 10.09, y: 10, theta: 0.0009
at 0.04s, x: 10.12, y: 10.0001, theta: 0.0012
at 0.05s, x: 10.15, y: 10.0001, theta: 0.0015
直接從宣告的架構去看,可以簡單把**類別(class)** 想成==可以宣告函式的結構(structure)==,是一個自定義的變數型態,而由這個型態定義的變數稱為**物件(object)**,或稱為該類別的一個實例(instance)。
<font size=4>**data member& member function**</font>
在物件內的變數,稱為**資料成員(data member)**,也稱為屬性(attribute),像是程式碼的30~39行宣告了這個類別會有的屬性;物件內的函數稱為**函數成員(member function)**,也稱為方法(method),在這個範例中即為```forwardKinematics```、 ```poseUpdate``` 和```output```。
<font size=4>**access specifier**</font>
第5行和第29行出現的```public```和```private```稱為access specifier,可以設定成員的存取權限。像是public這個區域下的成員可以被外部程式呼叫或使用,而private內的成員則只有本類別的函數成員可以使用。一般會將資料成員設為私有,防止外部程式更改資料成員的數值,這樣在debug時也會較為容易。
:::info
* *當未宣告使用access specifier時,編譯器會預設使用private*
* 還有另一種access specifier,為protected,可以使繼承自該類別的class使用
:::
<font size=4>**constructor&destructor**</font>
在第6行有一個跟類別名稱相同的函數成員,稱為**建構子(constructor)**,當一個物件被創立時會第一個呼叫這個函式,所以通常會在這個函式中進行初始化的工作。至於解構子(destructor)則相反,會在物件被刪除時呼叫,所以通常會拿來釋放記憶體。以這個class做舉例,可以寫一個這樣的destructor:
```cpp=
~Chassis(){
delete x;//如果x的記憶體是動態配置
delete[] v_wheels;
}
```
C++ 會有預設的 constructor 與 destructor,所以在某些情況可以不用寫。像是在上面的例子中,因為變數都是靜態取得記憶體空間的,所以不必另外撰寫 destructor。另外,因為這是個特殊的函式,所以沒有回傳值,也不能設定回傳值為```void```。
<font size=4>**分離函式定義**</font>
一般在實作時會將宣告class的部分放在一個```.h```檔,而把函式定義的部分放在另一個同名的```.cpp```檔中,並使用**作用域解析運算符(scope resolution operator**```::```來指定要定義的是該class裡面的函數。這樣做的目的可以使宣告類別的地方叫簡潔,不會被一堆程式碼打擾。
基於上面範例,將物件分成```chassis.h```和```chassis.cpp```:
```cpp=
//chassis.h
class Chassis{
public:
Chassis(float x_0, float y_0, float theta_0, float v_wheels_0[2]){}
void forwardKinematics();
void poseUpdate();
void output();
private:
float x;
float y;
float theta;
float v_x=0.0;
float v_y=0.0;
float omega=0.0;
float v_wheels[2]={0.0, 0.0};
float radius=100;
float dt=0.01;
float time=0.0;
};
```
```cpp=
//chassis.cpp
#include "chassis.h"
#include <math.h>
Chassis::Chassis(float x_0, float y_0, float theta_0, float v_wheels_0[2])
:x(x_0), y(y_0), theta(theta_0){
v_wheels[0]=v_wheels_0[0];
v_wheels[1]=v_wheels_0[1];
}
void Chassis::forwardKinematics(){
v_x=(v_wheels[0]+v_wheels[1])*cos(theta)/2;
v_y=(v_wheels[0]+v_wheels[1])*sin(theta)/2;
omega=(v_wheels[0]+v_wheels[1])/(radius*2);
}
void Chassis::poseUpdate(){
time+=dt;
x+=v_x*dt;
y+=v_y*dt;
theta+=omega*dt;
}
void Chassis::output(){
std::cout<<"at "<<time<<"s, ";
std::cout<<"x: "<<x<<", y: "<<y<<", theta: "<<theta<<std::endl;
}
```
:::spoiler this pointer
this 是一個指向物件的指標,用途主要有以下兩個:
- 如果member function中的輸入參數或區域變數的名稱與物件內的data member相同,可以使用this指向data member避免混淆。例如把this的寫法用在上面程式建構子的地方,會變成:
```cpp=
Chassis(float x, float y, float theta, float v_wheels_0[2]){
this->x=x;
this->y=y;
this->theta=theta;
v_wheels[0]=v_wheels_0[0];
v_wheels[1]=v_wheels_0[1];
}
```
- 如果外部程式需要呼叫該類別,可以使用this作為參數 [(reference)](https://stackoverflow.com/questions/23231603/how-do-you-use-the-this-pointer-in-c-in-objects):
```cpp=
void DoSomeThingElse(A *object) {};
class A
{
void DoSomething()
{
DoSomethingElse(this);
}
}
```
:::
## 特性介紹
物件導向有三大特性: 封裝(Encapsulation)、繼承(Inheritance)、多形(Polymorphism),以下一一介紹:
### 封裝
封裝是指把程式碼實現某個功能的部分隱藏起來,使用者在使用時只要知道有這個函式可以實現這個功能,但是不用在意怎麼實現。例如在上面的程式中,主程式只會知道poseUpdate會把位姿更新,但不知道是怎麼更新的;還有知道output會印出某個時刻的位姿資訊,但也不知道如何達成。
還有一個例子就是在新生教學時用到的```servo.h```,我們只知道他有```attach()```跟```write()```函式可以讓我們驅動伺服馬達,但是不知道他具體是怎麼做到的,這就是封裝。
### 繼承、多形
如果class中的功能需要擴充,可以用繼承的方式生成一個新的類別;或是有多個相似的概念時,可以先有一個主要的類別架構,之後再寫出其他繼承這個父類別的子類別,已達成多形的效果。
以上面的程式碼為例,假設我們今天要將功能擴充到可以包含差動輪和四輪配置的萬象輪底盤,可以這樣寫:
```cpp=
class Chassis{
public:
Chassis(float x_0, float y_0, float theta_0)
:x(x_0), y(y_0), theta(theta_0){}
virtual void forwardKinematics()=0;
void poseUpdate(){
time+=dt;
x+=v_x*dt;
y+=v_y*dt;
theta+=omega*dt;
}
void output(){
std::cout<<"at "<<time<<"s, ";
std::cout<<"x: "<<x<<", y: "<<y<<", theta: "<<theta<<std::endl;
}
protected:
float x;
float y;
float theta;
float v_x=0.0;
float v_y=0.0;
float omega=0.0;
float dt=0.01;
float time=0.0;
};
class Diff_wheel : public Chassis{
public:
Diff_wheel(float x_0, float y_0, float theta_0, float v_wheels_0[2])
:Chassis(x_0, y_0, theta_0){
v_wheels[0]=v_wheels_0[0];
v_wheels[1]=v_wheels_0[1];
}
void forwardKinematics() override{
v_x=(v_wheels[0]+v_wheels[1])*cos(theta)/2;
v_y=(v_wheels[0]+v_wheels[1])*sin(theta)/2;
omega=(v_wheels[0]+v_wheels[1])/(radius*2);
}
private:
float v_wheels[2]={0.0, 0.0};
float radius=100;
};
class Omni_wheel : public Chassis{
public:
Omni_wheel(float x_0, float y_0, float theta_0, float v_wheels_0[4])
:Chassis(x_0, y_0, theta_0){
for(int i=0;i<4;i++){
v_wheels[i]=v_wheels_0[i];
}
}
void forwardKinematics() override{
v_x=0.25*sqrt(2)*(v_wheels[0]-v_wheels[1]-v_wheels[2]+v_wheels[3]);
v_y=0.25*sqrt(2)*(v_wheels[0]+v_wheels[1]-v_wheels[2]-v_wheels[3]);
omega=(v_wheels[0]+v_wheels[1]+v_wheels[2]+v_wheels[3])/(radius*4.0);
}
private:
float v_wheels[4]={0};
float radius=200.50;
};
```
首先在30行和47行看到```: public Chassis```的寫法。表示```Diff_wheel```、```Omni_wheel```都是繼承於Chassis這個class。繼承又有分成 public, private, protect 三種繼承,最常用的是 public 繼承。我們會稱被繼承的```Chassis```是**基礎類別(base class)**,或稱父類別,經由繼承所產生的```Diff_wheel```和```Omni_wheel```則稱為**衍生類別(derived class)**,或是子類別。
:::spoiler 三種繼承方式
類別的繼承和成員的存取權限一樣,都可以有public、protected、private三個選項。使用哪一種繼承方式會影響外部程式使用成員的權限
|繼承方式|base class的public|base class的protected|base class的private|
|--|--|--|--|
|public|public|protected|private(derived class無法存取)|
|protected|protected|protected|private(derived class無法存取)|
|private|private(外部程式無法存取)|private(外部程式無法存取)|private(derived class無法存取)|
:::
在第3, 4 行看到了新的字,```virtual```。這是為了讓這個函式在被繼承的時候,derived class 可以重新定義這個函式。所以在 17, 18 行可以看到再次定義了這個函式,並且在後面加了```override``` 這個字表示他覆蓋的 base class 的函式定義。
至於```pure virtual function``` 跟```virtual function``` 的差別在哪?像是上面的例子 base class 完全不定義函式,並且在函式後面寫 ```=0```,這樣 derived class 無論如何都必須自己定義函式,這樣的函式就稱為 ```pure virtual function```。相反的,如果 base class 有定義函式,然後 derived class 不一定要自己定義函式,則是 ```virtaul function```。
像這樣有 pure virtual function 的 class,他無法被實體化,那我們就稱這個 class 是一個 **abstract class**.
## 在ROS環境執行分離函示定義的程式
通常會把```.cpp```檔案放在package底下的```src```資料夾,然後創建另一個```include```資料夾放置```.h```檔。
要執行主要的程式碼 *(有main function那個)* ,做法和前幾天介紹的方式相同,都是在CMakeList.txt增加可執行檔;標頭檔則是需要引入```include```資料夾,相關指令已經寫在CMakeList的Build區塊下面幾行的地方,直接解註解即可
```cmake=
include_directories(
include # 這行解註解
${catkin_INCLUDE_DIRS}
)
```
至於定義成員函式的cpp檔案,因為不包含main function,所以這個檔案應該被視為一個函式庫。引用方法如下:
```cmake=
add_library(chassis src/chassis.cpp)
```