コンピューター:C言語講座:パッケージ化


 プログラム作成手順でもお話ししたように、プログラミングにおいては1度作ったモジュールをいかに再利用できる状態にして自分の手元に置いておくかが仕事をするうえでも趣味で作るうえでも重要で、逆に言えばどれだけそう言ったライブラリーを持っているかで完成までの工数は人によって2倍にも3倍にも変わって来るものです。
 ところで、プログラム作成手順で課題としてあげたようなシンプルな物なら何も気にせずにライブラリーとして再利用できますが、もう少し複雑な、ライブラリと言うよりはパッケージとでも呼びたくなるような規模の場合にはまた少し違った気くばりが必要です。
 とくに、パッケージ固有のデータを保持しておき、それに対する操作用の関数群を用意する場合などではデータの安全面・引数の明確化さらに、複数のデータの扱いなどが問題になります。また、パッケージ自体の汎用性も複雑になればなるほど重要になります。
 ここでは、ビットマップデータを扱うパッケージを課題として考えてみたいと思います。ビットマップデータをメモリー上で管理し、初期化・開放・取得・書換えなどを行ないます。ビットマップデータとはイメージデータの取扱に良く使われ、点をX-Y方向に並べた形式で、2次元配列が良く使われます。例えば、unsigned char map[100][100]では100x100の点からなるイメージを1つの点につき8bit分のデータを持つことができます。これを色情報に使えば256色の色を管理できます。左下から(10,10)の座標の点の情報はmap[10][10]の値です。なお、map[x][y]でも良いのですが、どちらかというとxで細かく回るアルゴリズムが多いのでmap[y][x]とします。こうすれば、xの隣接アクセスはジャンプの距離が短くて済み、高速になります。

グローバルデータを使う方法
 こういったデータを管理するプログラムでは最もわかりやすいデータの持ち方と言えばグローバルデータを利用することです。例えば以下のプログラムのような感じです。

unsigned char Map[100][100]; /* Map[y][x] */

void main()
{
    InitMap();
    LoadMapData();
    /* 編集・表示などをする */
    SaveMapData();
}

int InitMap()
{
int x,y;

    for(y=0;y<100;y++){
        for(x=0;x<100;x++){
            Map[y][x]=(unsigned char)0;
        }
    }

    return(0);
}

int LoadMapData()
{
int x,y;

    /* 例えば、ファイルからデータをセット */

    for(y=0;y<100;y++){
        for(x=0;x<100;x++){
            Map[y][x]=getc();
            if(feof(stdin)){
                return(-1);
            }
        }
    }

    return(0);
}

int SaveMapData()
{
int x,y;

    for(y=0;y<100;y++){
        for(x=0;x<100;x++){
            putc(Map[y][x]);
        }
    }

    return(0);
}

 全く面白くないプログラムですが、とりあえず、Mapという2次元配列をInitMap()で0に初期化して、LoadMapData()で標準入力から得た値でセットし、SaveMapData()で標準出力に書き出しています。Mapは関数外で宣言されていますので、グローバルデータとなり、どの関数からでも参照できます。

問題点
 さて、このプログラムの問題点は何でしょうか?プログラム上の問題ではなく、他への使い回しと言う面からの問題です。このビットマップを扱うプログラムを他のプログラムに持っていくには、Mapというグローバル変数を持っていき、InitMap(),LoadMapData(),SaveMapData()を持っていくことになります。問題としては、
    ・毎回持っていく度に切り出すのが面倒。
    ・Mapという変数名が既に使われていたらどうするか?
    ・サイズを指定したい。
    ・ビットマップを2つ以上同時に使いたい時にどうするか?
    ・ビットマップの1点あたりの情報量(今はunsigned char:8bit)を変えたい時にどうするか?
    ・ビットマップデータの安全性は?

ソースファイル分割
 このように再利用可能なパッケージとはとても呼べない状態です。解決方法を考えてみましょう。まず、切り出す作業が面倒なのは、とりあえず、ファイルを分けて置けば良さそうです。

main.c:

extern unsigned char Map[100][100]; /* Map[y][x] */

void main()
{
    InitMap();
    LoadMapData();
    /* 編集・表示などをする */
    SaveMapData();
}

map.c:

