第26回 プログラミングについて 『関数とポインタ』

『関数とポインタ』
今回は関数とポインタの話をしましょう。
Windowsのプログラム(Quick Leaf)をC++で作っていて最初に戸惑ったことがあります。Windowsのプログラムは基本的にイベント駆動のプログラムです。イベントというのは、キーが押された、マウスが動いた、ボタンが押されたといった事象のことで、これらの事象が発生したときにアプリケーションプログラムはそれに対応して動作しなければなりません。Windowsはこれらの事象が発生したときに、メッセージという形でアプリケーションに通達します。
Windows アプリケーションプログラム
マウスが動いた! → えっ!マウスが動いたの!
キーが押された! → あっ!ボタンが押されたの!
メニューが選択された! → 大変だ!メニューが選択された!
: :
: :
といった具合にアプリケーションプログラムが休む暇もなくメッセージが送られてきます。このような人間が直接発生させたイベントの他、ウインドウの一部分が表に出たとか、マウスの形状を変えるぞとか、休憩してもいいぞとか様々なメッセージが送られてきます。
メッセージが一杯送られてくるのはそれはそれでいいのですが、問題はそのメッセージが必ず1つの関数に送られてくるのです。実際にはアプリケーションにメッセージが送られてきてから、アプリケーション上の多数のウインドウに振り分けられるまでにはかなり面倒な処理があるのですが、最終的にそのメッセージを処理しなければならないウインドウにメッセージが到達した後、適切な関数に情報が伝達されます。
例えば、キーが押されたときには OnKeyDown という関数が、マウスが動いたときには OnMouseMove という関数が起動されメッセージが伝達されます。
いまメッセージが各関数に振り分けれれる関数が OnMessage というものだったとすると、次のようにメッセージの種類に応じて関数に渡されます。
OnMessage(int message,int id...)
switch(message){
case KEYDOWN : OnKeyDown(...);
break;
case MOUSEMOVE : OnMouseMove(...);
break;
:
:
case COMMAND :
switch(id){
case DRAWLINE : OnDrawLine(...);
break;
case DRAWCIRCLE : OnDrawCircle(...);
break;
:
:
}
}
}
苦労したのは様々な関数が、マウスが動いたといった1つの種類のメッセージをどのように正しく使えるようにするかということでした。上の例でいえば OnMouseMove がどのようにしてマウスが動いたという情報とその座標を、今必要としている関数に渡すかということです。
マウスが動いたというメッセージを必要とする2つの関数 DrawLine と DrawCircle
があったとします。通常のWindowsのアプリケーションの操作の流れで考えると、メニューが選択されたときに COMMAND というメッセージとコマンドを認識するための番号 id がパラメータが上記の OnMessage 関数に渡されてきます。 OnMessage 関数はその情報を元に、OnDrawLine か OnDrawCircle を起動し、マウスの座標値などを伝達します。ここまでは問題はありません。
OnDrawLine または OnDrawCircle はこれから線または円の描画を開始するための準備を行ないますが、その次のマウスが移動したという情報を受け取るためにはWindowsがメッセージを送ってくるのを待たなければならないので制御をメッセージの受け取りの部分に戻さなければなりません。その後、マウスが動いたときに OnMouseMove関数が受け取った座標値を自分のものとして届けられるような仕組みを作っておかないと100年たっても1000年たってもどうにもなりません。
さてこの仕組み、何種類か考えられますがその1つをまず考えてみましょう。
OnDrawLine または OnDrawCircle が OnMouseMove に対して現在マウスの移動の情報を必要とする関数を示す認識番号を教える手法が考えられます。
#define NEED_MOUSEMOVE_ONDRAWLINE 1
#define NEED_MOUSEMOVE_ONDRAWCIRCLE 2
int need_mouse_move=0;
void OnDrawLine()
{
need_mouse_mode=NEED_MOUSEMOVE_ONDRAWLINE;
:
:
}
void OnDrawLine__MouseMove(int x,int y)
{
:
:
}
void OnDrawCircle()
{
need_mouse_mode=NEED_MOUSEMOVE_ONDRAWCIRCLE;
:
:
}
void OnDrawCircle__MouseMove(int x,int y)
{
:
:
}
void OnMouseMove(int x,int y)
{
if(need_mouse_mode==NEED_MOUSEMOVE_ONDRAWLINE){
OnDrawLine__MouseMove(x,y);
}
else if(need_mouse_mode==NEED_MOUSEMOVE_ONDRAWCIRCLE){
OnDrawCircle__MouseMove(x,y);
}
:
:
}
この手法で希望する動作を実現することは可能です。がしかし問題があります。
OnMouseMove が他人の関数のことを知っていなければならないことなのです。上の例では単純なのですが、線を描画するときでも最初の線分のときとそうでないときで別の関数にメッセージを送ってもらいたいときもあります。また描画する種類が増えたり、機能を強化していくに従って OnMouseMove をいつも改修しなければなりません。これではプログラムを作るのが辛くなってしまいます。
そこで次の手段が、メッセージを送ってもらいたい関数が送ってもらう関数を教える
ようにすることです。そのために単なる認識番号ではなく、送ってもらう関数の場所(アドレス)を教えるようにします。
void OnDrawLine()
{
void OnDrawLine__MouseMove(int x,int y);
OnMouseMove__SetSend(OnDrawLine__MouseMove)
:
:
}
void OnDrawLine__MouseMove(int x,int y)
{
:
:
}
void OnDrawCircle()
{
void OnDrawCircle__MouseMove(int x,int y);
OnMouseMove__SetSend(OnDrawCircle__MouseMove)
:
:
}
void OnDrawCircle__MouseMove(int x,int y)
{
:
:
}
static void (*send)(int x,int y)=NULL;
void OnMouseMove(int x,int y)
{
if(send)send(x,y);
}
OnMouseMove__SetSend(int (*s)(int x,int y))
{
send=s;
}
このようにすれば OnMouseMove 関数は他人が誰であろうと関係なく情報を伝達できます。この方法では情報を受け取る側が送り先を自由に設定できますので、プログラムを単純にする上、自由度が上がるのでこのようなときには有効な手法といえます。
頻繁に呼び出される関数が状況に応じて複数の同じ型で同じ引数の関数の1つを呼び
出す場合などにもこの手法を使うとスッキリすることもあります。
sub(int id,int x,int y)
{
if(id==1)return abc1(x,y);
else if(id==2)return abc2(x,y);
else if(id==3)return abc3(x,y);
else if(id==4)return abc4(x,y);
else if(id==5)return abc5(x,y);
else if(id==6)return abc6(x,y);
}
この関数が頻繁に呼び出されるとき、IF文の処理で時間がかかるとすれば、
int abc1(int x,int y);
int abc2(int x,int y);
int abc3(int x,int y);
int abc4(int x,int y);
int abc5(int x,int y);
int abc6(int x,int y);
sub(int id,int x,int y)
{
static int (*(func[]))(int x,int y)={abc1,abc2,abc3,abc4,abc5,abc6};
(func[id-1])(x,y);
}
とすると、スッキリします。
少しの説明のために長々とお話しましたが、今回紹介した手法はあまり頻繁に使用するとプログラムが分かりずらくなることもありますので注意して下さい。プログラムを書くときにはまずは正攻法で表現するのが基本です。とくにポインタを使うときにはプログラムが明快に表現できることもありますが、難解なものになることもあるので注意が必要です。
それではまた次回。