LoginSignup
23
20

More than 3 years have passed since last update.

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

Posted at

この記事の目的

こちらの記事で、C言語においてクラス、継承を表現する方法を記載しました。この記事では、C言語でオブジェクト指向における「インターフェース」を表現する方法についてJava言語と対比して記載します。

インターフェースの表現方法

C言語はオブジェクト指向のサポートはありませんが、オブジェクト指向に基づいた設計は可能です。インターフェースの目的は、オブジェクト指向設計における動的なポリモーフィズムの実現となりますが、これはC言語の関数ポインタの機能を使うことで達成できます。この関数ポインタを利用してインターフェース機構をC言語で実現します。

Javaの場合

Javaでは言語レベルでinterfaceキーワードをサポートしているため、簡単にインターフェースを定義できます。例として、ファイルのopen、closeを想定したインターフェースを定義します。open()ではファイル名を引数に取ります。

InterfaceFile.java

InterfaceFile.java
public interface InterfaceFile{
    void open(String filename);
    void close();
}

File.java

インターフェースを実装した具象クラスです。(この例では、ファイル名を表示するのみ記述していますが、実際は具象クラスがターゲットとするOS固有のサービスコール等でファイル入出力の処理を記述するようなイメージとなります。)

File.java
public class File implements InterfaceFile
{
    @Override
    public void open(String filename){
        m_filename = filename;
        System.out.println("open file!:" + m_filename);
    }

    @Override
    public void close(){
        System.out.println("close file!:" + m_filename);
    }

    public File(){
        m_filename = "";
    }

    String m_filename;
}

Main.java

Main.java
public class Main {
    public static void main(String[] args) throws Exception {
        File f = new File();
        file_processing(f, "sample.txt");
    }

    // この処理はインターフェースに対して行っています
    private static void file_processing( InterfaceFile ifile, String filename ){
        ifile.open(filename);

        // 何らかの処理・・・

        ifile.close();
    }
}

C言語の場合

C言語はinterfaceキーワードをサポートしていませんが、クラス表現と同様にstructでインターフェースを表現することができます。

インターフェースを表現するために、インターフェース本体を表す構造体定義と、インターフェースが提供する関数(メソッド)のテーブルを表す構造体定義の2つのstructを利用します。

InterfaceFile.h

インターフェースの定義は使用者へ公開するためヘッダに記述します。インターフェースを利用する場合は、このヘッダをインクルードします。

InterfaceFile.h
#ifndef ___INTERFACE_FILE_H___
#define ___INTERFACE_FILE_H___

// インターフェースが提供する関数のテーブルを保持する型を定義します
// (前方宣言です)
struct interfacefilemethods;

// インターフェース本体です
//
// このインターフェースが提供する関数を定義した
// 関数テーブルを保持する型のポインタのみを格納します。
typedef struct interfacefile
{
    const struct interfacefilemethods *p_methods;
}InterfaceFile;


// インターフェースが提供する関数(メソッド)のテーブルの定義です。
//
// それぞれの関数の第一引数はインターフェース本体のポインタ型に統一します
// このポインタには使用者側で具象クラスのインスタンスのポインタを代入するようにルール化します
typedef struct interfacefilemethods{
    // 関数独自の引数は第二引数以降に定義します
    void (*open)(InterfaceFile* const, const char*);
    void (*close)(InterfaceFile* const);
}InterfaceFileMethods;

#endif

File.h

インターフェースを実装する具象クラスを定義するヘッダです。インターフェースの実装は、継承の表現と同様に、具象クラスを表す構造体の先頭にインターフェース型の変数を配置することで達成できます。

File.h
#ifndef  ___FILE_H___
#define  ___FILE_H___

#include "InterfaceFile.h"

typedef struct file
{
    // インターフェースを実装(implements)するために
    // 継承と同様にインターフェース型の変数をクラスの先頭に定義します

    // 継承と同じ表現とすることでインターフェース型のポインタ変数に
    // 安全にキャストして代入することができます
    InterfaceFile interface;

    // このクラス独自のメンバ変数は
    // インターフェースの下に定義します
    const char* private_filename;
}File;

// 通常のクラスと同様に公開関数を定義します
// (インターフェースで定義される関数は除きます)
void File_construct( File* const p_this );

#endif

File.c

具象クラスの処理を記述するCファイルです。このファイルの中でインターフェースで定義される関数の実装本体を記述します。また、その関数にアクセスするための関数テーブル本体も定義します。

File.c
#include <stdio.h>
#include "File.h"

// Fileクラスのopen()メソッドのインターフェース実装本体です
static void file_open(InterfaceFile* const p_this, const char* filename)
{
    // 第一引数は必ずインターフェースを実装したインスタンスへの
    // ポインタが代入されるように呼び出し元でルール化します
    File* const p = (File* const)p_this;

    // メンバ変数へもアクセスできます
    p->private_filename = filename;
    printf("file open!: %s\n", p->private_filename);
}

