C言語でオブジェクト指向プログラミング~②継承の検証
はじめに
非オブジェクト指向プログラミング言語であるC言語を使用して、オブジェクト指向の3大要素である「カプセル化」「継承」「ポリモーフィズム」をどこまで実現可能かを検証してみます。
今回は、継承の実現について検証しています。
方針
C言語の文法には構造体はありますが、クラスはありません。ここでクラスとは、要素と、要素に対しての振る舞いを定義したものであるとします。構造体では要素を持てますが、要素に対する振る舞いを持つことは出来ません。そこで、構造体の持つ要素に対して振る舞いを行う関数を用意し、その関数のポインタを構造体に持たせることで、構造体が振る舞いを持ったような感じにします。
環境
Windows 10 Pro機にEclipse(厳密にはPleiades)を導入して、検証します。
フォルダ構成とファイル
今回の検証で使用した、フォルダ構成とファイルの配置です。
[プロジェクトrootフォルダ]
┣srcフォルダ
┃ ┣main.c
┃ ┣t_base.c
┃ ┗t_ext1.c
┗includeフォルダ
┣t_base.h
┗t_ext1.h
BASEクラスと、BASEクラスを継承したEXT1クラスを使用して、検証します。
t_base.h
#ifndef T_BASE_H_
#define T_BASE_H_
typedef struct _base TBASE, *BASE;
struct _base_member;
struct _base {
struct _base_member* m;
void (*setNumber)(BASE, long);
long (*getNumber)(BASE);
void (*addNumber)(BASE);
void (*subNumber)(BASE);
};
BASE new_BASE(void);
void delete_BASE(BASE this);
void construct_BASE(BASE);
void destructor_BASE(BASE);
#endif /* T_BASE_H_ */
BASEクラスの定義ファイルです。
t_ext1.h
#include "../include/t_base.h"
#ifndef T_EXT1_H_
#define T_EXT1_H_
typedef struct _ext1 *EXT1;
struct _ext1_member;
struct _ext1 {
TBASE my; // オーバーライド用
TBASE super; // superクラス参照用
struct _ext1_member* m;
void (*setString)(EXT1, char*);
char* (*getString)(EXT1);
};
EXT1 new_EXT1(void);
void delete_EXT1(EXT1 this);
void construct_EXT1(EXT1);
void destructor_EXT1(EXT1);
#endif /* T_EXT1_H_ */
BASEクラスを継承したEXT1クラスの定義ファイルです。
BASEクラスを継承するにあたり、BASEクラス自身を持つ必要があります。その際に、オーバーライドする場合と、EXT1クラス内でBASEクラスの振る舞いを使用する場合(superとして使用する場合)の2パターンに備えて、以下のようにBASEクラスの要素を2つ持っています。
TBASE my; // オーバーライド用
TBASE super; // superクラス参照用
ここでは、superよりもmyを先に持たせているのがポイントです。
t_base.c
#include <stdio.h>
#include <stdlib.h>
#include "../include/t_base.h"
struct _base_member {
long number;
};
static void setNumber(BASE this, long number);
static long getNumber(BASE this);
static void addNumber(BASE this);
static void subNumber(BASE this);
BASE new_BASE(void) {
BASE this;
this = (BASE)malloc(sizeof(struct _base));
return 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->subNumber = subNumber;
}
void destructor_BASE(BASE this) {
free(this->m);
}
void delete_BASE(BASE this){
destructor_BASE(this);
free(this);
}
void setNumber(BASE this, long number){
this->m->number = number;
}
long getNumber(BASE this){
return this->m->number;
}
void addNumber(BASE this){
this->m->number++;
}
void subNumber(BASE this){
this->m->number--;
}
BASEクラスの中身です。
t_ext1.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../include/t_ext1.h"
struct _ext1_member {
char* string;
};
static void setNumber(EXT1, long);
static void setString(EXT1, char*);
static char* getString(EXT1);
EXT1 new_EXT1(void) {
EXT1 this;
this = (void*)malloc(sizeof(struct _ext1));
return this;
}
void construct_EXT1(EXT1 this){
construct_BASE(&(this->my));
construct_BASE(&(this->super));
this->m = (struct _ext1_member *)malloc(sizeof(struct _ext1_member));
this->my.setNumber = setNumber;
this->setString = setString;
this->getString = getString;
}
void destructor_EXT1(EXT1 this){
destructor_BASE(&(this->super));
destructor_BASE(&(this->my));
free(this->m);
}
void delete_EXT1(EXT1 this){
destructor_EXT1(this);
free(this);
}
void setNumber(EXT1 this, long number){
if (number > 10) {
this->super.setNumber(this, 10);
}
else {
this->super.setNumber(this, number);
}
}
void setString(EXT1 this, char* string){
this->m->string = string;
}
char* getString(EXT1 this){
return this->m->string;
}
EXT1クラスの中身です。
ここでは、setNumberメソッドをオーバーライドしています。
また、EXT1クラス独自の要素としてstringを持ち、これを隠蔽しています。
main.c
#include <stdio.h>
#include <stdlib.h>
#include "../include/t_base.h"
#include "../include/t_ext1.h"
int main(void) {
BASE base = new_BASE();
EXT1 ext1 = new_EXT1();
construct_BASE(base);
construct_EXT1(ext1);
base->setNumber(base, 1);
((BASE)ext1)->setNumber(ext1,11);
printf("[L.%d] %ld\n", __LINE__, base->getNumber(base));
printf("[L.%d] %ld\n", __LINE__, ((BASE)ext1)->getNumber(ext1));
base->subNumber(base);
printf("[L.%d] %ld\n", __LINE__, base->getNumber(base));
printf("[L.%d] %ld\n", __LINE__, ((BASE)ext1)->getNumber(ext1));
((BASE)ext1)->addNumber(ext1);
printf("[L.%d] %ld\n", __LINE__, base->getNumber(base));
printf("[L.%d] %ld\n", __LINE__, ((BASE)ext1)->getNumber(ext1));
ext1->setString(ext1, "Hallo! Welt!");
printf("[L.%d] %s\n", __LINE__, ext1->getString(ext1));
delete_BASE(base);
delete_EXT1(ext1);
return EXIT_SUCCESS;
}
BASEクラスのbaseオブジェクトでsetNumberやsubNumberした結果は、baseオブジェクトにのみ反映される予想です。
一方、EXT1クラスのext1オブジェクトでsetNumberやaddNumberした結果は、ext1オブジェクトのみに反映される予想です。
と同時に、BASEクラスの振る舞いであるsetNumberやaddNumberを、ext1オブジェクトで使用可能である、という予想です。
また、EXT1クラス独自の振る舞いsetStringとgetStringが、ext1オブジェクトで使用可能である、という予想です。
実行結果
ビルドして実行してみます。
[L.16] 1
[L.17] 10
[L.20] 0
[L.21] 10
[L.24] 0
[L.25] 11
[L.28] Hallo! Welt!
想定通りの結果となりました。
終わりに
今回の検証ではビルド時に、複数の警告が発生しました。いずれも型不一致に関する内容でした。プロジェクトのコーディング規約で、型不一致の警告発生を許可していない場合は、この方法を採れませんので、その点はご了承ください。
親クラスの振る舞いの呼び出し方には、実は2パターンあります。1つは今回の検証内容ですが、親クラスにキャストして呼び出す方法です。
((BASE)ext1)->setNumber(ext1,11);
もう1つは以下のように、my要素を明示して呼び出す方法です。
ext1.my->setNumber(ext1,11);
後者の方法だと、継承の階層が深くなった場合に、myと書く回数が増えます。また、そのmyがどのクラスを指したmyなのかが分かりにくくなります。一方で前者の方法だと、myと書く必要がないため、前者に比べて可読効率が上がると思います。また、必ずキャストが発生することで、その振る舞いがどのクラスの振る舞いのものであるかを、視覚的に確認できます。
次回は、C言語で多様性の実現について検証してみます。