【電子工作C++編】A-1.ArduinoでC++言語のクラスを作ろう

 

Arduinoで、C++が使えることがわかりました。

ところが、Arduinoはプロトタイプ宣言がいらない謎の方式だったので、

イマイチと思っていたのですが、

 

どうも、C++の場合はプロトタイプ宣言が必要である模様。

ということは、C++言語の勉強用デバイスとして使えそうです。

 

 

ということで、C++学習用のデバイスにしようかと思います。

 

 

それでは、

今回はクラスを作ってみましょう。

 

1.クラスを理解しよう

まず最初にクラスとは?

 

一番簡単なイメージとしては、

C言語でいう構造体です。

 

関数と変数がセットになった構造体と考えるのが、

一番イメージしやすいんじゃないかと思います。

 

 

 

関数が3つと、変数が2つを構造体を作っているイメージです。

 

使い方ですが、

構造体を使うのと一緒です。

このクラスをローカル変数として定義することで、

クラスを生成できます。

 

  /* スタック上にクラスを生成 */
  LedClass  lLedClass(IoPort);

 ※引数がある点はいったん無視してください。


 

また、構造体の変数を扱うような形で、関数を呼び出せば、

関数の機能を使えます。

 

ここでポイントとなるのが、

変数も構造体の中に配置されていることです。

 

 

もしC言語であれば、

 

通常関数で処理をする場合、

何かの処理状態を残す際には、

関数とセットで複数のグローバル変数を定義する必要があります。

 

つまり、↓こんなイメージ。

static int gPort;

static bool gStatus;

void SetOutput(bool aOutput)

 

もし、制御するポートが3つに増えたら、

これを同じ数だけ定義する必要があります。

また、変数は同名で定義できないので、複数定義しなければならず、

複数のLED制御する際には定義が面倒です。

関数にも引数を増やしたりうまく実装する必要があります。

 

static int gPort1;

static bool gStatus1;

static int gPort2;

static bool gStatus2;

static int gPort3;

static bool gStatus3;

void SetOutput(bool aOutput,int aPort, bool aStatus);

※といはいえ、実装方法は10人10色でやり方は色々です。

 

 

では、これをC++言語で実装する場合は?

 

C++では、クラス内に関数と変数がパッケージになっています。

 

  LedClass  LedClass1(StackIoPort);

  LedClass  LedClass2(StackIoPort);

  LedClass  LedClass3(StackIoPort);

 

これで関数を呼び出すだけ。

似たような処理を作る場合など、C++言語はすごく便利ですね!

 

※今日は理解できなくとも、こんなスマートになるんだ

 思っていただければと思います。

 

 

2.クラスと構造体の違い

 

クラスには、

クラス生成時に呼び出されるコンストラクタ

クラス削除時に呼び出されるデストラクタがあります。

 

構造体の場合には、

構造体を定義した後、各数値を各々初期値を設定する必要があります。

 

LedStr gStruct;  

gStruct.iPort  =  13;

gStruct.iStatus=  false;

 

 

クラスの場合には、

クラス生成時にコンストラクタが呼び出されますので、

コンストラクタ内で初期化してやれば、

使用するたびに初期化するような処理を作らなくて済みます。

 

  LedClass  LedClass1(StackIoPort);

 

 

特に便利なのが、設計変更で変数を追加した際です。

 

構造体の場合、構造体を使用している個所すべてで

変数を初期化する処理を入れる必要が出てきますね。

 

LedStr gStruct;  

gStruct.iPort  =  13;

gStruct.iStatus=  false;

gStruct.iAppend  =  100;

 

 

クラスの場合はコンストラクタで処理をしていますので、

従来から使用している側は特に処理を書く必要がなくなります。

 

  LedClass  LedClass1(StackIoPort);

 

また、デストラクタという機能も便利です。

出力のフラグを落とし忘れたとしても、

デストラクタに出力停止の処理を書いていれば、

IO出力の落とし忘れが防げます。

 

 

 

3.クラスを書いてみよう

 

LEDをIO制御するためのクラスを作ってみましょう。

 

 

変数は、

iPortは、出力制御をするためのポートを覚えておくための変数

iStatusは、出力状況を覚えておくためのStatusです。

(ただ、今回はStatusは使いません。)

 

関数は、

