Edited at

C言語でオブジェクト指向を表現する (クラス、継承)


この文書の目的

C言語は言語文法としてオブジェクト指向設計のサポートはありませんが、オブジェクト指向設計ができないわけではありません。この記事では、ほぼすべての高級言語でサポートされているオブジェクト指向設計をC言語でも実現できる方法についてJava言語と対比して記載します。


クラス定義

クラスはオブジェクト指向の最も基本的な単位となります。C言語にはclassキーワードはありませんが、structによる構造体で表現することができます。


Javaの場合

以下のSampleクラスをC言語で表現していきます。


Sample.java

public class Sample

{
public void setNumber(int num){
number = num;
}

public int getNumber(){
return number;
}

private void clear(){
number = 0;
}

private int number = 0;
}


Main.java

public class Main

{
public static void main()
{
Sample sample1 = new Sample();
Sample sample2 = new Sample();

int num = sample1.getNumber();
sample2.setNumber(10);
}
}


C言語の場合

Javaではメンバ変数、メンバ関数をclass内に記載できますが、C言語の場合は、メンバ変数に相当するものだけを構造体内に定義します。カプセル化については、public/privateキーワードは持っていないため、命名規則などでルール化します。


sample.h

#infdef CLASS_SAMPLE_H

#define CLASS_SAMPLE_H

// クラス定義
typedef struct sample
{
// メンバ変数
// 言語レベルでは隠蔽できないため命名規則でカプセル化を実現
int privateNumber;

}Sample;

// public メンバ関数はヘッダに定義します

// 接頭辞としてクラス名をつけることでスコープを分けることができます

// 第一引数に必ず自身の構造体のポインタ型を定義してthis(インスタンスへのポインタ)として扱います
// メンバ関数の引数は第二引数以降に定義
void Sample_construct( Sample* const p_this );
void Sample_setNumber( Sample* const p_this, int num );
int Sample_getNumber( const Sample* const p_this );

#endif


メンバ関数の実体はcファイルに記載します。


sample.c

#include "Sample.h"


// private メンバ関数はstaticでスコープ隠蔽
static void sample_clear( Sample* const p_this );

// また、private関数の接頭辞は小文字で始めるなどのルールにより
// public/privateを分かりやすくすることができます
static void sample_clear( Sample* const p_this )
{
// 第一引数に自分自身のインスタンスへのポインタ(this)が格納されるように呼び出すので
// 第一引数のポインタを経由してメンバ変数へアクセスします
p_this->privateNumber = 0;
}

// コンストラクタは自動生成されないため必ず定義します
void Sample_construct(Sample* const p_this)
{
sample_clear(p_this);
}

void Sample_setNumber( Sample* const p_this, int num )
{
p_this->privateNumber = num;
}

int Sample_getNumber( const Sample* const p_this )
{
return p_this->privateNumber;
}


上記で定義したSampleクラスを使うアプリケーションコードは以下のようになります。


main.c

#include "Sample.h"


void main()
{
// インスタンス
Sample sample1;
Sample sample2;
int num;

// 関数の第一引数には必ずインスタンスへのポインタを格納
Sample_construct( &sample1 );
Sample_construct( &sample2 );

// インスタンスが異なっても関数は同じものを利用可能
num = Sample_getNumber( &sample1 );
Sample_setNumber( &sample2, 10 );
}



【参考】 private メンバを隠蔽する (Pimplパターン)

C++でも用いられるPimplパターンを適用すると、カプセル化をより強固にすることができます。


sample.h

struct sampleImpl; // プライベート変数を格納する構造体型を前方宣言

typedef struct sample
{
// 前方宣言していれば任意の構造体のポインタは内容定義を使用前まで遅延できるため
// sampleImplの詳細定義はヘッダ内で不要となります
struct sampleImpl *p_impl;

}Sample;



sample.c

#include "Sample.h"

#include <env_depend_headers.h> // sampleImpl内で利用する環境依存ヘッダをSample.hから排除できます

