2018-01-12

Cプログラマのための、
C++オブジェクト指向簡単裏口入門(4)

指向!?

前回、C++でのオブジェクトは実際にはメモリ上の構造に過ぎないことを示しました。では、そんなオブジェクトに対する指向とはどういうことなのでしょうか。単なるメモリ上の構造にしては、少々大げさな表現に思えますよね。

しかし、本当のところ、ここにはパラダイムシフトと言える程の大きな変化が隠されています。にもかかわらず、その変化の基本は非常にシンプルです。

オブジェクト指向ではないC言語では、メモリ構造に対する処理は通常次のようになります。

コード1

// C言語でのメモリ構造操作
typedef struct
{
    int m_x, m_y;
} Point;

void setPoint(Point * pPoint, int x, int y)
{
    pPoint->m_x = x;
    pPoint->m_y = y;
}

int main()
{
    Point p;
    setPoint( &p, 100, -200 );
    return 0;
}
では、次にC++でオブジェクト指向的に書いてみます(C++でもCと同じスタイルで書けてしまうのでこういう表現をしましたが、今後特別な場合を除いてこの説明は省きます)。

コード2

// C++でのメモリ構造操作
struct Point   // 今回はclassではなく、structとしておきます
{
    int m_x, m_y;
    void setPoint(int x, int y);
};

void Point::setPoint(int x, int y)
{
    m_x = x;    // 構造体メンバm_xに設定
    m_y = y;    // 構造体メンバm_yに設定
}

int main()
{
    Point p;
    p.setPoint( 100, -200 );
    return 0;
}
Cにはない書き方がありますね。
1つは、構造体の中に関数 setPoint() のプロトタイプがあることです。これはこの構造体のメンバ関数と呼びます。変数をメンバ変数と言うのと一緒です。そして、そのプロトタイプの実装が Point::setPoint(){} といった関数名で書かれています。Point構造体のメンバ関数なので、「::」(コロン×2)で繋いでメンバ関数であることを表します。
main()の中では、構造体のメンバ変数にアクセスするときと同じように「.」(ドット)を使ってメンバ関数にアクセスし、呼出しています。

コード1も2も同じ機能を果たすプログラムですが、コード2のC++の方はsetPoint()の引数が1つ減っています。これは、メンバ関数の中から同じメンバに対してはグローバル変数のようにアクセスできるので、わざわざパラメータを必要としないためです。

さて、これら2つのプログラムの本質的な違いは何でしょうか。関数を動詞に喩えると、

  • Cの場合、「~をする」
  • C++は、「~が~をする」

と、なります。C++の方には主語があるのです。

あれ、たったそれだけのこと? 目先が違うだけでは? と感じられた人もいるかも知れません。今後、徐々にその意義を理解して頂ければいいのですが、今回は概要を説明しておくことにします。

~が~をする、というのは、オブジェクトと動作が強く結びつけられている、ということでもあります。そうすると何が良くなるのか、ユーザインタフェースを例に説明します。

WindowsにしてもMacにしても、スマートフォンにしてもそうなのですが、GUIにはオブジェクト指向的なコマンドの起動方法が含まれています。

たとえば、Windowsのデスクトップにワープロのドキュメントファイルがあり、それを編集するとき、皆さんはどうされますか? 多くの人はそのファイルをダブルクリックしますね。すると、結びつけられているワープロソフトがファイルを開いた状態で立ち上がります。ファイルが表計算ソフトのものであれば、表計算ソフトが同様に起動します。
ダブルクリックではなく、マウスの右クリックでコンテキストメニューを開き、「編集する」というコマンドを選んでも同じ動作が可能です。また、右クリックでは印刷コマンドとか、プロパティを表示するといった、そのファイルで可能な別のコマンドも実行できるようになっています。

この動作はユーザにとって極めて自然です。もし、GUIにおけるオブジェクト指向がなかったらどうでしょうか。編集するときは目の前にファイルが見えていても、一旦ワープロソフトを起動し、ワープロソフトからファイルを探してオープンする操作をしなければなりません。

世の中のコンピュータがGUIになる以前は、全てコマンドプロンプトからプログラムを起動しなければなりませんでした。テキストを編集するコマンドが edt で、編集対象のファイルが test.txt であるなら、

    > edt  c:\myfiles\test.txt

等と入力しなければなりませんでした。コマンドも、対象ファイルもどちらも覚えておかなくてはならなかったのです。

現在主流のGUIは単にマウスを使ってグラフィカルに操作するだけでなく、ファイルとコマンドの結びつきが定義されていて、そのファイルに対して何ができるのかといったことをユーザが考えたり、予め知らなくてもいいようになっています。
オブジェクト(この場合はファイル)と動作(コマンドやアプリケーション)が密接に関連付けられている、という意味ではオブジェクト指向的なユーザインタフェースであると言えるのです。

プログラミングに戻りましょう。

上記コード1では、Point構造体とそれを扱う関数との間に直接の関連付けはありません。Point構造体だけ見ても、それに対するどんな処理が存在するのか不明です。不便ですね。上記のCUIのときと似た不便さがあります。
逆に、コード2ではそれが明らかで、これもGUIの例になぞらえることができます。

関数の側から見てみると、コード1ではsetPoint()という関数名はグローバル定義にするしかありません。なので、他の目的でも同じ関数名が必要になった場合、名前がぶつかってしまいます。なので、汎用的で意味の広い関数名は使いにくくなります。不便ですね。
コード2の場合、同じメンバでなければ(同じメンバでも引数が異なれば可能)、同じ関数名がいくつあっても問題ありません。なので、シンプルで判りやすい関数名を使うことができます。
ところで、コード1での関数名はPoint構造体に値を設定する、という意味でsetPoint()という関数名にしてありますが、コード2は関連付けが明らかなので、あえて関数名にPointを含める必要はありません。なので、この場合は単にset()とした方が良いでしょう(コード3参照)。

コード3

// C++でのメモリ構造操作
struct Point   // 今回はclassではなく、structとしておきます
{
    int m_x, m_y;
    void set(int x, int y);
};

void Point::set(int x, int y)
{
    m_x = x;    // 構造体メンバm_xに設定
    m_y = y;    // 構造体メンバm_yに設定
}

int main()
{
    Point p;
    p.set( 100, -200 );
    return 0;
}

ここまでをまとめると、
オブジェクトを主体に考えること(つまりオブジェクト指向)で、オブジェクトに対して何ができるのかがはっきりする。処理、という観点においても、オブジェクトが結びついていることで何に対する処理なのかが明確になる。ということになるでしょうか。

そして、この判りやすさは使うときだけではなく、設計やコーディング時にも享受できるのですが、その点については今後徐々に理解して頂けると思います。


0 件のコメント:

コメントを投稿