コンストラクタとデストラクタ

そして、出力を制御する関数です。

 

 

これをクラスにすると、

--------------------------------------

class LedClass
{
 public:
  /* costructor */
  LedClass(int aPort);
  /* destructor */
  ~LedClass();
  /* 出力設定 */
  void SetOutput(bool aOutput);
 private:
  int iPort;
  bool iStatus;
};

--------------------------------------

 

◆3.1 コンストラクタ

コンストラクタでは、

内部変数の初期化を行います。

また、このクラスを使うための事前処理、

今回でいうと出力ポートの設定をします。


LedClass::LedClass(int aPort)
:iPort(aPort),iStatus(false)         //  変数の初期化  iPort = aPort; iStatus = false; と同じ、
{
  /* コンストラクタで指定されたポートを出力設定 */
  pinMode(iPort, OUTPUT);
}

 

◆3.2 デストラクタ

デストラクタは、異常終了しても必ず出力を停止させるように、出力を落とす

LedClass::~LedClass()
{
  digitalWrite(iPort, LOW);
}

 

◆3.3 処理関数

 

引数に従って、コンストラクタで指定したポートを制御する

void LedClass::SetOutput(bool aOutput)
{
  digitalWrite(iPort, aOutput);
}

 

 

 

4.使ってみよう

 

実際に使ってみよう。

※コードは最後に書いてますので、試してください。

 

 

loop処理の最後、出力がONのまま終わっているはずが、

関数を抜けた後に出力がOFFになる動作が見えたでしょうか?

 

そのタイミングでデストラクタが動作しているのです。

 

 

 

便利でしょ!

SAIでした。

晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ

雑談

 

クラスを理解するのがに苦労した人に聞いてみると、

この部分の理解に苦労したとのことでした。

 

SAIは、コードを関数、変数ごと、

部品として丸っとパッケージになっているというイメージを持ったため、

クラスという概念に苦労することはありませんでした。

 

なので、この辺の説明は下手かもしれないです。

 

クラスの理解に苦しんだ方、

どの辺に苦しんだか教えていただけると、

このページの説明の改善になります。

 

もしよろしければ、苦労した点をコメントいただければと思います。

晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ


 

本日使ったコードは以下の通りです!

 

宇宙人しっぽ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人あたま

/*
 * @file    CPP_L_A_1_TestCLASS
 * @brief   Lesson A_1 Classを作ってみよう。
 * @author  SAI
 * @date    2021/03/15
 * @note    Classを作ってみよう
 */

/* *** Define 定義 *** */
const int HeapIoPort = 11;
const int LoopIoPort = 12;
const int StackIoPort = 13;

/*
 * @note    テストクラスのプロトタイプ
 */
class LedClass
{
 public:
  /* costructor */
  LedClass(int aPort);
  /* destructor */
  ~LedClass();
  /* 出力設定 */
  void SetOutput(bool aOutput);
 private:
  int iPort;
  bool iStatus;
};

/*
 * @fn    初期設定
 * @brief コンストラクタ
 * @param aPort LEDを制御するポート番号
 */
LedClass::LedClass(int aPort)
:iPort(aPort),iStatus(false)
{
  /* コンストラクタで指定されたポートを出力設定 */
  pinMode(iPort, OUTPUT);
}
/*
 * @fn    終了設定
 * @brief デストラクタ
 * @note  終了時には必ずリソースを終了状態にする。
 *        生成したヒープ、つないだセッションなど開放する
 */
LedClass::~LedClass()
{
  digitalWrite(iPort, LOW);
}
/*
 * @fn    出力設定
 * @brief 出力の設定を切り替える
 * @param aOutput ON/OFFを設定する true=点灯 false=消灯
 */
void LedClass::SetOutput(bool aOutput)
{
  digitalWrite(iPort, aOutput);
}

/*
 * @note  Arduinoで最初に呼ばれる関数
 */
void setup() 
{
//今回はなし
}

/*
 * @note  ぐるぐるループ
 */
void loop() 
{
  delay(5000);
  /* スタック上にクラスを生成 */
  LedClass  lStackLedClass(StackIoPort);
  delay(500);
  lStackLedClass.SetOutput(true);
  delay(500);
  lStackLedClass.SetOutput(false);
  delay(500);
  lStackLedClass.SetOutput(true);
  delay(1000);
  /* 関数終了時にスタックが解放されて、デストラクタが走る
    同時に、デストラクタでLEDを消灯する*/  
}

 

 

 