unsigned char Map[100][100]; /* Map[y][x] */

int InitMap()
{
    /* 同じ */
}

int LoadMapData()
{
    /* 同じ */
}

int SaveMapData()
{
    /* 同じ */
}

 このようにmain.cというファイルにそれぞれのプログラム固有の処理を記述し、map.cにパッケージとして使い回したいものを入れておき、変数Mapはmap.cに実体を置き、main.cではexternで外部参照する方法です。こうしておけば使い回す時にはmap.cだけ持っていけば良さそうです。

アドレス渡し
 Mapという変数名が既に使われているところに組み込むのはこの方法では大変です。map.cのMapを全て別の名前で置き換えるか、持っていく先のMapという変数名を全て置き換えないといけません。手っ取り早く対応するにはmap.cのMapをstatic宣言し、main.cでは直接外部参照せずにアドレスをもらう手もあります。

main.c:

unsigned char **MainMap;
unsigned char **GetMapAddr();

void main()
{
    InitMap();
    LoadMapData();
    MainMap=GetMapAddr();
    /* 編集・表示などをする */
    SaveMapData();
}

map.c:

static unsigned char Map[100][100]; /* Map[y][x] */

int InitMap()
{
    /* 同じ */
}

int LoadMapData()
{
    /* 同じ */
}

int SaveMapData()
{
    /* 同じ */
}

unsigned char **GetMapAddr()
{
    return(Map);
}

メモリー動的割り当て
 しかし、これでは同じデータをさす変数名が2つになってしまい、混乱してきます。さらに、サイズを指定したい場合はMapを配列ではなく、動的に割り当てれば良さそうです。その場合は忘れずに開放関数も準備します。

main.c:

unsigned char **MainMap;
unsigned char **GetMapAddr();

void main()
{
    InitMap(200,200);
    LoadMapData();
    MainMap=GetMapAddr();
    /* 編集・表示などをする */
    SaveMapData();
    FreeMap(200,200);
}

map.c:

static unsigned char **Map; /* Map[y][x] */

int InitMap(xsize,ysize)
int xsize,ysize;
{
int y;

    Map=(unsigned char **)malloc(ysize*sizeof(unsigned char *));
    for(y=0;y<ysize;y++){
        Map[y]=(unsigned char *)malloc(xsize*sizeof(unsigned char));
        /* 0で初期化 */
        memset(Map[y],0,xsize*sizeof(unsigned char));
    }

    return(0);
}

int FreeMap(xsize,ysize)
int xsize,ysize;
{
    for(y=0;y<ysize;y++){
        free(Map[y]);
    }
    free(Map);

return(0);
}

int LoadMapData()
{
    /* 同じ */
}

int SaveMapData()
{
    /* 同じ */
}

unsigned char **GetMapAddr()
{
    return(Map);
}

 これでサイズ変更にも対応できます。しかし、サイズは呼出側で覚えておかないと開放できません。

1点あたりの情報量変更
 1点あたりの情報量を変えるのは大変そうです。現在の8bitを16bitや32ビットに増やすのは型を変えれば簡単にできますが、たいていはメモリー削減の為ぎりぎりまで減らしたいもので、例えば、0,1,2,3の4つの値が持てれば良い場合には2bitで済むわけで、こうすればメモリー使用量は1/4*4=1/16に激減できます。160MByte必要なのが、10MByteで済むのですからこの違いは非常に大きいものです。
 どうすればそうできるでしょうか?C言語では1byteより小さい型はありません。従って、bit単位の操作が必要になります。例えば上記のように1点あたり2bitとすると、x方向のアクセスをする時に8/2で割って、残った分シフトして2bit分以外をマスクして取り出します。

/* x,yをアクセス */

    int xp,xoffset;

    xp=(x*2)/8;
    xoffset=x*2-xp*8;

    val=(Map[y][xp]>>xoffset)&0x03;

 セットする時はもっと面倒で、書き込みたい2bitをクリアーしてそこに足します。

/* x,yにflagをセット */

    int xp,xoffset;

    xp=(x*2)/8;
    xoffset=x*2-xp*8;

    Map[y][xp]=(((unsigned char)flag<<xoffset)&(0x03<<xoffset))+
        (Map[y][xp]&~(0x03<<xoffset));

 いかがですか?これをmain.c側で毎回記述しますか?しかも、1つのプログラム内でいろいろ違う時にはもう人間の手には負えなくなります。