struct sampleImpl
{
int privateNumber;
EnvDependType hwdepend; // 環境依存型変数
};

void Sample_construct( Sample* const p_this )
{
struct sampleImpl *p;

// private領域を初期化時に割り当てる
// ヘッダに詳細定義を書く必要がなくなり、他のモジュールとより疎結合を保つことができます
// コンパイル時間短縮効果も期待できます。
// ただし、別途メモリ確保の処理が必要となるデメリットが発生します
p = (struct sampleImpl*)malloc( sizeof(struct sampleImpl) );
p_this->p_impl = p;

// 割り当て後に各変数を初期化
p_this->p_impl->privateNumber = 0;
p_this->p_impl->hwdepend = EnvDependInit();
}



継承

C言語でも継承を表現することができます。ここでは単一継承の例を示します。多重継承も表現は可能ですが、少し複雑な表現となります。多重継承自体、あまり推奨される設計ではないため、(継承とインターフェースで実現する方法が一般的なため、)Javaと同様に単一継承だけ紹介します。

以下のように、ChildClassがParentClassを継承している例で説明します。

[ParentClass] <-- extends -- [ChildClass]


Javaの場合

親クラスを定義します。


SampleParent.java

public class ParentClass

{
public int parentMember = 0;
}

子クラスを定義します。extendsキーワードで親クラスを継承します。


ChildClass.java

import ParentClass;

public class ChildClass extends ParentClass
{
public int childMember = 1;

void clearValue()
{
parentMember = 0;
childMember = 0;
}
}



C言語の場合

ParentClass.hに親クラスのParentClassのクラス定義を記述します。


ParentClass.h

typedef struct parentclass

{
int parentMember;

}ParentClass;

void ParentClass_construct( ParentClass* const p_this );


メンバ関数の定義はcファイルに記述します。


ParentClass.c

void ParentClass_construct( ParentClass* const p_this )

{
p_this->parentMember = 0;
}

ChildClass.hに子クラスであるChildClassを定義します。ChildClassはParentClassを継承するため、ParentClass.hヘッダをインクルードします。単一継承は、クラスのトップメンバに親クラスの変数を配置することで達成できます。


ChildClass.h

#include "ParentClass.h"


typedef struct childclass
{
// 構造体のトップメンバーに親クラス型の変数を置くことで継承が完了
ParentClass parent;

// 子クラス独自変数は親クラス型より下に定義
int childMember;
}ChildClass

void ChildClass_construct( ChildClass* const p_this );


ChildClassのメンバ関数定義をcファイルに記述します。以下のコード例でアップキャストの実現方法を示します。


ChildClass.c

#include "ChildClass.h"


void ChildClass_construct( ChildClass* const p_this )
{
// 継承しているため、ポインタをアップキャストして親クラスの関数を利用可能
ParentClass_construct( (ParentClass*)p_this );
p_this->childMember = 1;
}

void ChildClass_clearValue( ChildClass* const p_this )
{
// 継承しているため、ポインタをアップキャストして親クラスのメンバ変数にアクセス可能
((ParentClass*)p_this)->parentMember = 0;
p_this->childMember = 0;
}


ChildClass型のポインタ変数 p_thisにおいて、親クラスのオブジェクト定義を構造体の先頭に配置したことで、p_this == &(p_this->parent) が必ず成立するためアップキャストが可能となります。やっていることは以下と同等です。

ParentClass_construct( &(p_this->parent) );

p_this->parent.parentMember = 0;

継承が表現されているため、ParentClass用に定義したすべての関数にChildClass型の変数を安全に代入することができます。コーディングルールなどで任意型へのキャストが禁止されている場合は、void*を経由してキャストするなどで対応できます。

void* p_temp = p_this;

ParentClass_construct( (ParentClass*)p_temp );


インターフェース

オブジェクト指向で重要となるインターフェースについてもC言語で表現することが可能です。継承より少し複雑な表現となるため別記事にまとめました。

C言語でオブジェクト指向を表現する (インターフェース)