LoginSignup
3

More than 5 years have passed since last update.

Cでの使いやすいオブジェクト指向"的"表現について考える。~その1 クラスの表現、継承

Last updated at Posted at 2018-04-28

0/2 はじめに void *
1/2 クラス表現と継承 ←ここ
1/2 2018/07/01 再考しました 再考:クラス表現と継承
2/2 インターフェイス, オーバーライド

C言語でのオブジェクト指向表現、本題に入ります。
今回はクラス表現と継承。

今回言っているクラスはメンバーを持つ普通のクラスの事と思ってください。
インターフェイスクラスだと表現、継承の印象が大分変わるので、次回改めて書きます。

クラスをどう表現するか

例えばJavaのこんなクラスを考えます。

HumanClass.java
public class HumanClass{
    public String name;
    private int age;

    //この人が年齢、(もしくは好みとして)ビールが飲めるのか?
    public bool can_drinking_beer();
    //この人がアラサー/アラフォーかの確認
    public bool is_alound(int generation);
}

Cで表現してみましょう。

Classメンバーの表現

基本ヘッダーにpublicな構造を定義し、Cファイルにprivateメンバーを書く感じですね。
public methodの前にまずは基本的な表現から。

HumanClass.h
typedef struct human_class{
    char * name;
} human_class;

human_class * human_class_new(コンストラクタパラメータ);
void human_class_init(human_class *, コンストラクタパラメータ);
void human_class_delete(human_class *);
HumanClass.c
#define HUMAN_CLASS_PUBLIC \
    char * name;

struct human_class_implement{
//先頭にpublic
HUMAN_CLASS_PUBLIC
    int age;
} human_class_implement;

//プライベートメソッドはCファイル内のstaticな関数で定義
static void is_you_want_to_use_private_metod(XXX);

human_class * human_class_new(コンストラクタパラメータ) {
    human_class_implement * instance = malloc(sizeof(human_class_implement));
    instance->name = malloc(名前の長さ);
    //初期処理
    return (human_class *)instance;
}

void human_class_init(human_class *this, コンストラクタパラメータ) {
    if(!this->name) {
        this->name = malloc(名前の長さ);
    }
    //初期処理

}

void human_class_delete(human_class *this) {
    human_class_implement * instance = (human_class_implement *)this;
    //ここではinstance->ageが使えます。
    ...
    free(instance->name);
    free(instance);
}

先頭にpublicメンバーを定義し、インスタンスを渡す際にhuman_classにcastしてあげます。
後はinitメソッドがあると実体の初期化も出来ますね。

human_classクラスと同じ定義でpublicメンバーを書くいいと思います。

使い方については1点注意が。
human_class_newを使わないと、privateメンバーのメモリが確保されない!

というわけで、privateメンバーが必要なら必ずリソースはnew APIで確保しましょう。

Object指向関係なく、大抵の場合はnew, free用のAPIはを用意しておくのが好きです。
理由はリソース管理がhuman_classの実装内で完結していることがわかるからです。
はい、好みです。

4/29追記 コメントでいただきましたが、このクラスをどう表現するか?にも様々な手段、趣向があるんですね。別途時間を見つけて考えて纏めてみたいと思います。

さて、メンバーはいいけどpublicメソッドはどうしましょうか。
手段は2つあるかな。
1. 公開APIで表現
2. 関数ポインタで表現

どちらも好きですが、私としては1派です。

Public methodの表現1. 公開APIを使う

こんな感じ

HumanClass.h
typedef struct human_class{
    char * name;
} human_class;

human_class * human_class_new(コンストラクタパラメータ);
bool human_class_can_drinking_beer(human_class *);
bool human_class_is_alound(human_class *, int);
void human_class_delete(human_class *);

ヘッダーにそのままpublicメソッドを定義します。実体がないので実体をもらいます。
"human_classのpublicメソッドである"ことがわかる名前の付け方をしておけば、
「human_classはこれで扱うのね。触っていいメンバーはnameなのね」ってのが直感的にわかりやすいと思います。

Public methodの表現2. 関数ポインタで表現

こんな感じ。実装側で定義した関数をcan_drinking_beer, is_aloundに代入します。

HumanClass.h
typedef struct human_class{
    char * name;
    bool (*can_drinking_beer)(human_class *);
    bool (*is_alound)(human_class *);
} human_class;

human_class * human_class_new(コンストラクタパラメータ);
void human_class_delete(human_class *, int);

使う側はこんな感じ。

izakaya.c
#include "HumanClass.h"

human_class * customer;

int come_customer(void) {
    customer = human_class_new(XXX);

    if(!customer->can_drinking_beer(customer)) {
        printf("お客様、最初の飲み物のご注文はお決まりですか?");
    } else {
        printf("お客様、最初の飲み物のご注文はお決まりですか?生にいたしますか?");
        }

    return;
}

int gohome_customer(void) {
    //お会計で年齢に悩む
    if(customer->is_alound(customer, 40)) {
        printf("...そっと40代のボタンを押す店員");
    }
    human_class_delete(customer);
}

よりオブジェクト指向言語の記載に近いですが、thisポインタは明示的に渡してあげないといけないんですよね。
好みがありそうですが、
私としては、is_aloundの定義がいらない分human_class_is_aloundを直接呼んだ方が見やすい気がします。

オブジェクト指向言語でも、thisを渡すメソッドなら、staticメソッドで実現しますよね?きっと。

Cでのクラス表現 まとめ

  • ヘッダー定義にpublicなクラス構造を定義し、new, delete(, init)を用意する。
  • Cファイル内にprivateメンバーを(publicメンバーの)下に追加して拡張。
    • newで返す実体は拡張したものをキャストOK
  • privateメンバーを利用したい際は、必ずnew, deleteでリソース確保

