C言語でオブジェクト指向プログラミング~①カプセル化の検証
はじめに
非オブジェクト指向プログラミング言語であるC言語を使用して、オブジェクト指向の3大要素である「カプセル化」「継承」「ポリモーフィズム」をどこまで実現可能かを検証してみます。
今回は、カプセル化の実現について検証しています。
方針
C言語の文法には構造体はありますが、クラスはありません。ここでクラスとは、要素と、要素に対しての振る舞いを定義したものであるとします。構造体では要素を持てますが、要素に対する振る舞いを持つことは出来ません。そこで、構造体の持つ要素に対して振る舞いを行う関数を用意し、その関数のポインタを構造体に持たせることで、構造体が振る舞いを持ったような感じにします。
検証環境
Windows 10 Pro機にEclipse(厳密にはPleiades)を導入して、検証します。
フォルダ構成とファイル
今回の検証で使用した、フォルダ構成とファイルの配置です。
[プロジェクトrootフォルダ]
┣srcフォルダ
┃ ┣main.c
┃ ┗BASE.c
┗includeフォルダ
┗BASE.h
BASE.h
#ifndef BASE_H_
#define BASE_H_
struct _base_member;
typedef struct _base *BASE;
struct _base {
struct _base_member *m;
void (*setNumber)(BASE,long);
long (*getNumber)(BASE);
void (*addNumber)(BASE);
void (*delNumber)(BASE);
};
BASE new_BASE();
void delete_BASE(BASE this);
void construct_BASE(BASE this);
#endif /* BASE_H_ */
構造体のメンバに
struct _base_member *m;
とありますが、こうすることで、mの中身を外部から隠蔽しています。
setNumber、getNumber、addNumber、delNumberはそれぞれ関数のポインタ変数です。クラスの振る舞い(メソッド)を実現しようとしています。
関数のポインタ変数の第1引数を自クラス(厳密には構造体)として、関数内で自分自身のインスタンスをthisとして扱えるようにしています。
BASE.c
#include <stdio.h>
#include <stdlib.h>
#include "../include/BASE.h"
struct _base_member {
long number;
};
static void setNumber(BASE this, long channel);
static long getNumber(BASE this);
static void addNumber(BASE this);
static void delNumber(BASE this);
BASE new_BASE(){
return (void*)malloc(sizeof(struct _base));
}
void delete_BASE(BASE this) {
free(this->m);
free(this);
}
void construct_BASE(BASE this) {
this->m = (struct _base_member *)malloc(sizeof(struct _base_member));
this->setNumber = setNumber;
this->getNumber = getNumber;
this->addNumber = addNumber;
this->delNumber = delNumber;
}
void setNumber(BASE this, long number){
this->m->number = number;
}
long getNumber(BASE this) {
return this->m->number;
}
void addNumber(BASE this){
if( this->m->number + 1 <= 12) {
this->m->number++;
}
}
void delNumber(BASE this){
if ( this->m->number - 1 >= 1) {
this->m->number--;
}
}
ここで_base_member構造体は、BASEクラスのprivate属性を保持する構造体です。_base_member構造体の実体をソースファイルに記載することで、中身を外部から隠蔽することが出来ます。今回は、long型のnumber変数を外部から隠蔽します。
struct _base_member {
long number;
};
main.c
#include <stdio.h>
#include <stdlib.h>
#include "../include/BASE.h"
int main() {
BASE base;
base = new_BASE();
construct_BASE(base);
base->setNumber(base, 11);
printf("[L.%d] %ld\n", __LINE__, base->getNumber(base));
base->addNumber(base);
printf("[L.%d] %ld\n", __LINE__, base->getNumber(base));
base->addNumber(base);
printf("[L.%d] %ld\n", __LINE__, base->getNumber(base));
base->setNumber(base, 2);
printf("[L.%d] %ld\n", __LINE__, base->getNumber(base));
base->delNumber(base);
printf("[L.%d] %ld\n", __LINE__, base->getNumber(base));
base->delNumber(base);
printf("[L.%d] %ld\n", __LINE__, base->getNumber(base));
delete_BASE(base);
return 0;
}
まずBASEクラスのオブジェクトを生成して、new_BASE関数で動的にメモリを割り当てています。1行に書くこともできます。
BASE base;
base = new_BASE();
construct_BASEでオブジェクトを初期化しています。
construct_BASE(base);
setNumberでnumberに11をセットした後、addNumberを2回実行しています。
最初のaddNumberでは、numberを+1してもnumberは12以下であるため、numberは+1されて12になる想定です。
2回目のaddNumberでは、numberを+1するとnumberは12より大きくなるため、numberは+1されずに12のままである想定です。
次にsetNumberでnumberに2をセットした後、delNumberを2回実行しています。
最初のdelNumberでは、numberを-1してもnumberは1以上であるため、numberは-1されて1になる想定です。
次のdelNumberでは、numberを-1するとnumberは1より小さくなるため、numberは-1されずに1のままである想定です。
実行結果
ビルドして実行してみます。
[L.12] 11
[L.15] 12
[L.18] 12
[L.21] 2
[L.24] 1
[L.27] 1
想定通りの結果となりました。
と同時に、隠蔽されたnumber変数を、振る舞いから操作可能であることが分かりました。
終わりに
C言語では、いわゆる振る舞いをどのように実装するかは、幾つかの方法があります。1つは今回のように、関数のポインタ変数を構造体に持たせる方法です。他には、例えば「クラス名_振る舞い名」という関数名を外部に公開する方法です。(この場合は、構造体には振る舞い関数のポインタ変数を持たせません。)
struct _base_member;
typedef struct _base *BASE;
struct _base {
struct _base_member *m;
};
void BASE_setNumber(BASE this, long number){
this->m->number = number;
}
どちらにもメリットとデメリットが存在します。カプセル化という事だけを考えれば、後者の方法の方が実装がラクかもしれません。しかし継承やポリモーフィズムを考えると、後者の方法では厳しくなるか、或いは無理かもしれません。一方前者の方法では、継承やポリモーフィズムが行いやすいというメリットがあります。しかし関数ポインタ変数の数だけ、メモリ領域を消費します。
どちらを使うかは、その時に応じて選んでみてください。そもそも、C言語はオブジェクト思考的なプログラミング言語ではないのですから。
次回は、C言語で継承の実現について検証してみます。