むかついたので、H8用のHEW2を引っ張り出して

環境構築してみた。

 

 

 

ふっふっふ

まずは、コンストラクタを記載しない場合

 

無事MAPゲット

-----------------------------------------------------------

SECTION=C
FILE=C:\Hew2\TClassTestZ_3\TClassTestZ_3\Debug\TClassTestZ_3.obj
                                  00002e34  00002e6f        3c
  _C_LedChikaParam
                                  00002e34        3c   data ,l         1

-----------------------------------------------------------

 

さて、

コンストラクタを入れると・・・

Initialization with "{...}" is not allowed for object of type "const TParam"

 

ビルドエラー

 

小細工しても、ビルドエラー

 

Initialization with "{...}" is not allowed for object of type "const TParam"

 

悔しいので、ルネサスにログインして、

最新のHEW4をゲット

(ルネサスの登録が広島の住所だったし・・)

 

 

HEWを入れてビルドしてみたけど、

コンストラクタを付けたら、ビルドが通らないじゃないか!

 

 

 

これは、

構造体クラスにコンストラクタを入れてビルドが通るかどうかは、

コンパイラ依存になるのではないか!?

 

疑惑が深まりました。。。

 

 

 

くうぅ

 

ねる!

-----------------------------

合格合格合格合格合格合格合格合格合格合格合格合格合格合格合格合格

 

 

追記

 

 

はい寝ぼけてましたね。

修正してビルドを通しました。

コンストラクタを付けた場合の書き方はこうすれば通せますね。

------------------------------------

