上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
新年あけましておめでとうございます!
明日から仕事ということで、大変憂鬱な気分になっているゆってぃでございます。

むひーーーーーーー!
もっと休みたいよーーーーーーーーーーーー!



さて・・・
今日は、C言語を使用したオブジェクト指向プログラミング技法について書きたいと思います。

みなさまご存知の通り、C言語はオブジェクト指向プログラミング言語(OOPL)ではありません。
ですので、JAVAやC++の様にバリバリのオブジェクト指向プログラミングをする事は困難です。
しかしながら、開発するシステムが大規模になると、オブジェクト指向型の設計の方がメリットがあることは、なんとなくイメージが出来ると思います。

オブジェクト指向にすると何が良いか?
設計がし易くなったり(UMLが適応しやすくなります)
ソフトの見通しが良くなったり(同上)
拡張性が向上したり・・・
メリットは沢山あります。

もちろん、デメリットもあります
コードが冗長になる
 →メモリ使用量(ROM/RAM)が増える、リアルタイム性も落ちる・・・
関数ポインタなどを多用する
 →慣れるまでが大変
などですね。

話を戻します。
先述の通り、C言語で出来るオブジェクト指向(っぽい)プログラミング方法を伝授いたします。
特に、組み込みシステム向けということで書きたいと思います。
ただし、継承とかオーバーロードとかオーバーライドはありません。
ググってみると、Cでもこれらを実現している歴戦の猛者はいらっしゃるのですが、ゆってぃでは使いこなすことが出来ないので、ここでは議論をしません。
もはやこの時点でオブジェクト指向ではない雰囲気むんむんですが
あくまでオブジェクト指向風味ってことで
有権者の方々にはひとつよろしくお願い申し上げたい所存でございます。

(1)クラスの定義は、構造体で実現する
C言語にはクラスという概念はありませんが、構造体は存在します。
んなことは知っているよ!
で、ですよね!すみません…(涙)

ただ、構造体で定義した変数は全てpublicメンバとなってしまい、カプセル化が実現できていません。
また、メソッドを持つことも出来ません。
そこで、以下の様に構造体を作成します。

 publicメンバ…構造体内で定義
 privateメンバ…構造体内に、void型のポインタとして定義(変数構造体は、ソースファイルで定義)
 publicメソッド…関数ポインタで定義
 privateメソッド…static関数として、ソースファイルで定義

実際に例を見てみましょう。
ここでは、LEDを制御するためのLED制御クラス(そのままやないかい!)を例に取ります。

-------------------------------------------------
(Led.h)

// LED制御クラス
typedef struct st_Led{

//メソッド
void (*on)(struct st_Led *pL);
void (*off)(struct st_Led *pL);
en_PortState (*getState)(struct st_Led *pL);

//メンバ変数
void *mem;

}Led;

extern Led* initLed(en_LED_NUM n);
extern Led* getLed(en_LED_NUM n);

-------------------------------------------------
(Led.c)

typedef struct st_LedMem
{
Dout *LedPort;
en_LedState state;
} LedMem;

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

こんな風にしておくと、privateなメンバ変数は、Led.c内からしか参照不能になります。
なお、今回は言及しませんが、Doutクラスは自身のメンバ変数にレジスタのアドレスなどをもつドライバとしてのクラスです。
IOレジスタへのアクセスなどは、Doutクラスのメソッドを呼び出すことで実現します。
(こうしておけば、仮にマイコンが変わってもLedクラスはコピペで持ってくるだけで移植できます)

(2)クラスのインスタンスは、staticなグローバル変数として実現
組み込み屋さんは、malloc(C++ならnew)とかそれに付随するヒープ系の呪文を嫌がります。
というか、メモリマップ作る段階でヒープ領域なんか確保しない時もあります。
だって、RAMとか内蔵メモリの128kBとか64kBとか32kBだけで頑張る時も多いから
それっぽちのMP(RAM)じゃヒープ系は使えないんです!
(いや、全然使えるんですけど、メモリ確保失敗とか不安なんですよね)

じゃ、どうするか。
基本的に、メモリは静的に確保する訳です。
無闇にグローバル変数を増やすことはダメって習ったかもしれませんが、きちんと役割のあるオブジェクトとしての存在であれば大丈夫です。そもそも、static宣言してますしね(笑)

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

