●COOL で書かれたソースコードの読み方


1.はじめに

C 言語によるオブジェクト記述法 COOL に従って書かれたソースコードには、 読むためのコツがあります。目的に応じて次のいずれかの方法があります。

  1. メイン関数からスタートして読んでいく
  2. クラスからスタートして読んでいく
  3. 特定の処理の関数からスタートして読んでいく
DOS(コンソール)プログラムでは、メイン関数から始まるので、そこから 読み始めるとよいでしょう。メイン関数も一般の関数と同じ読み方が 出来るので「2.関数からスタートして読む」の章を参照してください。
  int  main()   /* 最初にメインから読む */
  {
    /* 処理 ... */
  }
Windows プログラムでは、イベントドリブン(ユーザなどのきっかけから 処理を記述する方式)なのでメイン関数はありません。 MFC などのクラス・ライブラリから作られたものは、 メインウィンドウ・クラスの 「3.クラスからスタートして読む」の章を参照してください。
メイン(WinMain)関数の中に、メッセージループがある場合は、 それぞれのメッセージの case 文を、クラスインターフェイスとみなして ください。
  class  CMyDlg {  /* 最初にメインウィンドウ・クラスから読む */
  };

  int  WinMain()   /* クラスと見なす */
  {
    while ( PeekMessage() ) {
      switch() {
        case WM_PAINT:   /* インターフェイスと見なす */
          /* 処理 .. */
      }
    }
  }
以下で詳しく説明していきますが、その前に、そのプログラムがどのように 動作するかを確認できれば、まず、しておいた方がよいでしょう。

ここで書かれていることは、COOL に従ったソースコードの読み方ですが、 COOL に従っていない一般的なソースコードを読む場合にも参考になるでしょう。


2.関数からスタートして読む

メイン関数やある機能を持った関数を読むときのコツを説明します。
項目 2-1 から 項目 2-5 まで順番に進めていきます

2-1.初期化関数、後始末関数は後で読む
メイン関数から読んでいくと、始めに必ずと言っていいほど初期化関数が あります。しかし、初期化関数は、後で理解するとよいでしょう。 なぜなら、初期化関数は、一般的な処理関数を実行するために必要な 前処理なので、その処理関数があって初めて初期化関数が存在している 言わば脇役だからです。
後始末の関数も同様です。
  void  Class_init()    /* 初期化関数の内容は後で理解する */
  {
    /* 他の関数が使えるようにするための処理 ... */
  }

  void  Class_finish()  /* 後始末関数の内容は後で理解する */
  {
    /* 資源を戻すための処理 ... */
  }

2-2.関数全体の出力(インターフェイス)を確認する
引数や返り値は、関数全体の入力と出力になっていることが多いです。 特に出力に注目することで、関数の機能がよく分かります。
関数全体のコメントを読む際でも、出力に注意して読んでいくとよく分かります。
ただし、初期化関数は、基本的に出力が無いので注意してください。 (あえてあげればメンバ変数すべてに出力すると言えますが、 あまり理解を助けるものではありません。初期化はどんなモノであれ、 それ以外の関数の準備という共通な意味があり、それ以上追求しても仕方ありません。)
エラー値も一応出力ですが、エラー値からは関数の機能が分かりませんから、 それ以外の出力を探します。
  /* 関数全体のコメントを読むときも出力に注目 */
  int  func( int input, int* output )   /* 引数や返り値の中から出力に注目する */
  {
    *output = f( input );   /* 出力は output、こいつが重要★ */
    return  err;            /* エラー値は出力ではない */
  }
出力は、値として出力するものだけではありません。
あるクラスや装置(画面など)にメッセージ(命令)を送る(関数を実行する) ものもあることに注意します。
また、出力は引数にするのがよいプログラムですが、 構造体(クラス)のメンバ変数に格納(命令)することもよくあります。 特に、クラス操作メソッド、メンバ関数)では、そのクラス属性(メンバ変数)を処理の対象とすることが多くなっています。
グローバル変数(オブジェクト)に格納(命令)することもあることに注意します。 特に、メイン関数に多くあります。 たとえば、printf は、グローバルなオブジェクト(標準出力装置)に 出力するものと考えます。
  void  put( STRUCT* st, int data )  /* 構造体のメンバに出力する可能性も */
  {
    fprintf( st->file, "%d", data );   /* メンバ変数のファイルに出力 */
    printf( "%d", data );              /* 標準出力(グローバル)に出力 */
  }

2-3.ブロックに分けて構造を理解する
メイン関数やある機能を持った関数の内容を理解するために、 ブロックという数行の単位に分割します。

関数の意味的な構造には、大きく2種類があります。

  1. 処理を手順ごとに並べたもの
  2. 汎用的な関数を再利用したもの