構造体の使用
 さらに、複数のビットマップを同時に扱うのはどうしましょう?変数名の重複でアドレスを持ち回るようにしたので、いっそのことmap.cでデータを保持するのを止めて、関数呼出時に引数でアドレスを指定するようにすればできます。それであれば、このビットマップパッケージ用に構造体でも定義して、x,yのサイズなども一緒に持ち歩いたほうが良さそうです。さらに、点の値を書換え、取得するのもビット操作が大変なので関数にします。

typedef struct{
    unsigned char **map;
    int xsize,ysize;
    unsigned char bitl; /* 1点のビット長 */
    unsigned char bitf; /* bitlの全てのビットが1の値(マスク用) */
}MAP;

main.c:

void main()
{
MAP map1,map2;
unsigned char flag;

    InitMap(&map1,100,100,2);
    InitMap(&map2,50,50,4);

    LoadMapData(&map1);
    LoadMapData(&map2);

    /* 編集・表示などをする */
    flag=GetMapData(&map1,10,10);
    SetMapData(&map2,10,10,flag);

    SaveMapData(&map1);
    SaveMapData(&map2);

    FreeMap(&map1);
    FreeMap(&map2);
}

map.c:

int InitMap(map,xsize,ysize,bitl)
MAP *map;
int xsize,ysize,bitl;
{
int i,y;

    map->bitl=bitl;
    while(8%map->bitl!=0){
        map->bitl++;
    }

    map->bitf=0;
    for(i=0;i<bitl;i++){
        map->bitf+=(1<<i);
    }

    map->xsize=((xsize*map->bitl)>>3);
    if(xsize*map->bitl-map->xsize*8!=0){
        map->xsize++;
    }
    map->xsize++;
    map->ysize=ysize+1;

    map->map=(unsigned char **)malloc(map->ysize*sizeof(unsigned char *));
    for(y=0;y<map->ysize;y++){
        map->map[y]=(unsigned char *)malloc(map->xsize*sizeof(unsigned char));
        /* 0で初期化 */
        memset(map->map[y],0,map->xsize*sizeof(unsigned char));
    }

    return(0);
}

int FreeMap(map)
MAP *map;
{
    for(y=0;y<map->ysize;y++){
        free(map->map[y]);
    }
    free(map->map);

    return(0);
}

unsigned char GetMapData(map,x,y)
MAP *map;
int x,y;
{
int xp,xoffset;

    xp=(x*map->bitl)>>3;
    xoffset=x*map->bitl-xp*8;

    return((map->map[y][xp]>>xoffset)&map->bitf);
}

int SetMapData(map,x,y,flag)
MAP *map;
int x,y;
unsigned char flag;
{
int xp,xoffset;

    xp=(x*map->bitl)>>3;
    xoffset=x*map->bitl-xp*8;

    map->map[y][xp]=(((unsigned char)flag<<xoffset)&(map->bitf<<xoffset))+
        (map->map[y][xp]&~(map->bitf<<xoffset));

    return(0);
}

int LoadMapData(map)
MAP *map;
{
int x,y;

    /* 例えば、ファイルからデータをセット */

    for(y=0;y<100;y++){
        for(x=0;x<100;x++){
            SetMapData(map,x,y,(unsigned char)getc());
            if(feof(stdin)){
                return(-1);
            }
        }
    }

    return(0);
}

int SaveMapData(map)
MAP *map;
{
int x,y;

    for(y=0;y<100;y++){
        for(x=0;x<100;x++){
            putc(GetMapData(map,x,y));
        }
    }

    return(0);
}

 だいぶパッケージらしくなってきました。これならこのパッケージを組み込んで、複数のビットマップデータを個別に管理できそうです。実はこれくらいでも十分プログラミングで仕事を取れるレベルです。ビットマップ管理のプログラムを作れ、といわれてこのくらい気を効かしたものが作れれば私の仲間になってほしいくらいです。ただ、C++が流行しはじめてからのプログラミングとしてはやはりデータの安全性が気にかかります。要するに、MAP型のデータはプログラムのどこでもこのパッケージを使わなくてもアクセスできてしまうので良くわからないプログラマーが修正を頼まれた時などに、直接自分でmap->mapなどを書き換えたりすることも無いとは言えません。