(Utility.h)

/*
* インスタンス生成マクロ
*/
#define INSTANCE_CREATE(ClassName, InstanceName) \
static ClassName InstanceName; \
static ClassName##Mem InstanceName##Mem
-------------------------------------------------
(Led.c)

/*
* ToDo
* クラスのインスタンス化
*/
INSTANCE_CREATE(Led, RedLed);
INSTANCE_CREATE(Led, BlueLed);

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

こうしておけば、RedLedとBlueLedおよびそのメンバ変数であるRedLedMemとBlueLedMemのインスタンスが生成されます。

(3)インスタンスの初期化は初期化テーブルで実現
上記まででインスタンスは生成されましたが、メンバ変数のインスタンスを、クラスそのもののインスタンスに紐付ける必要があります。
これは、初期化テーブルと初期化関数を用いて実現します。



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


(Led.c)

typedef struct st_LedInstanceTable
{
en_LED_NUM ledNum;
Led *LedInstance;
LedMem *LedMemInstance;
} LedInstanceTable;

static LedInstanceTable ledInstanceTable[] = {
{LED_1, &RedLed, &RedLedMem},
{LED_2, &BlueLed, &BlueLedMem},

{0xff, (Led*) 0, (void*) 0}, //番人
};

/*
* インスタンスの初期化とハンドル取得
*/
Led *initLed(en_LED_NUM n)
{

LedInstanceTable *lt;

//指定されたインスタンスをテーブルから検索
for (lt = ledInstanceTable; lt->LedInstance != (Led*) 0; lt++)
{
if (n == lt->ledNum)
{
break;
}
}

//メンバ変数インスタンスの紐付け
*(LedMem**)&lt->LedInstance->mem = lt->LedMemInstance;

//メンバ変数の初期化
((LedMem*) (lt->LedInstance->mem))->LedPort = initDout(n);
((LedMem*) (lt->LedInstance->mem))->state = LED_OFF;

//メソッドの初期化
lt->LedInstance->on = on;
lt->LedInstance->off = off;
lt->LedInstance->getState = getState;

//インスタンスの初期処理
lt->LedInstance->off(lt->LedInstance);

//インスタンスのアドレスを返す
return lt->LedInstance;

}

/*
* インスタンスのハンドル取得(初期化済みが前提)
*/
Led *getLed(en_LED_NUM n)
{

LedInstanceTable *lt;

for (lt = ledInstanceTable; lt->LedInstance != (Led*) 0; lt++)
{
if (n == lt->ledNum)
{
break;
}
}

return lt->LedInstance;

}

/*
* LEDをオンする
*/
static void on(Led *pLed)
{

((LedMem*) (pLed->mem))->LedPort->write(((LedMem*) (pLed->mem))->LedPort, PORT_HI);
((LedMem*) (pLed->mem))->state = LED_ON;
}

/*
* LEDをオフする
*/
static void off(Led *pLed)
{

((LedMem*) (pLed->mem))->LedPort->write(((LedMem*) (pLed->mem))->LedPort, PORT_LO);
((LedMem*) (pLed->mem))->state = LED_OFF;

}

/*
* LEDの状態を取得する
*/
static en_PortState getState(Led *pLed)
{
return ((LedMem*) (pLed->mem))->state;
}


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

(4)インスタンスへのアクセスはアドレス経由で行う
インスタンスはstaticな領域に確保してしまったので、別のオブジェクトから参照できません。
そこで、アドレス取得関数を使用し、他のオブジェクトからはアドレス経由でアクセスします。

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

(main.c)

int main(int argc, char** argv) {

Led *l;

initCpu();

l = initLed(LED_1);
l->on(l);

//以下略

}

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

以上が大まかな流れです。

こうすれば、例えば黄色のLEDが追加になった場合は、Led.c内で

INSTANCE_CREATE(Led, YellowLed);

とし、初期化テーブルに

static LedInstanceTable ledInstanceTable[] = {
{LED_1, &RedLed, &RedLedMem},
{LED_2, &BlueLed, &BlueLedMem},
{LED_3, &YellowLed, &YellowLedMem},

{0xff, (Led*) 0, (void*) 0},
};


と書いたりすればOKです。
(もちろん、別途レジスタの設定は必要ですが、それはブート時やドライバ層のクラスで行います)