正直クラスの意識をしなくても、これだけ守って実装するだけでCの実装が一気に見やすくなると思います。

クラス継承をどう表現するか

HumanClassを継承したクラスについて考えましょう

HumanClass.java
public class FamilyClass extends HumanClass{
    public String fatherName;
    public String motherName;
    public String[] childrenName;
}

家族構成が追加されました。さあどうしましょかね。

privateメンバーがいるなら必ずリソースはそのクラスのnewで確保という前提があるので、
前提として親クラスにprivateメンバーがいない場合の継承について書いていきます。

こちらは2つ紹介
1. 親クラスのメンバーを持つ
2. 親クラスと同じ定義を先頭に書いてあげる。

1か2で選ぶなら2です。

クラス継承の表現 1: 親クラスのメンバーを持つ

C 言語によるオブジェクト記述法 COOL ver.2がこのイメージです。
human_classは公開APIとしてpublicメソッドを使ってクラスを表現した場合でサンプルを書きます。

FamilyClass.h
typedef struct family_class_{
    human_class super;
    char * fatherName;
    char * motherName;
    int children_num;
    char ** children;
} family_class;

family_class * family_class_new(XXX);
FamilyClass.c
#define  super_init(this, XXX) human_class_init(human_class*)(this), XXXX )
#define  super_can_drinking_beer(this) human_class_can_drinking_beer(human_class*)(this))

family_class * family_class_new(XXX) {
    family_class * family = (family_class*) malloc(sizeof(family_class));

    super_init(family, XXX)
    ///初期処理色々

    //メンバーはsuperをつけてアクセス
    printf("name:%s", family->super.name)

    //親メソッド呼び出しはマクロでキャストして使う。
    if(super_can_drinking_beer(family)) {
        printf("ビールが飲めます!");
    }

    return family;
}

メソッドはいいけど、メンバーアクセスにsuperが付くのが付いちゃうんですよね。

クラス継承の表現 2: 親クラスと同じ定義を先頭に書いてあげる。

HumanClass側のヘッダーに少し工夫を施します。
関数ポインタで表現した方が良さがイメージしやすいのでこちらの改変

HumanClass.h
typedef struct human_class{
    char * name;
    bool (*can_drinking_beer)(human_class *);
    bool (*is_alound)(human_class *);
} human_class;

#define HUMAN_SUPER \
    char * name;\
    bool (*can_drinking_beer)(human_class *);\
    bool (*is_alound)(human_class *);\

human_class * human_class_new(コンストラクタパラメータ);
void human_class_delete(human_class *, int);
//継承相手向けにhuman_class_init(human_class* this, XXXX )みたいな関数も定義する。

継承用の定義記載をHUMAN_SUPER で用意してあげます。
継承したい人は、同じ定義を先頭に記載。

FamilyClass.h
typedef struct family_class_{
//先頭に定義を羅列。用意されたマクロを書くだけ
HUMAN_SUPER
    char * fatherName;
    char * motherName;
    int children_num;
    char ** children;
} family_class;

family_class * family_class_new(XXX);

すると、同じ名前でメンバーアクセスが可能になります!

FamilyClass.c
#define  super_init(this, XXX) human_class_init((human_class*)(this), XXXX )

family_class * family_class_new(XXX) {
    family_class * family = (family_class*) malloc(sizeof(family_class));

    super_init(family, XXX)
    ///初期処理色々

    //メンバーは直書きでOK!
    printf("name:%s", family->name)

    //親メソッド呼び出しはキャストは必要です。
    if(family->can_drinking_beer((human_class*)family)) {
        printf("ビールが飲めます!");
    }

    return family;
}

こちらの方がより継承チックなのでいいかなと。

多重継承はどうするの?

複数のクラスを継承させたいとなると、一気にわかりにくくなります。位置をずらさなきゃいけないんですよね。
human2_classという別のクラスを継承しましょう。

FamilyClass.h
typedef struct family_class_{
    human2_class super2;
    human_class super1;
    char * fatherName;
    char * motherName;
    int children_num;
    char ** children;
} family_class;

family_class * family_class_new(XXX);

で、human1_classのメソッドを呼びたいんだけど…

FamilyClass.c
//位置が先頭じゃないので、human_classの場所を与えてあげないといけない。
void call_super1_init(family_class * this, XXX) {
    human_class_init(&this->super1, XXXXXXXXX);
}

family_class * family_class_new(XXX) {
    family_class * family = (family_class*) malloc(sizeof(family_class));

    call_super1_init(family, XXX)
    ///初期処理色々
    ...

    return family;
}

位置をずらすため、ラッパーを用意しなきゃいけません。

Cでのクラス継承表現 まとめ

1 継承らしく利用出来るのは、親クラスにprivateメンバーがいるときのみ
2 うまく書けば継承を上手に表現できる。
3 多重継承は位置検索のラッパーが必要。

…制約中々ありますね。特に1, 3があるので、私のとしては、無理に継承を実現するメリットがあまりないかなと。
なのでいっそ、「クラス設計の継承をCで実装する時は集約で実現する」
としてしまった方がいいと思っています。

クラス設計と実際のコードにギャップがありますが、そこは

FamilyClass.h
typedef struct family_class_{
    human_class * super;/*親クラスとしてhuman_class を保持*/
    char * fatherName;
    char * motherName;
    int children_num;
    char ** children;
} family_class;

とか1行コメント入れてあげればいいかと。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3