// Fileクラスのclose()メソッドのインターフェース実装本体です
//
// これらのインターフェース実装本体は直接コールせず
// 関数ポインタを格納した関数テーブルを経由してコールします
// (そのため、スコープをファイル内に制限しています)
static void file_close(InterfaceFile* const p_this)
{
    File* const p = (File* const)p_this;
    printf("file close! :%s\n", p->private_filename);
}

// Fileクラスのインターフェース実装へアクセスするための関数テーブルです
// (実行中に書き換えないため、定数で定義します)
static const InterfaceFileMethods FILE_METHODS = {
    file_open,
    file_close
};

// コンストラクタ
void File_construct( File* const p_this)
{
    // インターフェースを表現している構造体が格納する
    // 関数テーブルのポインタを初期化します。
    //
    // これにより、インターフェース定義からインターフェースの実装本体への
    // アクセスが設定されます
    ((InterfaceFile*)p_this)->p_methods = &FILE_METHODS;

    // メンバ変数も初期化します
    p_this->private_filename = NULL;
}

main.c

インターフェースの使用例です。

main.c
#include <stdio.h>

#include "InterfaceFile.h"
#include "File.h"

// プロトタイプ宣言です
// この関数はインターフェース型に対して実装します
static void file_processing(InterfaceFile* const p_file, const char* name);

int main(void){
    // インターフェースを実装したFileクラスのインスタンスです
    File f;

    const char filename[] = "sample.txt";

    // コンストラクタは手動で呼ぶ必要があるためコールします
    //
    // この中でFileクラス向けに定義した関数テーブルが
    // インスタンスfにセットされることになります
    File_construct(&f);

    // FileクラスはInterfaceFileインターフェースを実装しているため
    // 安全にInterfaceFile型のポインタにキャストできます
    file_processing( (InterfaceFile*)&f, filename );

    return 0;
}


// インターフェースにより、具象クラスを意識することなく
// 関数の処理を実装することができます
static void file_processing(InterfaceFile* const p_file, const char* filename)
{   
    // 各メソッドの第一引数には必ずインターフェース自身のポインタを代入します
    //
    // これにより、各メソッドの実装側で第一引数を経由して
    // インスタンス自身へアクセスすることができます
    p_file->p_methods->open( p_file, filename );

    // 何らかの処理・・・

    p_file->p_methods->close( p_file );
}

参考

関数テーブルを用いない表現の場合

上記の例は、インターフェースが提供する関数群を関数テーブルに格納し、関数テーブルを経由して各関数へアクセスを実現しています。そのため、インターフェースからポインタを二回経由してメソッド呼び出すという若干複雑な表現になっています。

インターフェースの定義の際に、関数テーブルを介さずに、シンプルに以下のように表現することも考えられます。

// インターフェース型の構造体内に直接関数を定義しています
typedef struct interfacefile{
    void (*open)(struct interfacefile *const, const char*);
    void (*close)(struct interfacefile *const);
}InterfaceFile;


// 先の例と同様に先頭に配置してインターフェースを実装します
typedef struct file
{
    InterfaceFile interface;
    const char* private_filename;
}File;

このように定義すると、インターフェースの使用側では以下のように呼び出すことができます。


static void file_processing(InterfaceFile* const p_file, const char* filename)
{   
    // インターフェースから直接メソッドを呼び出す表現ができます
    // 関数テーブルを経由しない分、多少のオーバーヘッド削減も期待はできます
    p_file->open( p_file, filename );
    p_file->close( p_file );
}

関数テーブルを用いない場合の問題

こちらの方法では表現をシンプルにすることができますが、問題が一つあります。インターフェースに直接関数ポインタ群を定義することで、インターフェース型の構造体のサイズが、定義する関数の数の分だけ増えることになります。そのため、インターフェースを実装した具象クラスのサイズも増加してしまうことになります。

具象クラスにおいて、インターフェースで定義した関数ポインタ群にセットされるアドレスは、そのクラスの全てのインスタンスで共通のため、この方法は冗長な情報が各インスタンスに格納されてしまう表現方法となっています。

インターフェースとなる関数の数や、具象クラスのインスタンスの数が数個の場合は問題にはならないかもしれませんが、これらの数が多くなれば、メモリの使用効率が犠牲になることになります。

C言語を用いた開発は、マイコンなどのメモリ制約の厳しい組み込み系で使われる事も多いと考えられるため、この方法では適用が困難になってしまう可能性があります。

関数テーブルを用いるメリット

先の関数テーブルを介した表現方法では、若干、表現が複雑にはなりますが、インターフェースが提供する関数の個数にかかわらずインターフェース本体のサイズが一定(ポインタ型のサイズ)となるため、この問題を回避することができます。

また、関数テーブルの定義自体を定数化することで、関数テーブルをROMへ配置することも可能となり、RAMを節約することも可能となります。

C++の仮想関数(virtual)の仕組みを実現する機構でも、暗黙的にこのような関数テーブル(仮想関数テーブル:vtable/vptr)を生成して実現しているコンパイラの実装もあります。

23
20
0

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
23
20