1.のタイプの関数構造は、関数として必要な処理を実行するために 手順ごとに関数や式を並べたもので、 関数全体をブロックごとに分けることができます。 よいソースコードでは、ブロックごとに適切にコメントがしてあります。 1つ1つのブロックが何を処理して いるのかを理解していくことで全体を理解することができます。

関数は一般に次のような構造になっています。

  void  func()
  {
    /* 初期化ブロック */
    ...;

    /* 後で必要になるデータを求める処理のブロック */
    ...;

    /* 関数全体のメインとなる処理のブロック ★*/
    ...;

    /* 後始末ブロック */
    ...;
  }
ですから、関数全体のメインとなる処理がある、関数の後の方のブロックから 読んでいくと、トップダウンに全体を理解することが容易になるでしょう。
また、メインとなる関数は、関数全体の出力を出力していることが 非常に多いので、メインとなる関数をいかに見つけるかがポイントになります。

2.のタイプの関数構造は、汎用的な関数を用いて(再利用して)、 その差分を記述したものです。 まず、その汎用関数を理解しなければ、関数全体を理解するのは かなり難しいでしょう。なぜなら、関数全体のほとんどの処理が、その汎用関数の仕様に 合わせるための処理になるからです。
汎用関数は、上で説明した関数構造の「関数全体のメインとなる処理のブロック」に 存在する傾向が高く、これも関数の後の方のブロックになります。

  void  func()
  {
    /* 初期化ブロック */
    ...;

    /* 汎用的な関数に指定するデータを求める処理のブロック */
    ...;

    /* 汎用的な関数をコール ★*/
    ...;

    /* 後始末ブロック */
    ...;
  }
どちらの構造であれ、メインとなる処理のコメントに、目立つ★印などを付けると 後で読みやすくなります。

2-4.引数とローカル変数の意味を理解する
引数とローカル変数(構造体)の意味を確認します。
その意味がコメントに書いてあればいいのですが、書いてない場合や 書いてあっても理解できない場合は、関数内でどのように使われているかを 確認しなければなりません。その場合のコツは、 引数がアドレス参照になっている設定関数(初期化関数)や、代入文に注目することです。 その際、エディタの検索機能を用いて変数名から検索するとよいでしょう。

  void  func()
  {
    int  n;

    n = calc( x );       /* 代入文に注目 */
    printf( "%d", n );
    calc2( x, &n );      /* アドレス参照に注目、コール先も確認 */
    printf( "%d", n );
  }

構造体、またはクラスの意味を理解する場合は、 「3.クラスからスタートして読む」の章を参照してください。

  void  func( STRUCT* st )  /* 構造体(クラス)を理解する */
  {
    int  n;     /*(コメント)*/   /* コメントから意味を理解する */
    STRUCT st;  /*(コメント)*/   /* コメントと構造体の内容から意味を理解する */

    /* 処理 ... */
  }

2-5.コール先の関数のインターフェイスを確認する
関数の内容を理解するためには、コール先の関数の意味(インターフェイス) も確認すると理解が早まります。具体的には 「2-2.関数全体の出力(インターフェイス)を確認する」 に書かれていることを確認します。
特にメインとなる関数や、再利用した汎用的な関数の意味を まず理解しないと、関数全体の意味の理解も難しいでしょう。

  void  main()
  {
    func( n, &x );   /* この関数は n や x にどんな処理をしているのか */
  }