いかがでしたでしょうか??
慣れるまでは大変ですが、このプログラミング法を用いれば、プログラムの設計がやりやすくなります。
CPU性能やリソースが豊富で、かつ、プログラムの規模がある程度大きくなりそうなときは、この様なプログラムを行うと、後々ラクになったりするかもしれません^^

なお、周波数が遅いマイコンではオススメしません。
最適化をどの程度かけるかにもよりますが、このプログラミング技法ではオーバーヘッドが大きくなるため、周波数が遅いと処理時間にクリティカルに影響してきます。
メモリ使用量も大きくなるため、実際に使用する場合は、事前検討を念入りに行う必要がある旨をご留意下さい。


因みに、今回ご紹介したコードは、オブジェクト指向のような雰囲気を出していますが、設計自体は構造化プログラミングです。
つまり、クライアント(main.c)がLedクラスを通じて、Doutクラスを制御するという階層構造ですね。
なぜかというと、冒頭に述べたように、この書き方ではカプセル化は実現できているものの、多態性が実現できていないからです。

実は、この書き方にもう少し工夫を施すと、よりオブジェクト指向の恩恵を預かることが出来るコードになります。
具体的には、変更や拡張に強いコードになります。
例えば、今回は出力ポートをハイにするとLEDがオンされるというソフトを書いていますが、別の回路では、出力ポートをローにした時、LEDがオンという仕様(負論理)になるかもしれません。
あるいは、オン時にいきなり点灯するのではなく、ぼわぁっとゆっくり点灯する仕様になるかもしれません。

ソフトの拡張で大切なことは2つあります。
1つ目は、クライアントが使用するLedクラスのメソッド(関数)を変更せずに、これらの仕様変更に対応すること
2つ目は、ソフト変更を行っても、変更箇所以外には影響しないこと
です。

現実装では、Ledクラスのもつonメソッドは、Doutクラスのもつwriteメソッドに処理を委譲しています。
ここを変更して、wirteメソッドの代わりに、それぞれの仕様に応じた関数を入れてあげればよい訳ですね。
そうすれば、クライアントからは同じようにonメソッドを呼ぶだけで良いし、Ledクラスもonメソッドの委譲先を変えるだけで済みます。新たに委譲先の関数を書く必要がありますが、委譲先として記述した関数は他の関数と独立しているので、互いに影響することが無いようにすれば問題ありません。
この様に、同じonメソッドでも動作を変えることが出来るのがオブジェクト指向であり、この特徴を多態性と呼びます。

具体的なアプローチとしては、デザインパターンの中のストラテジパターンという設計のテンプレートを用います。
ただし、ストラテジパターンはインターフェース(あるいは抽象クラス)を用いて実現することが多いので、C言語では少し工夫が必要です。
次回は、このパターンを用いて、今回のコードを改良したいと思います。

今回はずいぶん長くなってしまいましたが、最後までお読みいいただいて、ありがとうございました!

今年もよろしくお願いいたしますm(_ _)m
関連記事
スポンサーサイト
コメント
コメントの投稿
トラックバック URL
トラックバック
ご訪問者様
プロフィール

ゆってぃ

Author:ゆってぃ
経歴7年の組み込み系・制御系エンジニアです。
("ど素人"という文言は取りました…笑)
ソフトウェア開発経験ゼロの状態から、なんとか実務がこなせるようになってきた現在に至るまでの経験を、備忘録代わりに綴っていきたいと思います。
入門者の方、大歓迎!
(上級者の方、ごめんなさい…)

あと、ブログには全然関係ないですが、Bumpy Headというバンドのギターをやっています。
ライブ情報なんかも書いたりすることがあるので、その時に「行ってもいいよ~」といった感じのコメントを戴けると、泣いて喜びます(泣)
ブログ読んでくださってる方なら、チケット代サービスしちゃいます!

最後に…滅多に流用することは無いでしょうが、このブログに書かれているソースは、特に指定の無い限りMITライセンスとします。ただし、一部それ以外のものもございますのでご注意下さい。
※ブログのリンク先にあるコードに関しては、リンク先のポリシーに従ってください。

最新記事
最新コメント
カテゴリ
RSSリンクの表示
メールフォーム

名前:
メール:
件名:
本文:

twitter
リンク
ブロとも申請フォーム
スポンサードリンク


上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。