const TParam  C_LedChikaParam[] = 
{
    TParam( 100 ,true )
  , TParam( 900 ,false)
  , TParam( 200 ,true )

------------------------------------

 

で、結果ですが意外な結果になりました。

 

コンストラクタなどの関数がない場合

------------------------------------------------

SECTION=C
FILE=C:\WorkSpace\TClassTest_Z_3\TClassTest_Z_3\Release\TClassTest_Z_3.obj
                                  000009d0  00000a0b        3c
  _C_LedChikaParam
                                  000009d0        3c   data ,l         * 

------------------------------------------------

 

コンストラクタを付けた場合

------------------------------------------------

SECTION=B
FILE=C:\WorkSpace\TClassTest_Z_3\TClassTest_Z_3\Release\TClassTest_Z_3.obj
                                  0000fd80  0000fdbf        40
  _C_LedChikaParam1
                                  0000fd80         4   data ,l         * 
  _C_LedChikaParam
                                  0000fd84        3c   data ,l         * 

------------------------------------------------

 

意外や意外、

同じ容量になりました。

 

ただ、コンストラクタを付けると、SECTION=Bに割当たっています。

未初期化領域扱いになりますね。

 

謎。

 

うーん。。

手間はかかるものの、コンストラクタを付けてもいいかもしれませんね。

 

ちなみに、HEWでビルドエラーになった理由ですが、

 

H8コンパイラの最終更新年月日は、2009年です。

 

C++の11から配列の初期化が変わったっぽいです。

おそらく、それが影響してるんじゃないかと思います。

 

 

ということは、

今回の結果は正しいとは言い切れないかもしれない。。

 

 

SAIでした。

 

【電子工作番外編】Z-2.C++番外編 構造体の代用 Tクラスにコンストラクタは必要か?

 

こんばんわ。

SAIです。

 

最近花粉症がひどいです。

仕事が終わり次第、眠気に負けて寝てる感じですね。

起きたら10時でした・・・

 

さて、今回のお題です。

 

すべてのClassには、必ずコンストラクタが必要か?

 

うーん。回答に困りました。

 

メンバ変数を持っていて、newをするクラスには必ずコンストラクタをつけたいです。

 

では、newをしないクラスにも、コンストラクタを付ける必要があるのか?

 

やっぱり初期値などを設定するため、基本的には付けたいです。

また、コピーコンストラクタもあるべきですよね。

 

 

がしかし、ちょっと目をそむけていた点が気になりました。

C++だと、構造体を使わずクラスを使います。

これを配列にしてデータを定義することがありますよね。

 

こんな感じ↓

 

// 点灯制御用構造体クラス //
class TParam
{
public:
  int   iDelay;
  bool  iStat;
};

// LED点灯時間を入れる配列 //
const TParam  C_LedChikaParam[] = 
{
    { 100 ,true }
  , { 900 ,false}
  , { 200 ,true }
  , { 800 ,false}
  , { 300 ,true }
  , { 1000 ,false}
};

これ、TParamクラスを連続して配置した配列です。

 

これにコンストラクタを付けるとどうなるのでしょうか?

 

SAIはずっとこの問題を避けていました。

いや、本当はただ目をそむけていただけではなく、

関数軍も配列にアドレス変わり当たっちゃうはずなので、

メモリがもったいないという考えで

わざと避けていたという言い訳をしていたというのもあります。

 

 

コンストラクタを追加すると、

クラス生成時にメンバーが一つ増えてしまうことになります。

おそらく、デフォルトのコピーは使えなくなる予想です。

 

というわけで、コンストラクタを付けてビルドを試してみます。

 

まずは、いたずら的に、引数なしのデストラクタで試すと、、、

TParam::TParam(){};

 

まぁ、コンストラクタの引数なしに対して、

operater?の機能で無理やり2つの引数を渡そうとしているので、
当然エラーになりますよね。
 
というわけで、まじめに作ります。
 
配列でメンバーに渡したい変数は2つです。
順番は、(1)int (2) boolの順ですね。
 
というわけで、以下のようなコンストラクタが欲しいです。
 
TParam(int aDelay , bool aStat);
 
続いて、Tクラス(構造体クラス)を作成するということは
コピーコンストラクタを作っていないとデータの移管が面倒です。
 TParam(const TParam &aClass);
 
あと operater = 演算子は・・・今度考えます。
 
この二つを追加してみました。
 
基本的には、①のコンストラクタが代入を処理してくれるはずです。
 
処理のイメージですが、
 
const TParam  C_LedChikaParam[] = 
{
    { 100 ,true }    // ・・・・(A)
};
 
この配列を初期化するときには、
(A)のタイミングで、
TParam(  TParam(100 ,true ) ); という処理が働いているイメージ。

さて、
どうなったでしょうか?
 
 
はい、コンパイルを通すのは楽勝。

さすがSAI 晴れ晴れ (自画自賛)
 
 
 
さて、
ここからがSAIの懸念です。

最初の配列の場合、
クラス生成時に必要とするメモリは、
 
コンストラクタなしクラスのの配列の場合、
メンバ変数の2つが配置されます。
 
const TParam  C_LedChikaParam[] = 
    { 100 ,true }
 
コンストラクタありクラスの配列の場合、
メンバ変数の2つに加え、関数軍が配置されるはずだと考えています。
 ↓の赤い部分は見えないけど配置されてるんじゃないかと予想です。
 
const TParam  C_LedChikaParam[] = 
    { 100 ,true (,コンストラクタ、コピーコンストラクタ、セット関数)}
これを確認する方法ですが・・・・

 
リンカーが動作したときに生成されるMAPファイルを見ればわかるのですが・・・
ArduinoだとMAPファイルが見つけられませんでした。。
 
ほかに見る方法がないか設定を色々調べていたのですが、
不明!
 
代わりに、ビルド完了時に
スケッチサイズグローバル変数のサイズというのを見つけました。
                     
 
const 宣言しているので、スケッチ側が増えるはず。
 
ここで比較ができそうですね。
コンストラクタなしのビルド結果はスケッチが968バイト、変数が55バイト
 
この状態に対して、配列を一行要素追加した場合、
スケッチが970バイト、変数が57バイト
変数が二つ追加されたので、
想定通りスケッチサイズが2バイト分だけ増えましたが、
想定とは違いグローバル変数も増えました。
 
なぜ変数が増えたんだろう。
ArduinoはRAMが増えるのかな?
 
 
さて、関数を追加したほうはどうなったでしょうか?
コンストラクタありのビルド結果はスケッチが1088バイト、変数が54バイト
 
・・・・
は?
 
なんでコンストラクタ付けたら変数サイズが減るんや・・
 
意味不明。。
 
この状態に対して、配列を一行要素追加した場合、
スケッチが1094バイト、変数が57バイト

変数が二つ、関数が五つ追加されたのですが、
想定とは違いスケッチは6バイト変数が3バイト増えました。
は?
この状態に対して、さらに配列を一行要素追加した場合、
スケッチが1108バイト、変数が60バイト
変数が二つ、関数が五つ追加されたのですが、
想定通りスケッチは14バイト変数が3バイト増えました。
 
うーん。
色々試していたのですが、
結局の変数が増える理由はよくわかりません。
 
コンストラクタなし配列
3列の時 変数19バイト スケッチ932バイト
4列の時 変数21バイト スケッチ934バイト
5列の時 変数25バイト スケッチ938バイト
6列の時 変数27バイト スケッチ940バイト
7列の時 変数31バイト スケッチ944バイト
8列の時 変数33バイト スケッチ946バイト
9列の時 変数37バイト スケッチ950バイト
 
※スケッチ増加は、2バイトか4バイト
 
コンストラクタあり配列
4列の時 変数21バイト スケッチ990バイト
5列の時 変数24バイト スケッチ1000バイト 
6列の時 変数27バイト スケッチ1010バイト
7列の時 変数30バイト スケッチ1020バイト
8列の時 変数33バイト スケッチ1030バイト
9列の時 変数36バイト スケッチ1040バイト
※スケッチ増加は、10バイト
 
少なくとも、当初予定通りスケッチの容量増加量は
コンストラクタがついたせいで関数分くらいは増えています。
 
ちょっともったいない気がしますね。
 
 
もっと正確に知るには・・・
 
H8の環境を引っ張り出してきて、
MAPをチェックするしかないかなぁ・・・
 
とりあえず、容量が無駄に増えそう・・・ということは確かでしょうね。
 
 
というわけで、
構造体クラスにコンストラクタは、
場合によっては付けないほうがいいんじゃないかという
認識は変わらずです。
 
ただ、ルールにするには条件の書き方が難しそうですね。
 
配列に使用する場合は許可するとかでしょうか・・・
 
 

晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ

雑談

コード書いてビルド通すのに10分だった癖、動作が意味不明で
2時間くらい悩んだという。。
H8環境もwindows10で動くようにするかな・・・

晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ晴れ

 

 

本日使ったコードは以下の通りです!

宇宙人しっぽ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人からだ宇宙人あたま


// 点灯制御用構造体クラス //
class TParam
{
//* コンパイルスイッチの代わり
public:
  TParam(int aDelay , bool aStat);
  TParam(const TParam &aClass);
  inline int GetDelay(){return iDelay;}const;
  inline bool GetStat(){return iStat;}const;
  void SetStat(int aDelay , bool aStat);
// */
public:
  int   iDelay;
  bool  iStat;
};

//* コンパイルスイッチの代わり
// コンストラクタ //
TParam::TParam(int aDelay , bool aStat)
:iDelay(aDelay),iStat(aStat)
{
};
// コンストラクタ //
TParam::TParam(const TParam &aClass)
:iDelay(aClass.iDelay),iStat(aClass.iStat)
{
};
void TParam::SetStat(int aDelay , bool aStat)
{
  iDelay = aDelay;
  iStat  = aStat;
};
// */

//LED点灯時間を入れる配列 //
const TParam  C_LedChikaParam[] = 
{
    { 100 ,true }
  , { 900 ,false}
  , { 200 ,true }
  , { 800 ,false}
  , { 300 ,true }
  , { 700 ,false}
  , { 400 ,true }
  , { 600 ,false}
  , { 500 ,true }
  , { 500 ,false}
  , { 600 ,true }
  , { 400 ,false}
  , { 700 ,true }
  , { 300 ,false}
  , { 1000 ,false}
};


// 点滅時間確認用ID //
const int C_TestPinId = 13;

void setup() {
  // 出力設定 //
  pinMode(C_TestPinId, OUTPUT); 
  // 動作確認用LEDのPIN制御 //
  digitalWrite(C_TestPinId, LOW);    // IO OFF 
}

void loop() {
    int lCnt = sizeof(C_LedChikaParam)/sizeof(TParam);
    
    for(int i = 0; i < lCnt;i++)
    {
     digitalWrite(C_TestPinId , C_LedChikaParam[i].iStat);    // IO OFF 
     delay(C_LedChikaParam[i].iDelay);
    }
}