その際、全体を理解しようとしている関数と、コール先の関数の両方を マルチウィンドウで両方表示するとよいでしょう。

  void  main()
  {
    func( n, &x );
  }
  /* 関数全体のコメント */
  /*  ・int n;    コメント */
  /*  ・int* x;   コメント */
  void  func( int n, int* x )
  {
    ...


3.クラスからスタートして読む

構造体やクラス、MFC などのクラスライブラリを用いたアプリケーションを 読むときのコツを説明します。
アプリケーションの場合は、「3-2.クラスのインターフェイスを確認する」項目 から参照してください。(アプリケーションクラスのモデルは自明なので)
項目 3-1 から 項目 3-4 まで順番に進めていきます

3-1.クラスのモデルになったものを確認する
オブジェクト指向で設計されたクラスや構造体は、何かをモデリングして コンピュータ上に表現したものなので、まず、そのモデルになったものが 何かを確認します。
ただし、ワーク領域やコンテキストなど、モデルになったものが無い 場合もあります。その場合は、次の項目 「3-2.クラスのインターフェイスを確認する」に進みます。

モデルになったものは、クラス名やコメントによって確認できますが、 それでも確認できない場合は、クラスのメインとなる操作属性を 確認します。操作属性かは、クラスのタイプによって別れます。

メインとなる操作は、「3-2.クラスのインターフェイスを確認する」の項目を、 メインとなる属性は、「3-3.メンバ変数、関連するクラスのインターフェイス確認する」の項目を 参考に探します。
機能的なクラスかデータ的なクラスか分からない場合は、両方確認します。

3-2.クラスインターフェイスを確認する
クラスインターフェイスは、メンバ関数(操作属性的関数)です。 (C++ では public: なメンバ関数です。)
MFC などのクラスライブラリを用いたアプリケーションでは、 イベントに対応したインターフェイスが用意されています。
一般的なクラスでは、ヘッダファイルや、Knowledge Take! で処理された HTML ソースファイルの文頭(目次)にまとめて書いてあります。

send.c.htm (Knowledge Take! で処理された HTML ソースファイル)
  send.c
  ・〜の処理 ★[Send_go()]      ... KnowledgeTake! によって作られた目次
  ・〜の処理 [Send_go2()]       ... KnowledgeTake! によって作られた目次

   1| void  Send_go( Send*, int n )
   2| {
   3|     /* 処理 */

send.h
   struct _Send {
     Class  src;
   }
   void  Send_go( Send*, int n );    /* インターフェイス ★*/
   void  Send_go2( Send*, int n );   /* インターフェイス */

send.hpp ( C++ のヘッダファイル )
    class Send {
      void  go( int n );              /* インターフェイス */
    };

インターフェイスを一覧して、そのクラスを用いて何が出来るかについて 確認します。ただし、特定のメンバ関数の内容を理解するのではなく、 どのようなインターフェイスがあるかといった概観を確認します。
具体的には、「2.関数からスタートして読む」の章の 「2-2.関数全体の出力(インターフェイス)を確認する」の項目に書かれていることを 確認します。
内容の理解は、次の項目「2-3.メンバ変数、関連するクラスを確認する」に 書かれていることを確認してから始めるとよいでしょう。

メインとなるインターフェイスには、目立つ★印コメントをつけるとよいでしょう。
ただし、重要な機能のインターフェイスに付けるのではなく、 クラスの役割を理解するために必要な基本的なインターフェイスに付けます。

3-3.メンバ変数、関連するクラスインターフェイスを確認する
クラスは、メンバ変数(集約したオブジェクトや関連するオブジェクトや基本型)と 協調しながら目的の処理を実行します。ですから、関数の内容を理解する前に メンバ変数の意味(インターフェイス)を確認すると理解が早まります。
  struct _Send {   /* たとえば、src から dst へデータ転送するクラス */
    UseClass  src;    /* ソース(転送元)*/
    UseClass  dst;    /* デスティネーション(転送先)*/
    int  n;        /* 補足情報 */
  }
メンバ変数の意味を確認するには、この章「3.クラスからスタートして読む」と 同じ方法で行います。つまり、ボトムアップに(全体的なクラスの前に、部分的な クラス(メンバ変数)から)理解することになります。
具体的には「3-1.クラスのモデルになったものを確認する」と 「3-2.クラスのインターフェイスを確認する」 の項目に書かれていることまでを確認し、 メンバ変数のインプリメント(インターフェイスに対する実装的な部分、 ここではメンバ変数のメンバ変数やメンバ変数の関数の内容のこと)まで 確認する必要はありません。
  struct _UseClass {     /* Send クラスの1つのメンバ変数のクラスを確認する */
    UseUseClass  src;
  }
  void  UseClass_func();  /* インターフェイスまで確認する */
汎用的なクラスを利用して(再利用して)差分を実装することがよくあります。
クラス継承やメンバ変数に所有する(委譲する)ことで実装しますが、 その再利用したクラスを、どのメンバ変数より始めに理解する必要があります。
  struct _Class {
    SuperClass  reuse;   /* 再利用、実装のほとんどはこのクラスの内部 ★*/
    int  n;
  }
ただし、クラス継承の場合、インターフェイスのみ継承することがあります。 インターフェイスには、非常に抽象的なものもあるので、特に理解しなくても よいこともあります。(C++のみ)
  class _Class : public InterfaceClass {  /* インターフェイス的な継承 */
    int  n;
  }
メンバ変数は、常に有効な値が格納されていることが保証されています。 (カプセル化と呼びます)
なぜなら、初期化関数(コンストラクタ)によって初期値が 与えられますし、インターフェイス操作)を通してアクセスしているからです。

3-4.特定のインターフェイスの内容を理解する
メンバ変数を理解したら、特定のインターフェイス(メンバ関数)の内容の 理解も容易になります。
クラス初期化関数や後始末関数は後回しにし、 理解したい機能を持った関数について 「2.関数からスタートして読む」の章を参考に理解します。

  void  Class_func( STRUCT* st )   /* 機能的な関数を理解する */
  {
    int  n;
    STRUCT st;

    ...
  }

マクロで実装されているインターフェイスの内容は、ヘッダファイルにあるので 注意してください。

class.h.htm (ヘッダファイル・Knowledge Take! で処理された HTML)
  class.h
   ・マクロの処理 [Class_macro()]   ... 目次もヘッダファイルにある

  1| #define  Class_macro( this, n )  \
  2|       SubClass_init( this->sub )


written by Masanori Toda from Apr.3.1998