データの安全化
 C++などのオブジェクト指向言語ではデータのアクセス件がクラスの中で指定でき、public以外はメンバー関数以外ではアクセスできないなどの保護機能が使用できますが、C言語はなんでも有りの言語なので、そういった保護機能はほとんど有りません。そこで、なんでも有りなのを利用して、保護機能を実現する方法を説明します。
 そもそもどこでもアクセスできる原因として、MAP型を公開しているのが問題なのです。main.cで考えると、MAP型が必要なのは宣言だけで、それ以外は全てパッケージの関数を呼び出して処理しています。そこで、MAP型をパッケージ外には公開しない方法を考えれば良いことになります。どうすればmain.cなど外部で宣言ができて、しかもMAP型を知らずにいられるのでしょうか?
 ここで、C言語のいい加減さというか、自由さを使うことになります。C言語の最も初心者に嫌われているアドレスとポインターがこの問題を簡単に解決してくれます。main.cなど外部ではMAP型の実体は持たず、map.cに確保してもらったMAP型の領域のアドレスを持つようにするのです。

main.c:

char *InitMap();

void main()
{
char *map1,*map2;
unsigned char flag;

    map1=InitMap(100,100,2);
    map2=InitMap(50,50,4);

    LoadMapData(map1);
    LoadMapData(map2);

    /* 編集・表示などをする */
    flag=GetMapData(map1,10,10);
    SetMapData(map2,10,10,flag);

    SaveMapData(map1);
    SaveMapData(map2);

    FreeMap(map1);
    FreeMap(map2);
}

map.c:

typedef struct{
    unsigned char **map;
    int xsize,ysize;
    unsigned char bitl; /* 1点のビット長 */
    unsigned char bitf; /* bitlの全てのビットが1の値(マスク用) */
}MAP;

char *InitMap(xsize,ysize,bitl)
int xsize,ysize,bitl;
{
MAP *map;
int i,y;

    map=(MAP *)malloc(1*sizeof(MAP));

    map->bitl=bitl;
    while(8%map->bitl!=0){
        map->bitl++;
    }

    map->bitf=0;
    for(i=0;i<bitl;i++){
        map->bitf+=(1<<i);
    }

    map->xsize=((xsize*map->bitl)>>3);
    if(xsize*map->bitl-map->xsize*8!=0){
        map->xsize++;
    }
    map->xsize++;
    map->ysize=ysize+1;

    map->map=(unsigned char **)malloc(map->ysize*sizeof(unsigned char *));
    for(y=0;y<map->ysize;y++){
        map->map[y]=(unsigned char *)malloc(map->xsize*sizeof(unsigned char));
        /* 0で初期化 */
        memset(map->map[y],0,map->xsize*sizeof(unsigned char));
    }

    return((char *)map);
}

int FreeMap(mapptr)
char *mapptr;
{
MAP *map=(MAP *)mapptr;

    for(y=0;y<map->ysize;y++){
        free(map->map[y]);
    }
    free(map->map);

    return(0);
}

unsigned char GetMapData(mapptr,x,y)
char *mapptr;
int x,y;
{
MAP *map=(MAP *)mapptr;
int xp,xoffset;

    xp=(x*map->bitl)>>3;
    xoffset=x*map->bitl-xp*8;

    return((map->map[y][xp]>>xoffset)&map->bitf);
}

int SetMapData(mapptr,x,y,flag)
char *mapptr;
int x,y;
unsigned char flag;
{
MAP *map=(MAP *)mapptr;
int xp,xoffset;

    xp=(x*map->bitl)>>3;
    xoffset=x*map->bitl-xp*8;

    map->map[y][xp]=(((unsigned char)flag<<xoffset)&(map->bitf<<xoffset))+
        (map->map[y][xp]&~(map->bitf<<xoffset));

    return(0);
}

