みなさんこんにちわ、こんばんわ
SAIです。
さて、前回はC++言語で関数ポインター呼び出しを
コールバック関数に利用しようとしても、うまくいきませんでしたね。
では、C++言語ではどのようにコールバックを実装したらよいのでしょうか?
その方法は純粋仮想関数(pure virtual function)で実装する方法です。
これを理解するには、もっと基本的なところから勉強する必要があります。
少し話がそれたところからスタートしますが、
頑張ってついてきてくださいね!
C++言語はオブジェクト指向言語ということで、
継承(inheritance) という機能があります。
難しそうな単語が出てきましたね。
◆継承とは
継承は、親から子供に受け継がれるというイメージです。
絵にすると判りやすいと思います。

(Fig:1継承)
親クラスHogehogeBaseというクラスを、
子クラスHogehogeChildというクラスが継承したとします。
(左図の2つで、継承方法は★後述★)
すると、
親クラスの関数や変数(priveteを除く)が
子供のクラスでも自身の持っている機能として扱えるようになるというものです。
また、継承をする際には、関数や変数に色々付加することができます。
継承する際に親クラス側にvirtual(仮想)と書いている関数に関しては、
子供がoverrideとして、処理を進化することもできるのです。
仮想とある通り、子が独自進化する可能性があるかもしれないことを示唆しているのです。
(この場合、親の能力は消えて子供の能力だけになります※下図赤字)

(Fig:2仮想関数)
面白いでしょ!
クラスは”構造体みたいなもの”といいましたが、
子供のクラス扱えば、子供のクラスと親クラスのすべてを使うことができます。
オブジェクト指向言語の面白いところは、
この子供の実体を親クラスのポインターとして扱うこともできるのです。
ただし、その場合使用できる関数は、親クラスに定義しているものだけになります。
が、
Fig:2仮想関数に記載のFunc1は、実体である子クラスの能力になりますよね。
そうです。
親クラスの型で呼び出したとしても、
virtualがついている関数に関しては、子クラスが呼び出されるのです。
さらに、仮想関数にはさらに面白い機能があり、
残念ながら親は持てなかった機能関数を、
子供は絶対にこの機能関数を持たせるんだ!!
と強い意志の元、約束の機能を定義することもできるのです。
この約束の機能のことを純粋仮想関数(pure virtual function)といいます。
現実だったら、なんて傲慢な親なんでしょうね。(笑)
純粋仮想関数は、以下図のFunc1()のようなイメージです。

(Fig:3 純粋仮想関数)
この純粋仮想関数は
親クラスには実体はないけど、子供が絶対に実装します!と約束したものです。
なので、純粋仮想関数は、派生した子は必ず処理を実装する必要があります。
親は持っていないんだけど、
親クラスの型であるHogehogeBaseとしてFunc1()を呼び出せば、
必ず子クラスが処理を持っていることになるので、
親クラスの型でも、Func1()はあるものとして使うことができるのです。
さて本題です。
コールバック関数をC++で実現するには・・・・
感の良い方はもう気づいたでしょうかね?
コールバックを作る側は、
純粋仮想関数を定義した親クラスを作ってやればよいのです。
純粋仮想関数なので、クラス定義だけで十分です。
つまり、こんな感じ。
class MTimerCallBack
{
public:
virtual void CallBack(int aParam) =0;
};
実体の関数は、書かなくていいわけです。
これを提供して、利用する側はこれを継承した子クラスを実装し、
呼び出し側には以下のように、
子クラスのポインタを親クラスの型で渡してやればいいわけです。
SetCallBack(MTimerCallBack* aCallBackClass)
周期呼び出しだったら、コールバック関数はいらないね。
とりあえず、
今日は親クラスの型で配列に入れられるところのみを実装してみるよ!
それでは、後述するといったコードの実装の記載方法です。
まずは、継承です。
今まで作っていたクラスの記載に、以下の文章を付けるだけです。
class CSegControlClass
{
・・・
・・・
};
↓
class CSegControlClass :public MPeriodicCall
{
・・・
・・・
};
これだけです。
:public はおまじないだと思ってください。
たったこれだけで、継承ができます。
次に仮想関数
派生側には何も処理は必要ありません。
あえて言うなら関数の後ろにoverrideと書いた方がより明確ですね。
void SetOutput(int aOutput);
↓
void SetOutput(int aOutput)override;
親クラス側は、関数定義の前に virtual と書いて、仮想関数だよと定義する必要があります。
virtual void SetOutput(int aOutput);
そして、純粋仮想関数にしたいときは、プロトタイプ宣言の定義の最後に =0 と書くだけです。
virtual void SetOutput(int aOutput) = 0;
なんと簡単!
覚えてしまえば使い勝手が良いので、是非覚えましょう!





























