C言語でオブジェクト指向する【単一継承の方法】

317, 2021-09-01

目次

C言語でオブジェクト指向はできない?

オブジェクト指向?

うん、C言語じゃできないでしょ

できるよ

うそだ~

はい(コードを見せる

ぎょ、これは・・・構造体を継承している・・・?

オブジェクト指向は昔からある設計方針ですが、実はC言語でもオブジェクト指向は可能です。
ネズミくんはC言語ではできないと言ってますが、それは誤りです。
現にGtkなどのGUIライブラリはC言語によるオブジェクト指向で実装されています。

この記事ではネズミくんに見せたC言語による構造体の継承のコードを解説します。
C言語でオブジェクト指向が可能となる根拠の1つになるコードです。

関連記事
C言語で構造体を初期化する方法

設計方針

今回は構造体の単一継承がテーマです。
そのため、必要な構造体は2つです。

1つはAnimalという構造体で、もう1つはCatという構造体です。
CatAnimalを継承して作成します。

ではまずは先にAnimalのコードから見ていきましょう。

Animalの実装

Animalの実装で必要なファイルは2つです。
1つはAnimalの実装を書いたanimal.cでもう1つはヘッダファイルのanimal.hです。

先にanimal.hに構造体を書きます。それから必要とする関数のプロトタイプ宣言も。

#pragma once

#include <stdio.h>
#include <stdlib.h>

/**
 * Catのベース(親の構造体)となるAnimal構造体
 * ageとweightの属性を持つ
 */
typedef struct {
    int age;
    double weight;
} Animal;

/**
 * イニシャライザ
 * 確保されたAnimalのメモリを引数で初期化する
 */
Animal *
Animal_Init(Animal *self, int age, double weight);

/**
 * コンストラクタ
 * Animalを動的メモリの確保で構築する
 * 内部ではイニシャライザを呼び出す
 */
Animal *
Animal_New(int age, double weight);

/**
 * デストラクタ
 * 確保されたAnimalのメモリを破棄する
 */
void
Animal_Del(Animal *self);

Animal構造体の定義は非常にシンプルで、属性はageweightの2つだけです。
特に変わったところはありません。

必要となる関数は、コンストラクタとデストラクタ、それからイニシャライザです。
今回の設計では、コンストラクタで構造体の動的メモリを確保します。
そしてその確保したメモリをイニシャライザで初期化します。
デストラクタは確保したメモリを解放します。

Catについてもこの設計を引継ぎたいと思います。
ここらへんの設計を統一しておくと、作成するモジュールに統一感が生まれ開発がしやすくなります。
ただ、コンストラクタでは動的メモリの確保が必須になっているため、静的メモリに比べて処理速度が落ちます。
ここのデメリットは設計の効率とのトレードオフになりそうです。

静的なメモリをイニシャライザで初期化し、動的メモリの確保を使わなければこのデメリットは解決します。
しかし、イニシャライザ内で構造体の属性などに動的メモリの確保を行う場合は、メモリ解放のためにイニシャライザと対になる破棄関数(デストロイヤーなど)が必要になるでしょう。

静的メモリと動的メモリのちゃんぽんは設計とAPIが複雑になり、これはデメリットといえます。
私はPadというインタプリタの開発では、動的メモリの確保でAPIを統一してます。

腕に自信があるならやってもいいのよ

また、今回は構造体はパブリックなものになっています。
C言語でも構造体を隠蔽してカプセル化を行うことは可能ですが、これは今回は無視します。

animal.cは↓のような実装になります。

#include "animal.h"

Animal *
Animal_Init(Animal *self, int age, double weight) {
    // 構造体の属性を初期化する
    self->age = age;
    self->weight = weight;
}

Animal *
Animal_New(int age, double weight) {
    // 動的メモリの確保を行う
    Animal *self = calloc(1, sizeof(*self));
    if (!self) {
        return NULL;
    }

    // 確保したメモリをイニシャライザで初期化する
    if (!Animal_Init(self, age, weight)) {
        free(self);
        return NULL;
    }

    return self;
}

void
Animal_Del(Animal *self) {
    if (!self) {
        return;
    }

    // 確保したメモリを解放する
    free(self);
}

コンストラクタ内でイニシャライザを呼び出しています。
今回の継承の実装で注目してほしいのはこのイニシャライザの部分です。
Animalのイニシャライザは非常にシンプルで、構造体の属性を関数の引数で初期化しているだけです。
これは基本的にはCatでも一緒ですが、Catの場合はイニシャライザで親の構造体を初期化する処理が生まれます。

Catの実装

では今回のテーマの中核部分であるCatの実装を見てみます。
Catの場合も必要なファイルはcat.ccat.hです。

cat.hを見てみます。

#pragma once

#include <stdio.h>
#include <stdlib.h>

#include "animal.h"

/**
 * Animalを継承したCat構造体
 */
typedef struct {
    // 構造体の先頭で継承する構造体を書く
    Animal parent;

    // この属性はCat独自の属性
    int legs;
} Cat;

/**
 * イニシャライザ
 * 内部ではAnimalのイニシャライザを呼び出して構造体の初期化を行っている 
 */
Cat *
Cat_Init(Cat *self, int age, double weight, int legs);

/**
 * コンストラクタ
 * Catを動的メモリの確保で構築する
 * 内部ではイニシャライザを呼び出す
 */
Cat *
Cat_New(int age, double weight, int legs);

/**
 * デストラクタ
 * 確保されたCatのメモリを破棄する
 */
void
Cat_Del(Cat *self);

関数のプロトタイプ宣言などはAnimalと同じインターフェースです。
Animalと明確に違うのは構造体の定義です。
Cat構造体の先頭Animal構造体の変数parentを定義しています。
ここがCat構造体がAnimal構造体を継承している部分です。

「え?」っと思われるかもしれませんが、構造体の継承は非常にシンプルで、これだけです。
あとはこれにアップキャストを組み合わせて使います。
parent以降の属性(legs)はCat独自の属性です。

cat.cの実装を見てみます。

#include "cat.h"

Cat *
Cat_Init(Cat *self, int age, double weight, int legs) {
    // selfをAnimalにアップキャストしてAnimalのイニシャライザを呼び出す
    // Catの構造体の先頭ではAnimal構造体の変数が置かれているため、
    // このようにアップキャストすることでAnimal構造体のように扱うことが出来ます
    // ここが継承した構造体の初期化に関しての中核部分です
    if (!Animal_Init((Animal *) self, age, weight)) {
        return NULL;
    }

    // Cat独自の属性はここで初期化する
    self->legs = legs;

    return self;
}

Cat *
Cat_New(int age, double weight, int legs) {
    // 動的メモリの確保を行う
    Cat *self = calloc(1, sizeof(*self));
    if (!self) {
        return NULL;
    }

    // 確保したメモリをイニシャライザで初期化する
    if (!Cat_Init(self, age, weight, legs)) {
        free(self);
        return NULL;
    }

    return self;
}

void
Cat_Del(Cat *self) {
    if (!self) {
        return;
    }

    // 確保したメモリを解放する
    free(self);
}

コンストラクタやデストラクタの設計はAnimalと大差ありませんが、明確に違うのがイニシャライザの部分です。
selfCat型のポインタ変数ですが、これをAnimal構造体のポインタにアップキャストして、Animalのイニシャライザに渡しています。
このようにすることで、アップキャストされたポインタからCatの持つparentにアクセスすることができます。

これはあまり直感的ではなく、なんというかマジックみたいな感じですが、これが可能なのはparentCatの先頭で定義されているからです。
構造体内の属性の配置がアップキャスト後も変わらないため、Animal構造体のポインタのように扱うことができるわけです。

アップキャストのマジック!

ごいすー

このアップキャストの部分がC言語で継承を扱う場合の大事な部分と言えます。
キャストは奥が深いですね。

main関数の実装

ではこれらのオブジェクトを実際に使っているところ、main関数を見てみます。
これはmain.cのコードです。

#include <stdio.h>
#include "cat.h"

int main(void) {
    // Catをコンストラクトする
    Cat *cat = Cat_New(20, 32.1, 4);
    if (!cat) {
        return 1;
    }

    // Catの初期化された属性を見る
    printf("cat legs[%d]\n", cat->legs);

    // CatをAnimalにアップキャストすると↓のようにAnimalの属性にアクセスできる
    // これはCat構造体の先頭にAnimal構造体の変数があるため
    Animal *animal = (Animal *) cat;
    printf("cat age[%d] weight[%lf]\n", animal->age, animal->weight);

    // Catを破棄
    Cat_Del(cat);
    return 0;
}

今回はゲッターやセッターなどの関数は定義していません。
そのため構造体の属性に直接アクセスしています。
これまでのコードをコンパイルして実行すると↓のように出力されます。

cat legs[4]
cat age[20] weight[32.100000]

ちゃんと構造体の属性が初期化されていますね。

おわりに

今回はC言語による構造体の継承について見てみました。
C言語は奥が深く、オブジェクト指向も可能な言語です。
たとえばGUIライブラリのGtkなどもこのような継承を使った実装を行っています。
つまりC言語の継承は実用になるということですね。

そうだ、GUIライブラリを作ろう

ノリが軽いね