注意
- 原理的にCではオブジェクト指向プログラミングはできない。ここで述べるのはオブジェクト指向 チック であることに注意。C言語を使わざるを得ない環境で使うと良い。(例:コンパイラがCしかない、ランタイムが貧弱)
- あくまで例であり、すべての問題はこのスタイルでは解決できない。プログラミングする対象に合わせて、柔軟にスタイルを切り替えるべし。
- この例をコピペして使わないこと。
例1:Fooモジュールの実装
例として、Foo
というモジュールの実装を示す。
ヘッダファイル foo.h
/* ヘッダの多重インクルード防止(ヘッダインクルードガード) */
#ifndef FOO_H_INCLUDED
#define FOO_H_INCLUDED
/* このモジュールが使用する型定義を含むヘッダ(C99) */
#include <stdint.h>
/* 構造体定義は隠して宣言のみを行う(前方宣言) */
typedef struct FooTag *FooHn;
/* インスタンスを作成するためのコンフィグ構造体 */
typedef struct FooConfigTag {
int32_t num_parameters; /* パラメータ数 */
} FooConfig;
/* APIの成否を示す型 */
typedef enum FooApiResultTag {
FOO_APIRESULT_OK = 0, /* 成功 */
FOO_APIRESULT_NG, /* 分類不能な失敗 */
FOO_APIRESULT_INVALID_ARGUMENT /* 引数が不正 */
} FooApiResult;
/* Cリンケージで関数宣言(補遺にて解説) */
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
/* Fooモジュールの初期化 */
/* 必要に応じてリソースの初期化などを行う */
void Foo_Initialize(void);
/* Fooモジュールの終了 */
/* Initializeで初期化した内容を片付ける */
void Foo_Finalize(void);
/* Fooインスタンス作成に必要なワークサイズの計算 */
/* 失敗した場合は負値を返す */
int32_t Foo_CalculateWorkSize(const FooConfig* config);
/* Fooインスタンスの作成 */
/* 失敗した場合はNULLを返す */
FooHn Foo_Create(const FooConfig* config, void* work, int32_t work_size);
/* パラメータ数の取得 */
FooApiResult Foo_GetNumParameters(FooHn foo, int32_t* num_parameters);
/* Fooインスタンスの破棄 */
void Foo_Destroy(FooHn foo);
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* FOO_H_INCLUDED */
Foo
というモジュールがあった場合、そのクラスインスタンスを扱う実体(ハンドル)は FooHn
、関数は Foo_HogePiyo
という命名規則をつける(命名規則は各プロダクトのコーディングスタイルによって揺らぐので、この規則は絶対ではない)。
Foo_Create
によってクラスインスタンスが作られる(クラスのコンストラクタに該当)。Foo_Create
の前にFoo_CalculateWorkSize
によって必要なワークサイズ(メモリ領域サイズ)を計算する必要があることに注意。Foo_Create
ではwork
に任意のアドレスを指定する事ができる(C++の配置newに該当)。- インスタンスに対して何かを実行する際には、
Foo_GetNumParameters
の様に、必ずハンドルを介して行う(Pythonのself
等に該当)。 - インスタンスを破棄するときには
Foo_Destroy
を使用する(クラスのデストラクタに該当)。
実装ファイル foo.c
#include "foo.h"
#include <string.h>
/* このモジュールが保証するアラインメント */
/* アラインメントについては別途説明する */
#define FOO_ALIGNMENT 16
/* アドレスをアラインメント境界に揃えるマクロ */
#define FOO_ALIGN_NBYTE(ptr, alignment) ((((ptr) + ((alignment) - 1)) / (alignment)) * (alignment))
/* メモリの無効値を示す値 */
#define FOO_INVALID_BYTE_PATTERN 0xCD
/* Fooオブジェクトの構造体定義 */
struct FooTag {
float* parameters; /* パラメータ領域 */
int32_t num_parameters; /* パラメータ数 */
void* work; /* ワーク領域先頭アドレス */
int32_t work_size; /* ワークサイズ */
};
/* ローカル変数宣言 */
/* モジュール初期化フラグ */
static int32_t st_foo_is_initialized = 0;
/* Fooモジュールの初期化 */
void Foo_Initialize(void)
{
/* 多重初期化防止 */
if (st_foo_is_initialized != 0) {
return;
}
/* 必要に応じてリソースの初期化・確保 */
/* 初期化フラグを立てる */
st_foo_is_initialized = 1;
}
/* Fooモジュールの終了 */
void Foo_Finalize(void)
{
/* 初期化前では実行不可能 */
if (st_foo_is_initialized == 0) {
return;
}
/* 必要に応じてリソースの破棄 */
/* 初期化フラグをクリア */
st_foo_is_initialized = 0;
}
/* Fooインスタンス作成に必要なワークサイズの計算 */
int32_t Foo_CalculateWorkSize(const FooConfig* config)
{
int32_t work_size;
/* 引数チェック */
if (config == NULL) {
return -1;
}
/* コンフィグチェック */
if (config->num_parameters < 0) {
return -1;
}
/* 構造体分のサイズ */
work_size = sizeof(struct FooTag);
/* パラメータ領域分のサイズ */
work_size = sizeof(float) * config->num_parameters;
/* アラインメント分加算 */
work_size += FOO_ALIGNMENT;
return work_size;
}
/* Fooインスタンスの作成 */
FooHn Foo_Create(const FooConfig* config, void* work, int32_t work_size)
{
FooHn foo;
uint8_t* work_ptr;
/* 引数チェック */
if ((config == NULL) || (work == NULL) || (work_size < 0)) {
return NULL;
}
/* コンフィグチェック */
if (config->num_parameters < 0) {
return NULL;
}
/* 未初期化メモリ領域のアクセス防止(メモリを無効値で埋める) */
memset(work, FOO_INVALID_BYTE_PATTERN, work_size);
/* アラインメント境界に揃える */
work_ptr = (uint8_t *)FOO_ALIGN_NBYTE((uintptr_t)work, FOO_ALIGNMENT);
/* 構造体の配置 */
foo = (FooHn)work_ptr;
work_ptr += sizeof(struct FooTag);
/* メンバに値を設定 */
foo->num_parameters = config->num_parameters;
/* パラメータ領域の配置 */
foo->parameters = (float *)work_ptr;
work_ptr += sizeof(float) * config->num_parameters;
/* バッファオーバーラン検知 */
if ((work_ptr - (uint8_t*)work) > work_size) {
return NULL;
}
/* 領域先頭アドレスとワークサイズを記録 */
foo->work = work;
foo->work_size = work_size;
return foo;
}
/* 何らかの処理の実行 例: パラメータ数の取得 */
FooApiResult Foo_GetNumParameters(FooHn foo, int32_t* num_parameters)
{
/* 引数チェック */
if ((foo == NULL) || (num_parameters == NULL)) {
return FOO_APIRESULT_INVALID_ARGUMENT;
}
/* 結果をセット */
(*num_parameters) = foo->num_parameters;
/* 成功終了 */
return FOO_APIRESULT_OK;
}
/* Fooインスタンスの破棄 */
void Foo_Destroy(FooHn foo)
{
/* メモリが無効な場合は何もしない */
if (foo == NULL) {
return;
}
/* メモリを無効値で埋める */
memset(foo->work, FOO_INVALID_BYTE_PATTERN, foo->work_size);
}
メインエントリ main.c
ライブラリ使用者たるユーザが書くコードは次のようになる。
/* 依存ヘッダインクルード */
#include "foo.h"
#include <stdlib.h>
#include <stdio.h>
/* メインエントリ */
int main(void)
{
FooHn foo;
int32_t work_size;
FooConfig config;
void* work;
/* コンフィグの設定 */
config.num_parameters = 10;
/* インスタンス生成に必要なワークサイズ計算 */
work_size = Foo_CalculateWorkSize(&config);
if (work_size < 0) {
return 1;
}
/* メモリ領域確保 */
work = malloc(work_size);
if (work == NULL) {
return 1;
}
/* インスタンス生成 */
foo = Foo_Create(&config, work, work_size);
if (foo == NULL) {
/* 確保済み領域は開放 */
free(work);
return 1;
}
/* やりたいことをする */
{
int32_t num_params;
if (Foo_GetNumParameters(foo, &num_params) == FOO_APIRESULT_OK) {
printf("Number of parameters: %d \n", num_params);
}
}
/* インスタンス破棄 */
Foo_Destroy(foo);
free(work);
return 0;
}
例2:インターフェース
多相性(ポリモーフィズム)を実現する例を示す。多相性とは、関数呼び出しは同じだけども振る舞いが変わるようなことを指す。例えば、犬とか猫は共通の「鳴く」という動作(関数)を持つが、実際に実行すると全く違った鳴き声が得られるといったことを指す。
うまくインターフェースが定義できると、モジュール間の情報のやり取りが非常に楽になる。例えば、画像に対してフィルタを適用することを考えてみる。インターフェースを使う側から考えると、フィルタに画像入力を与えて処理済みの出力さえ得られればよいので、その中身の実装を気にする必要がなくなる。もし出力がおかしければ、フィルタ側の不具合を疑って修正していけばよいのでデバッグ効率も高まる。
インターフェースの定義ヘッダ animal.h
C言語でインターフェースを実現するには、 関数ポインタの構造体 を使うのが普通である。インターフェースの定義をまとめたヘッダを以下に示す。
#ifndef ANIMAL_H_INCLUDED
#define ANIMAL_H_INCLUDED
#include <stdint.h>
/* インスタンス生成用のコンフィグ */
typedef struct AnimalConfig {
int32_t age;
} AnimalConfig;
/* インターフェース定義 */
typedef struct AnimalInterfaceTag {
/* ワークサイズ計算 */
int32_t(*CalculateWorkSize)(const AnimalConfig* config);
/* インスタンス生成 */
void* (*Create)(const AnimalConfig* config, void* work, int32_t work_size);
/* 鳴かせる */
void (*MakeSound)(void* handle);
/* インスタンス破棄 */
void (*Destroy)(void* handle);
} AnimalInterface;
#endif /* ANIMAL_H_INCLUDED */
インターフェースの実装例1(Piyo)
Piyoのヘッダ piyo.h
インターフェースを実装したPiyoは、インターフェースの取得関数 Piyo_GetInterface
のみを公開する。こうすることで使用者側に実装の内容を隠蔽でき、インターフェースのみを介して操作するようにできる。
#ifndef PIYO_H_INCLUDED
#define PIYO_H_INCLUDED
#include "animal.h"
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
/* Piyoインターフェースの取得 */
const AnimalInterface* Piyo_GetInterface(void);
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* PIYO_H_INCLUDED */
Piyoの実装 piyo.c
インターフェースの実装はソースファイル(.c)に行う。
#include "piyo.h"
#include <stdlib.h>
#include <stdio.h>
/* Piyoオブジェクト定義 */
typedef struct PiyoTag {
int32_t age;
} *PiyoHn;
/* ワークサイズ計算 */
static int32_t Piyo_CalculateWorkSize(const AnimalConfig* config);
/* Piyoインスタンス生成 */
static void* Piyo_Create(const AnimalConfig* config, void* work, int32_t work_size);
/* Piyoを鳴かせる */
static void Piyo_MakeSound(void* handle);
/* Piyoインスタンス破棄 */
static void Piyo_Destroy(void* handle);
/* Piyoインターフェース定義 */
static const AnimalInterface st_piyo_interface = {
Piyo_CalculateWorkSize,
Piyo_Create,
Piyo_MakeSound,
Piyo_Destroy
};
/* Piyoインターフェースの取得 */
const AnimalInterface* Piyo_GetInterface(void)
{
return &st_piyo_interface;
}
/* ワークサイズ計算 */
static int32_t Piyo_CalculateWorkSize(const AnimalConfig* config)
{
/* 引数チェック */
if (config == NULL) {
return -1;
}
/* 自分でメモリ領域を確保するため要求サイズは0 */
return 0;
}
/* Piyoインスタンス生成 */
static void* Piyo_Create(const AnimalConfig* config, void* work, int32_t work_size)
{
PiyoHn piyo;
/* 引数チェック */
if (config == NULL) {
return NULL;
}
/* Piyoハンドルの作成 */
piyo = (PiyoHn)malloc(sizeof(struct PiyoTag));
/* コンフィグ値をセット */
piyo->age = config->age;
return piyo;
}
/* Piyoを鳴かせる */
static void Piyo_MakeSound(void* handle)
{
PiyoHn piyo;
/* 引数チェック */
if (handle == NULL) {
return;
}
piyo = (PiyoHn)handle;
/* 鳴く */
printf("I'm Piyo. Age: %d \n", piyo->age);
}
/* Piyoインスタンス破棄 */
static void Piyo_Destroy(void* handle)
{
if (handle != NULL) {
free(handle);
}
}
インターフェースの実装例2(Fuga)
同一インターフェースだが、Piyoと異なる振る舞いを示すモジュールFugaのヘッダと実装を示す。
#ifndef FUGA_H_INCLUDED
#define FUGA_H_INCLUDED
#include "animal.h"
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
/* Fugaインターフェースの取得 */
const AnimalInterface* Fuga_GetInterface(void);
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* FUGA_H_INCLUDED */
#include "fuga.h"
#include <stdlib.h>
#include <stdio.h>
/* Fugaオブジェクト定義 */
typedef struct FugaTag {
int32_t age;
} *FugaHn;
/* ワークサイズ計算 */
static int32_t Fuga_CalculateWorkSize(const AnimalConfig* config);
/* Fugaインスタンス生成 */
static void* Fuga_Create(const AnimalConfig* config, void* work, int32_t work_size);
/* Fugaを鳴かせる */
static void Fuga_MakeSound(void* handle);
/* Fugaインスタンス破棄 */
static void Fuga_Destroy(void* handle);
/* Fugaインターフェース定義 */
static const AnimalInterface st_fuga_interface = {
Fuga_CalculateWorkSize,
Fuga_Create,
Fuga_MakeSound,
Fuga_Destroy
};
/* Fugaインターフェースの取得 */
const AnimalInterface* Fuga_GetInterface(void)
{
return &st_fuga_interface;
}
/* ワークサイズ計算 */
static int32_t Fuga_CalculateWorkSize(const AnimalConfig* config)
{
/* 引数チェック */
if (config == NULL) {
return -1;
}
/* 自分でメモリ領域を確保するため要求サイズは0 */
return 0;
}
/* Fugaインスタンス生成 */
static void* Fuga_Create(const AnimalConfig* config, void* work, int32_t work_size)
{
FugaHn fuga;
/* 引数チェック */
if (config == NULL) {
return NULL;
}
/* Fugaハンドルの作成 */
fuga = (FugaHn)malloc(sizeof(struct FugaTag));
/* コンフィグ値をセット */
fuga->age = config->age;
return fuga;
}
/* Fugaを鳴かせる */
static void Fuga_MakeSound(void* handle)
{
FugaHn fuga;
/* 引数チェック */
if (handle == NULL) {
return;
}
fuga = (FugaHn)handle;
/* 鳴く */
printf("I'm Fuga. Age: %d \n", fuga->age);
}
/* Fugaインスタンス破棄 */
static void Fuga_Destroy(void* handle)
{
if (handle != NULL) {
free(handle);
}
}
メインエントリ main.c
PiyoとFugaを使用するソースコード例は以下になる。インターフェースを利用する側としては、内部の実装を知らずにインスタンスの操作を行うことができる。
/* 依存ヘッダインクルード */
#include "piyo.h"
#include "fuga.h"
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
/* メインエントリ */
int main(void)
{
void *animal, *animal_work;
int32_t work_size, i;
AnimalConfig config;
const AnimalInterface *interface_list[2];
/* 処理対象のインターフェースを取得 */
interface_list[0] = Piyo_GetInterface();
interface_list[1] = Fuga_GetInterface();
/* リスト内のインターフェースに対して共通の処理 */
for (i = 0; i < 2; i++) {
const AnimalInterface* animal_if = interface_list[i];
/* コンフィグの設定 */
config.age = 10;
/* インスタンス生成に必要なワークサイズ計算 */
work_size = animal_if->CalculateWorkSize(&config);
if (work_size < 0) {
return 1;
}
/* メモリ領域確保 */
animal_work = malloc(work_size);
if (animal_work == NULL) {
return 1;
}
/* インスタンス生成 */
animal = animal_if->Create(&config, animal_work, work_size);
if (animal == NULL) {
/* 確保済み領域は開放 */
free(animal_work);
return 1;
}
/* 鳴かせる */
animal_if->MakeSound(animal);
/* インスタンス破棄 */
animal_if->Destroy(animal);
free(animal_work);
}
return 0;
}
補遺
C言語で書かれたライブラリの提供形態
C言語で開発したライブラリを商業に展開するときは、以下の2つをユーザに提供することが多い。
- ヘッダファイル(.h)
-
ライブラリを使用するために必要な型、構造体、列挙型、関数宣言等が含まれる。
-
ライブラリファイル(.a, .so, .lib)
- 多数のソースファイル(.c, .h)をコンパイルしてアーカイブした(取りまとめた)もの。
例ではソースファイル(.c)を載せているが、関数の実装は隠蔽するためにソースファイルは原則公開しない。
extern "C" について
Cの関数(変数)をヘッダで宣言するときは、以下のようにextern "C"
で括った方が良い。
#ifndef BAR_H_INCLUDED
#define BAR_H_INCLUDED
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
void bar(void);
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* BAR_H_INCLUDED */
単一の関数(変数)ならば、以下のように書いても良い。
extern "C" void bar(void);
これはCとC++との相互運用の際に必要になってくる。C++の言語仕様には名前空間があり、関数(変数)は名前空間に応じた名前(シンボル)を持って(マングルされて)オブジェクトファイル(.obj, .o)に出力される。一方、Cの関数は(static
でない限り)全てグローバルなシンボルを持つ。
何も対策を行わない場合、C++コンパイラとCコンパイラで異なる名前(シンボル)を持つようになってしまい、相互に参照できなくなりリンクエラーが生じてしまう。そこで、extern "C"
が必要になってくる。extern "C"
で括られた関数はC言語と同様の宣言を行うことを強制するため、上記の問題を回避できる。
アラインメント
メモリに関して気にしたければ読んでください。 CPUはある特定のバイト境界に合わせてメモリをアクセスしている。この境界を(メモリ)アラインメントという。例えば、16バイトアラインメントでアクセスする場合、変数や配列の先頭アドレスは必ず16の倍数に配置されていなければならない。配置されていない場合はバスエラーが発生する。(実行時に補正を行うことでエラーが起きない場合もあるが、補正によるオーバーヘッドがかかる)
上記のプログラム例では、メモリ上に直接構造体の内容を書き込んでいるため、アラインメントを意識しなければならない。
使用するCPUによってアラインメントは異なり、もっと言うと特定の機械語命令がアラインメントを要求するときがある。例えば、IntelのSSE命令(1命令で複数数値の数値演算を行う命令)では、命令に渡すアドレスに16バイトアラインメントを要求する。
マルチスレッド
上記の例ではマルチスレッド環境のことは全く考えずに実装している。排他はユーザ側が行う必要がある。
実装者がマルチスレッドセーフにモジュールを作るのは難しい。上記の例でも、例えば、Foo_Destroy
が呼ばれた直後に別のスレッドから Foo_GetNumParameters
が呼ばれるかもしれない。そうなるとどうなるか分からない。すぐクラッシュすれば運が良いが、ひどい場合は何も起こらずに、開放したメモリ領域を再利用した他のモジュールのメモリ領域を破壊して実行し続けるかもしれない。いずれにせよ、追跡の難しい不具合を生むことになる。
マルチスレッドセーフに作る仕組みとしては例えばロックやセマフォがある。しかし、次はデットロック(ライブロック)、排他待ちによる性能悪化に対応する必要がある。
一般にC言語でマルチスレッド実装を安全に実現するのは難しい。
コンフィグのメンバにポインタを使う時
文字列を始め、配列をコンフィグに渡すならばポインタを使うことになる。
そのポインタが指す内容の生存期間を仕様として明らかにし、実装者とユーザ両方で約束として守らせる。 - インスタンスにアドレスだけコピーされるのか? - ポインタが指す内容ごとコピーされるのか?
さもなくばメモリの不定領域にアクセスして奇妙な振る舞いを示すようになる。設計の際に注意する。
内部モジュールとして使う時
ユーザに公開することのない内部モジュールで実装するならば、プログラミングミスを防いだり、デバッグ効率を高めるために、チェックを厳しくしたほうがよい。具体的には assert
(C標準の assert.h
にマクロ定義がある) を使う。
/* Fooモジュールの初期化 */
void Foo_Initialize(void)
{
/* 多重初期化防止 */
assert(st_foo_is_initialized != 0);
...
補足:assert
は引数の条件が成立しないときにプログラムを強制停止(アボート)させる。その際、(ランタイムにより多少の差はあるが)条件チェックに失敗した行などの診断メッセージを出せる。(補足の補足:デバッグ向けにコンパイルしていないと assert
が有効にならない場合がある。gcc
では-DDEBUG
フラグを指定する必要がある。)