int LoadMapData(mapptr)
char *mapptr;
{
MAP *map=(MAP *)mapptr;
int x,y;

    /* 例えば、ファイルからデータをセット */

    for(y=0;y<100;y++){
        for(x=0;x<100;x++){
            SetMapData(map,x,y,(unsigned char)getc());
            if(feof(stdin)){
                return(-1);
            }
        }
    }

    return(0);
}

int SaveMapData(mapptr)
char *mapptr;
{
MAP *map=(MAP *)mapptr;
int x,y;

    for(y=0;y<100;y++){
        for(x=0;x<100;x++){
            putc(GetMapData(map,x,y));
        }
    }

    return(0);
}

 上記のように、main.c側ではmapはchar *として、別に型はなんでも良いのですが、とにかくポインター型にしておきます。InitMap()でMAP型のデータをmallocで確保し、そのアドレスを返します。main.c側では以降、そのアドレスへのポインターを持ち歩いてパッケージ関数を使う時に渡すようになります。パッケージ関数側では受け取ったポインターをMAPポインターにキャストして使用します。こうすればmap.c以外ではMAP型の構造を知りませんから、当然直接メンバーにアクセスすることもできなくなります。なお、8で割るところはスピードをあげる為に>>3に置き換えています。

 このプログラムはこのままで使用できると思いますが、おそらく、複雑な処理でデータを非常に多くアクセスする場合に速度の問題が出ると思います。最も効果のある対処としては最下位ループをX方向でまわし、同じデータを連続してセットする場合用に関数を準備し、そこでは開始・終了近辺以外は8bit単位でダイレクトにセットするようにすることです。私も実際にこのパッケージに似たものでその方法で効果を確認しています。それ以外にもアルゴリズム特有のアクセス方法に応じた関数を準備すると良いでしょう。

まとめ
 いかがですか?ここまでしておけばこのパッケージをどんなアプリケーションと組み合わせても何の問題もおきないと思いませんか?実はこれでも関数名の重複という問題だけは起きてしまうのですが、これはクラスという概念の無いC言語では避けられない問題です。少なくともパッケージ内でしか使わない関数はstaticにしておくくらいの気遣いで勘弁してもらいましょう。

 実はこういったパッケージの作り方はMotifのライブラリーパッケージなどで良く使われていて、Widgetなどといった型も中身は使う側はわからないが、とにかくこれを渡せば処理してくれるという仕組みになっています。Xはもう少しオープンで、Display型なども見ようと思えば見れますが、Motifになると、Widgetなどは普通には見れません。Widgetを作る人にはもちろん見えますが。これなども段々に複雑になるにしたがって中を勝手にいじられないようにしてきているわけで、これにはデータの安全性の他にも意味があってパッケージを変更する時にも呼び出し側に影響を与えない為の配慮でもあります。アクセス関数を使っていれば中の構造が変わったりしても使う側はそのままで済みます。

 このように、簡単に作れるライブラリーでも誰にでも手放しで渡して正しく使ってもらう為の配慮を考えると結構大変なものです。しかし、せっかく苦労して作ったアルゴリズムや関数を別のシステムにも応用できるかどうかは、苦労する時の気分的にもだいぶ違ってきます。よく、前にこんな機能を作ったと思うから、その部分を切り出してくれ、と頼むと、他に使うことは考えていなかったから簡単に切り出せない、と言うプログラマーがいますが、これはこう言った気くばりができていない証拠で、言われなくても他に使えそうなルーチンはそのように作るようにするべきです。また、頼む時にもそういうように作ってほしいと頼むことが大切です。
 システム固有の型やグローバル変数名をサブルーチンにできそうな部分にまで持ち回るのはできるだけ考えてからにするべきです。そうしないと、せっかく作った素晴らしい動作をするルーチンでも他の仲間からいやがられる物になってしまいます。もちろん自分でも使い回しが大変です。
 C言語は同じ動きをするものでもプログラマーによって、応用性があるように組まれたものと、そうでないものの差が非常に大きくでます。皆さんも是非、プログラムを作る時には他の人にも喜んで使ってもらえるような部品を作るように気を配ってみてください。何の修正も無しに他人のプログラムに組み込んでもらえるようになれば一人前です。


よろしければブログもご覧ください
ゴルフ練習場紹介サイト:ゴルフ練習場行脚録更新中!
ipv400004460 from 1998/3/4