▲68通信vol.1のページへ


偏見に満ち溢れた中級者向けC言語講座


X68kでゲームなんかを作る時に気が付いたことをつらつらと書いてみようと思う。 使っているのは真里子版GCC(gcc version 1.28 Tool#2(X680x0))。 ほとんどANSI−C準拠だそうだが、いろんな拡張もされている。 僕が便利だと思う機能も紹介しようと思う。ただし、これから書くのは 僕が便利だと思っているだけで実際はそうでもないかも知れないし、 趣味の問題も含まれているかも。それから、僕はOh!X等を読んでないから、 そっちに詳しく書かれてる事も あるだろうと思うけど。
  1. コメントに//を使おう!

    たいした事じゃないけど、環境変数GCC_OPTIONに+を付け加えておくと、 C++式のコメント//が使えるようになる。 //から行末までコメントアウトされる。これは/* */の間にあっても大丈夫。 って言うと変に聞こえるかも知れないけど、例えば

            foo();  /* 呼びだし1 */
            zzz();  /* 呼びだし2 */
    
    の2行をコメントアウトしたいときに、
    /*      foo();  /* 呼びだし1 */
            zzz();  /* 呼びだし2 */
    */
    
    とやってもエラーが出て、うまくいかない。これを
            foo();  // 呼びだし1
            zzz();  // 呼びだし2
    
    としておけば、
    /*      foo();  // 呼びだし1
            zzz();  // 呼びだし2
    */
    
    とやってもエラーが出ない。 ちなみに、頻繁にコメントアウトし直すような部分では、
            foo();  // 呼びだし1
            zzz();  // 呼びだし2
    /**/
    
    としておいて、foo()の前に/*を付けたり外したりするだけで簡単に入れ替えられる。
    /*      foo();  // 呼びだし1
            zzz();  // 呼びだし2
    /**/
    
    要するに、Cではコメントのネスト(入れ子)が出来ないって事に起因するものばかりなんだよね。

  2. 構造体を使おう!

  3. マクロを使おう!
    何かあったら、マクロを使おう。

    1. 一番よく使うのは、定数の定義

      1. 普通の使い方
        これは基本中の基本だから言うまでもないと思うけど。
        EDATA   enemy[10];
        
        よりは、
        #define ENEMY_MAX 10
        EDATA   enemy[ENEMY_MAX];
        
        とした方がいい。これだと、敵の数を変えたい時は#defineを変えて、このマクロを使っている ファイルをコンパイルし直すだけで済む。

      2. 列挙
        連続した定数を定義する時は、列挙を使おう。
        定数の宣言で#defineをたくさん使うなら、特に連続した数を使うなら、列挙を使うのが便利。 例えばアイテムの番号を管理する時、普通はただの数字でやってしまうかもしれないが、 これを列挙で表してみる。
        enum{ item_yakusou=0,item_dokukeshi,item_potion,item_not=-1 };
        /* このenumは下のdefineと同じ
        #define item_yakusou    0
        #define item_dokukeshi  1
        #define item_potion     2
        #define item_not        -1
           あるいは、こうでもいい。
        enum{ item_not=-1,item_yakusou,item_dokukeshi,item_potion };
        */
        
        そして、その名前をプログラム中では使うようにする。
                if(equip==item_yakusou){ // 薬草を装備している時
                        equip=item_not;
                        ...
                }
        
        あるいは
                switch(equip){
                    case item_not:      //何も持ってない
                        break;
                    case item_yakusou:
                        ...
                }
        
        といった具合に。こうしておけば、アイテムの追加や削除を行った時のプログラムの 改造、あるいは番号の変更などが ものすごーく楽になる。

    2. 大域的な変数もマクロにしてしまおう

      変数なんかマクロにしてどうするんだとか言われそうだけど。言いたいのは、 構造体の特定のメンバを指定する場合はマクロにしておくと便利ってこと。

      typedef struct{
              int     str,def,mp;
      } PLAYER;
      #define pl_str  pdata.str
      #define pl_def  pdata.def
      #define pl_mp   pdata.mp
      extern  PLAYER  pdata;
              if(pl_mp>0) ...;
      
      とか。あまり意味無いように見えるかも知れないけど。もっと便利そうな例としては
      #define EDX(p)  (p)->x
      #define EDY(p)  (p)->y
      extern  EDATA   enemy[];
              EDX(enemy+1)=10;
      
      とか、
      #define EDX(i)  enemy[i].x
      #define EDY(i)  enemy[i].y
      extern  EDATA   enemy[];
              EDX(1)=10;
      
      みたいな感じかなあ。関数に代入してるみたいに見えて変だけど。 この利点は、構造体を変更した時にすごくよく現れる。EDATAを
      typedef struct{
              struct{
                      int     x,y;
              } xy;
              int     hp;
      } EDATA;
      
      という風に改造したとする。この時、直接enemy[0].xとかやっていたら、それらを全てenemy[0].xy.xに 直さなければいけない。しかしマクロを使っていれば、マクロの部分だけ
      #define EDX(i)  enemy[i].xy.x
      #define EDY(i)  enemy[i].xy.y
      
      という風に直すだけで済む。この差は大きいよ、ほんとに。

    3. 関数もマクロにしてしまおう

      大した事じゃ無いけどね。もともとマクロは関数と形が似てるし。
      例えば、グラフィック画面内で転送するルーチンを

      void    gcopy(int sx,int sy,int sp,int nx,int ny,int dx,int dy,int dp)
      {// ページspの(sx,sy)からページdpの(dx,dy)へnx*nyの範囲だけコピーする
      auto    short   *s=gad(sx,sy,sp),*d=gad(dx,dy,dp);
      ...
      }
      
      という具合に作ったとする。gadは
      #define gad(x,y,page)   ((short*)(0xc00000|((page)*0x80000)|(((y)*512+(x))*2)))
      
      という、座標から画面のアドレスを算出するマクロ。 ところが、これを改造して
      void    gcopy(short *s,int nx,int ny,short *d)
      {// *sから*dへnx*nyの範囲だけコピーするルーチン
      ...
      }
      
      としたとする。これに伴って呼びだし側を全て変えなくてはいけないが、ここで
      #define gcopy(sx,sy,sp,nx,ny,dx,dy,dp) gcopy_(gad(sx,sy,sp),nx,ny,gad(dx,dy,dp))
      
      というマクロを新設する。実際の関数gcopyは、マクロと区別する為にgcopy_と名前を変える。 gcopyを呼び出しているソースの方は、このマクロを先頭に加える以外は何も変える必要が無い。

    4. 初期化式もマクロにしよう

      ANSI−Cでは、構造体に代入できる。 初期化の代入もマクロにしてしまえば、構造体を変更した 時に便利。

      #define init(x,y,hp)    { (x),(y),(hp) }
      EDATA   enemy[ENEMY_MAX]={ init(0,0,10),init(1,0,20), ...};
      
      例えば構造体が
      typedef struct{
              int     hp,x,y;
      } EDATA;
      
      に変わったら(hpとx,yの順序が変わった)、マクロinitを
      #define init(x,y,hp)    { (hp),(x),(y) }
      
      にするだけで、実際のデータの初期化の方は何も変える必要が無い。 後で見て混乱する可能性はあるけど。

    5. マクロの注意点

      関数や変数のマクロを使っていると、エラーが出たときに、一見どこにも間違いが 無いように見えることがある。そんな時は、マクロ定義の方が間違っているのかも 知れない。gcc -E で出てくるプログラムをコンパイルしてみれば、 間違っている部分が分かるかも。

    6. マクロ関係で知ってると便利?なこと

      1. 複数行にわたるマクロ
        こんなの、書くまでも無いと思うけど。複数行にまたがるときは、行の最後に \ を書くだけでいい。この方法はマクロ定義に限らず、ソースファイル 上のどの位置でも通用する。

      2. 文字列の連結
        これはマクロとは直接の関係は無いけれど、文字列は並べるだけでくっつけられる。
        って意味不明だけど、例を見ればすぐ分かるでしょ。
        static  char    test[]="foo" "zzz";
        
        というのがあったら、
        static  char    test[]="foozzz";
        
        と同じことになる。printfとかで長い奴をまとめるのにいいんじゃないかな。
                printf("test-func\n");
                printf("usage: test.x  ...\n");
        
        というのと、
                printf("test-func\n"
                       "usage: test.x  ...\n");
        
        というのは(見た目は)全く同じ動作をする。printfの実行が2つから1つになった分だけ速くなる と思うよ。

      3. トークンの連結
        これは文字列の連結に似ている。
        #define test(x,y)       x##y
        
        この##というのがポイント。test(foo,1)はfoo1というトークンになる。 ゲーム作ってる時には使った事無いけど、知ってるので自慢出来る(?笑)。

      4. トークンから文字列への変換
        これはマクロで使う。それ以外では多分使えないと思う。
        #define pri(s)  printf( #s "\n")
        
        というマクロは、pri(test)とすると、マクロ展開でprintf("test" "\n")となり、文字列の連結で printf("test\n")と同じになる。 これらは、あくまでコンパイル時に処理されるのであって、実行時に処理されるわけでは無いので、 注意。下手に変数なんかを使うとコンパイルエラーが出ると思うよ。

      5. 引数不定のマクロは出来ない
        関数定義でいうところのfunc(int arg,...)みたいな形の、引数の個数が不定の マクロは作ることが出来ない。出来ないってことだけ書いてもしょうが無いけど、 たまにある質問らしいので。

  4. ヘッダファイルを分けよう!
    機能別にヘッダファイルを分けよう。

    僕の場合は、構造体の定義ファイル・マクロの定義ファイル・変数の定義ファイルに分離している。 いずれも大域的な物のみだけど。 最近では、変数に直接関わる構造体は変数の定義ファイルに 加えてしまった方がいいんじゃないかと思ってるけど。

    EDATA   enemy[ENEMY_MAX];
    
    みたいな奴はね。そうでなければ、逆に構造体を使う変数だけさらに別のファイルにするか。 ファイルをincludeするのは、結構時間がかかる(iocslib.hとかdoslib.hなどを includeしてみれば良く分かる)。 なるべくなら、includeするのは少ない方がいい。ヘッダファイルを分けておけば、 関係ないヘッダはincludeしないという風に、融通がきく。 それから、関数の宣言ファイルも作っておく方がいいかも。 ANSI−C式の関数宣言をしておけば、引数の型のチェックはしてくれるからね。
    // 関数の宣言のヘッダファイル
    void    sub(int x,int y);
    int     attack(ENEMY *ep);
    int     dec_hp(int hp,int damage);
    
    特に引数がポインタや構造体の時にすごく役に立つ。ただ、 関数を新しく作ったり引数や返り値を変更したりする度にこのヘッダファイルを 書き換えなければいけないのが面倒だけど。 ところで、大域変数の宣言は
    int     data;
    extern  int     data;
    
    と2種類ある。上のは宣言し、その領域を確保する。下のは宣言だけで、領域の確保はしない。 もし全てのソースファイルで領域の確保がされていないと、リンクの段階でエラーになる。 逆に、領域確保されているのが2ヶ所以上あると、二重定義でエラーか警告になる。 そこで、僕の変数の定義ファイルでは
    #ifndef EXT
    #define EXT     extern
    #endif
    EXT     int     data;
    EXT     ENEMY   enemy[ENEMY_MAX];
    
    という具合になっている。そして、このファイルをincludeしているどれか1つのソースファイルのみ、 includeする前に
    #define EXT
    
    という行を付け加えている。こうしておくと、そのファイルのコンパイル時には領域確保もするし、 それ以外のファイルでは領域の確保はしないので、大丈夫。 難点は、初期化が単純に書けないこと。初期化式だけは#ifとかを使って別々にしなければいけない。あるファイルのみ
    #define INIT
    #include        "variable.h"
    
    とやっておいて、ヘッダファイルの方は
    #ifdef INIT
            int     data=10;
    #else
    extern  int     data;
    #endif
    
    という風にするとか。

  5. asm命令を使おう!

    これは68のGCCの拡張だと思う。trap命令なんかを記述するには便利。いろいろと複雑な事が 出来るみたいだけど、僕は単純なのしか知らないし、それだけで取り敢えず充分だと思うけど。 使うには、環境変数MARIKOにAを加えておく必要がある(かも)。 参考までに、僕のGCC関係の環境変数の一部を書いておこう。

            SET MARIKO=ABD
            SET GCC_OPTION=GTFAMLJKOE+
            SET GCC_NO_XCLIB=USE_LIBC
    
     で、asmの例として、ZMUSICのm_stat(0)に相当する関数をば。
    int     zmdstat(void)
    {
    register long   rd0     asm("d0");
    register long   rd2     asm("d2");
            rd2=0;
            asm volatile(
                    "moveq.l #9,d1\n"
                    "trap #3"
                    :"=d"(rd0)      /* 値が返るレジスタ変数 */
                    :"d"(rd2)       /* 引数として使われるレジスタ変数 */
                    :"d1","a0"      /* 破壊されるレジスタ */
            );
    return rd0;
    }
    
    registerというのは、autoとほぼ同じ。GCCでは、最適化の為にautoとregisterはほとんど 区別しないらしい。違いは、register指定した変数はアドレスを持たないということだけ。 この辺の詳しいことはANSI−Cの本に出てるでしょ。
    register long   rd0     asm("d0");
    
    は、変数rd0にd0レジスタが割り当てられる事を明示している。
    register long   rd0;
    
    だと、変数rd0にはどのレジスタが割り当てられるか分からない。 ちなみにこのzmdstatの変数定義の部分は、マクロを使って
    #define REG_LONG(reg)   register long r##reg asm(#reg)
    REG_LONG(d0);REG_LONG(d2);
    
    という具合にすることも出来なくは無い。それは置いといて、
    asm(*文字列* : *値の返るレジスタ* : *使用レジスタ* : *破壊レジスタ*);
    という形式は、コンパイルしてアセンブリ言語に直している時、そのasm文の在る位置に*文字列*を ただ置くだけなのだ。gcc -S とやって見てみればよく分かる。従って、"moveq #9,d1\n"は 改行コードまで入れている。こうしないと、アセンブル時にエラーになる。 GCC使っててアセンブル時にエラーが出るのは、これ位でしょ。多分。実際のところ、文字列の 連結によって"moveq #9,d1\ntrap #3"という文字列を置いているわけだからね。 また、*値の返るレジスタ*,*使用レジスタ*,*破壊レジスタ*は必要無いなら省略出来る。 それから、"d"(rd2)の"d"はデータレジスタを表しているらしい。"a"ならアドレスレジスタだと思う。 この辺は詳しくないんだよ。GCCの本にもっと詳しく出てるでしょう。 それと、asm(...じゃなくてasm volatile(...になっているのは、このasm文を必ずコンパイルする ってこと。上の例では関係無いけど、返り値の指定が無いようなasm文では、最適化によってasm文が コンパイルされなかったりする事があるらしい。そうならないように、volatileを入れているのだ。

  6. autoを書くのだ!
    これはほとんど趣味の問題だけど。 ローカルな変数の宣言では、たいていの人はautoなんて書かない。でも僕は書く。何故かというと、
    #define US      unsigned
    #define REGIST  register
    void    sample(void)
    {
    static  US char c;
    auto    int     zzz;
    auto    EDATA   *ep;
    REGIST  int     i,j;
    ...
    }
    
    という具合に、他の指定と一緒に書くとき綺麗なのだ。 構造体名も8文字未満に抑えておけば、型名の所もぴったり収まる。タブが8文字ならね。 だから、デフォルトでタブが自由にならないemacsは大っ嫌いだ。

  7. 知ってると便利?
    知ってると便利かも知れない事を、思い付くだけ書いてみる。

    1. C言語知っててprintfを知らない人はいないだろう。そして、たいていfprintfも知っているだろう。 でも、sprintfを知ってる人って少ないような気がする。
              printf(format,arg...)
              fprintf(FILE*,format,arg...)
              sprintf(char*,format,arg...)
      
      sprintfはこっちで用意するバッファに書き込んでくれるのだ。 ちなみにcprintfというのもあるのだが、これが何かは知らない。誰か教えて。
      それから、vprintf,vfprintf,vsprintfというのもある。 これは、複数の引数を指定する代わりに引数の配列を使うものだ。 詳しいことは自分で調べてね。

    2. 僕の友達によれば、charはunsignedで、shortはsignedで使うのが良いらしい。
      charのデータはアセンブラではbyte単位でレジスタにロードするわけだけど、その時に signedならlongに直すために2回符号拡張する。unsignedならロードする前にレジスタに0を 代入するようだから、あながち間違ってもいないと思う。
      shortの場合はsignedの符号拡張も1回だし、unsignedのロード前の0の代入も 1回だから、よく分からない。アセンブラの命令の実行時間の差かな?
      そういうわけで、なるべくintを使うのが、速さの問題から言うと良い。 単なるフラグに4バイトも使ってしまうのがもったいない気もするけど。

    3. GCCでは、ブロックに値を持たせる事が出来る。
      i=({ auto int d;while(1){ d=getc(fp);if(d==EOF||d=='\n') break; } d; });
      
      { }で作られたブロックを( )でくくるだけでいいのだ。このブロックの最後に変数がただ置いてある 事に注目。これがブロックの値になるのだ。値を返すマクロを作るのに便利かも。

    4. エスケープ文字は#includeのファイル名の指定の中でさえ有効なので注意。
      #include        "A:\gcc\include\test.h"
      
      は駄目で、
      #include        "A:\\gcc\\include\\test.h"
      
      あるいは
      #include        "A:/gcc/include/test.h"
      
      としなければならない。つまり、パス名の区切りのつもりで \ を使っても、だめだってこと。/で代用しよう。

    5. &,|は加算減算などよりも優先順位が低い。おかげでif文などの条件式で &&,||の代わりに使っても大丈夫なのだが、お勧めは出来ないね。実行が遅くなるから。
      BASIC to C で変換したプログラムのif文が&&,||でなく&,|を使っているのは、 BASICがそういう仕様だから。つまり、BASICでは全ての条件を計算し、その後で ANDなりORなりの演算をする。しかしC言語では、1つ1つの条件を順番に計算して いって、条件に合わなくなった(あるいは条件を満たした)時点で残りの条件は 無視して次の文に進む。だから、必ずしも全ての条件を計算しない&&,||の方が 早い、というわけ。

    6. 構造体のビットフィールドは使い方次第では使える。 ビットフィールドはANSI−Cで定義されてるが、ビットの割り当ての順序は 決まっていない。 68のGCCでは、上位ビットから割り当てられていくようだ。それを利用して、 スプライトの表示用のデータなんかは
      typedef struct{
              short   x,y;
              int     v:1,h:1,dummy:2,color:4;
              char    sp_code;
              short   prw;
      } SPSR;
      
      という構造体で表す事が出来る。スーパーバイザーモードなら、0xeb0000に この構造体のデータを直接書き込むことによってスプライトを表示することが出来る。 僕はたまにIOCSのVDISPSTを使った割り込みの中でスプライトを表示するけど、 この割り込みではスーパーバイザーモードだから、何の遠慮もなくこの方法が使える。
      でも、パレットデータなんかを
      struct RGB{
              int     g:5,r:5,b:5,i:1;
      } pal;
      
      という風に表して、pal.r++;みたいな演算をやると、コンパイルされたプログラムは 結構複雑になる。 下手をすると、自分で専用のルーチンを書いた方が良くなることも有り得るので注意。 この場合でいえば、
      auto    int     pal,r=pal;
      r+=1<<6;r&=0b11111<<6;pal&=~(0b11111<<6);pal|=r;
      
      とする方が良いかも知れない。何通りかプログラムを書いてみて、gcc -S で確認するのが確実でしょう。

    7. 割り込み内で変化する変数(カウンタにするとか)を使う場合、その変数はvolatile修飾しよう。
      #define __IOCS_INLINE__
      #include        <iocslib.h>
      #include        <interrupt.h>
      static  volatile int    wc;
      static  interrupt void  wait(void){
              if(wc>0) wc--;
              IRTE();
      }
      int     main(void){
              VDISPST(wait,0,1);
              wc=10;while(wc);
      }
      
      という感じだね。これは、VDISPSTを呼んだ後に10/60秒待つプログラムだ。 もしwcの宣言部分にvolatileが無かったら、wc=10;while(wc);は最適化によって while(10);に置き換えられてしまうのだ。そうすると、いつまで経っても終わらなくなってしまう。 割り込み関数はinterrupt修飾し、その関数はマクロIRTE()で終わるようにするのも忘れずに。 それと、疑問点を1つ。何故か、68の電源を入れて最初に実行したVDISPSTって、割り込みが 発生するのに時間がかかるんだよね。不思議だ。

    8. 関数へのポインタは使った方がいいかどうかは分からないけど。やり方は簡単で、
      extern  long    foo();
      auto    long    (*funcp)(int a,int b);
              funcp=foo;(*funcp)(1,2);
      
      という具合。それと、何故か関数のポインタの配列の初期化が出来ないんだな。仕方ないから
      extern  int     foo1(),foo2(),foo3();
      static  void*   farr[]={ foo1,foo2,foo3 };
      auto    int     (*funcp)(void);
              funcp=farr[0];(*funcp)();
      
      という具合にやってるけど。

    9. 今までに gcc -S とか gcc -E とかを使うといいって書いた部分もあるけど、これは コンパイルを途中の段階で止めるものだ。-Sは最終的に出力されるアセンブリを 標準出力(だったと思う)に出力し、-Eはマクロ展開した段階(だったかな?)で 標準出力に書き出す。最適化の確認や、デバッグに使うのもいいかも知れない。

    10. 環境変数MARIKOに D あるいは E を加えておくと、エラーや警告が出たときに ed.x用のmariko.errというタグファイルを作成してくれる。これはed.xやsupered.xで ロードする。 それにはエラー(や警告)の出たファイル名と、その行番号が書かれている。 そして必要な行にカーソルを移動して ESC-v と押すと、そのファイルを自動的に 読み込んで、エラーの出た行に飛んでくれるのだ。これがタグジャンプと呼ばれる 機能で、とっても便利。

  8. 互換性の問題!

    ちょっと地域ローカルな話になるが、他機種で動くように配慮して68で作ったプログラムが、 研究室のUNIXのライブラリではうまく動かなかった事がある。
    ○68のsprintfは文字数を返すんだが、あっちはアドレスを返す。
    ○68でrealloc(NULL,10)はmalloc(10)と同じ動作になるのに、あっちはNULLを返す。
    これにはハマったぞ。他にも何かありそうだし、みんなも移植する時は気をつけよう。 標準関数といえども当てにはならない。
    それから、コードの問題もある。漢字コードなんかはその機種の物に変換した方がいいし、 ^Mや^Zのコードは邪魔になり、ひどいときはその所為でコンパイルできない!

  9. 終わり!
    という訳で、適当に書いてみたけど。

    分かる人には分かると思うし、そんなの当たり前じゃんと いう人もいるだろうけど、ちっとは役に立つこともあるんでないかい? 中級者向けとか銘打っておきながら、初心者相手みたいな事も書いて しまったけど。
    一番書きたかった事は、マクロで変数をアクセスすることかな。それと、 構造体を変更したら、コンパイルし直すことね。
    えらそうな口調で書いてあるけど、あんまりうまく説明出来て無いし、 C言語とライブラリをごっちゃにしてるし、 間違ってる事もあるだろうけど(特に用語・概念・文法)、 参考くらいにはなると思うから、あとは自分でいろいろ試してみて。
    それから、文中のプログラムについては 正しいとは思っているけれども 実際に全て確認したわけでは無いので。そこんところ、よろしく。

  10. 参考文献だ!


1995/10 著:Hishida Masato hishida@tera.is.uec.ac.jp

▲68通信vol.1のページへ