作ったコードは以下の通り。
◆継承用のクラス ”MPeriodicCall.h”
#ifndef _MPERIODIC_CALL__
#define _MPERIODIC_CALL__
/*
* @file MPeriodicCall
* @brief Lesson B_2
* @author SAI
* @date 2021/06/27
* @note 定期呼び出し用の呼び出し関数継承用のクラス
*/
class MPeriodicCall
{
public:
virtual void SetOutput(int aOutput) =0;
};
#endif //_MPERIODIC_CALL__
◆継承 SegControlClass.hの改造
↓で使ったコードの改変になります。
53行目のクラス定義を、継承する形に変更
class CSegControlClass
↓
class CSegControlClass :public MPeriodicCall
60行目を
void SetOutput(int aOutput);
↓
void SetOutput(int aOutput)override;
ちなみに、ソース側は修正不要です!
◆メインinoファイル
/*
@file CPP_L_B_2_TestCLASS
@brief Lesson B_2 Classをcppファイルに分離みよう。
@author SAI
@date 2021/06/27
@note Classをcppファイルに分離みよう。
*/
/* *** Include List *** */
#ifndef __SegControlClass_
#include "SegControlClass.h"
#endif // __SegControlClass_
/* *** Define 定義 *** */
const int ASeg = 13;
const int BSeg = 12;
const int CSeg = 11;
const int DSeg = 10;
const int ESeg = 9;
const int FSeg = 8;
const int GSeg = 7;
const int DotSeg = 6;
const int LED_Port = 2;
const T7SegSetter C_7SegSetter =
{
// ON/OFF情報を設定
LOW
, HIGH
// 各ポートの番号を設定する/
, ASeg
, BSeg
, CSeg
, DSeg
, ESeg
, FSeg
, GSeg
, DotSeg
};
// 関数配列 //
// typedef void (*pFunc)(int aNum);
// pFunc FunctionList[3] ;
MPeriodicCall *FunctionList[3]; //定期呼び出しクラスの配列 //
/* LED制御クラス */
class CLedCtrl :public MPeriodicCall
{
public:
CLedCtrl(int aPort);
void SetOutput(int aOutput)override;
int iPort;
};
CLedCtrl::CLedCtrl(int aPort)
:iPort(aPort)
{
pinMode(iPort, OUTPUT);
}
void CLedCtrl::SetOutput(int aOutput)
{
if(aOutput%2 == 0)
{
digitalWrite(iPort, LOW);
}else
{
digitalWrite(iPort, HIGH);
}
}
/*
@note Arduinoで最初に呼ばれる関数
*/
void setup()
{
//今回はなし
pinMode(Keta1Seg, OUTPUT);
pinMode(Keta2Seg, OUTPUT);
pinMode(Keta3Seg, OUTPUT);
pinMode(Keta4Seg, OUTPUT);
digitalWrite(Keta1Seg, HIGH);
digitalWrite(Keta2Seg, HIGH);
digitalWrite(Keta3Seg, HIGH);
digitalWrite(Keta4Seg, HIGH);
}
/*
@note ぐるぐるループ
*/
void loop()
{
// --------------- こんな方法で設定してもいいけど --------- //
/* スタック上にクラスを生成 */
// T7SegSetter l7SegSetter;
//
// ON/OFF情報を設定
// l7SegSetter.iLED_ON = LOW;
// l7SegSetter.iLED_OFF = HIGH;
// 各ポートの番号を設定する/
// l7SegSetter.iASeg = ASeg;
// l7SegSetter.iBSeg = BSeg;
// l7SegSetter.iCSeg = CSeg;
// l7SegSetter.iDSeg = DSeg;
// l7SegSetter.iESeg = ESeg;
// l7SegSetter.iFSeg = FSeg;
// l7SegSetter.iGSeg = GSeg;
// l7SegSetter.iDotSeg = DotSeg;
// CSegControlClass lSegCtrlClass(l7SegSetter);
// -------- ↓のようにconst定数を作って設定したほうが楽だよね --------- //
/* 制御クラスを生成 */
CSegControlClass lSegCtrlClass(C_7SegSetter);
CLedCtrl lCedCtrl(LED_Port);
/* 配列に並べる */
FunctionList[0] = &lSegCtrlClass;
FunctionList[1] = &lCedCtrl;
/* 3つを同時に点灯 */
for (int i = 0 ; i < 100; i++)
{
//moto lSegCtrlClass.SetOutput(i);
// NG (void)(*FunctionList[0])(i); // 1秒ごとにカウントアップ //
FunctionList[0]->SetOutput(i); // 1秒ごとにカウントアップ //
FunctionList[1]->SetOutput(i); // 1秒ごとに点滅 //
delay(1000);
}
/* 関数終了時にスタックが解放されて、デストラクタが走る
同時に、デストラクタでLEDを消灯する*/
}