2009年1月15日木曜日

第一回:main関数の引数

第一回はmain関数の引数あれこれ。C言語の基本事項なので、大したことは書きません。

1.序章



C言語を勉強し始めた頃、教科書に載っている台本通りのサンプルプログラムは以下のようなものだったと思う。


#include <stdio.h>

main()
{
   printf( “Hello!world!” );
}



長いことこういった書き方をしていると、これが定型文のようになってしまう事があるが、今回は初回と言うこともあり、C言語のプログラムがスタートする場所、つまりmain関数に関してちょっと掘り下げてみたいと思う。

 まず、mainは「関数」と言う名前が付いている訳なので、通常の関数と同様、引数と戻り値が存在している。main関数の本当の姿は以下のような形をしている。


int main( int argc , char * argv[] , char * envp[] )



戻り値はint型、第一引数(argc)はコマンドライン引数の数、第二引数(argv)はコマンドライン引数、第三引数(envp)は環境変数と言う形になっている。

 ひとつずつ解説していこう。



2.コマンドライン引数とは?



コマンドライン引数とは何だろうか?

DOSコマンドプロンプトやUNIXシェルで作業したことのある人にはなじみ深いものだと思うが、文字通りコマンドラインから入力する引数のことである。

例えば、DOSプロンプト内でカレントディレクトリのファイル一覧を表示するには以下の様なコマンドを使用する。


dir



カレントディレクトリではなく、指定したディレクトリのファイル一覧を取得するには以下のように入力すればよい


dir c:¥temp



上記のように入力すると、cドライブのtempディレクトリ内のファイル一覧が表示される。

ファイルが多すぎてスクロールアウトしてしまうので、画面内に入りきるだけを表示したい場合には以下のように入力すればよい


dir c:¥temp /p



更に、ファイル名がバラバラで表示されるので、文字列順にソートして表示したい場合には以下のように入力する


dir c:¥temp /p /n:o ※



別にdirコマンドの解説をするつもりではないので、この辺りで終わらせておくとする。dirコマンドが詳しく知りたい人は


dir /?



と入力すると、すごく分かりにくいヘルプが表示されるので、参考にして欲しい。

さて、dirがプログラムでC言語で書かれていると仮定しよう。(※)の例を見返すと、dirプログラムに対して、「c:¥temp」「/p」「/n:o」と言う3つのパラメータを渡していることになる。このパラメータがコマンドライン引数と呼ばれるものとなる。

例えば下記のようなプログラムを作成してみよう。


#include <stdio.h>

int main( int argc , char * argv[] )
{
   int i;
   for( i=0 ; i < argc ; i++ ){
     printf( "argv[%d]=%s¥n" , i , argv[i] );
   }

   return 0;
}



説明を簡単にするために、mainの第三引数は省略した。

このプログラムをコンパイルして実行してみよう。出来上がったプログラムは「cmdargtest.exe」とする。コマンドラインからどのようにこのプログラムを実行するかは省略する。

何もパラメータをつけずに実行すると、以下のような表示結果となる。


argv[0]=cmdargtest.exe



一方、先ほどdirで指定したような、パラメータをつけて実行すると以下のような表示結果となる。


cmdargtest.exe c:¥temp /p /n:o




argv[0]=cmdargtest.exe
argv[1]=c:¥temp
argv[2]=/p
argv[3]=/n:o



プログラムから分かるとおり、main関数の第一引数(argc)には数字の4が、

 argv[0]にはcmdargtest.exe
 argv[1]にはc:¥temp
 argv[2]には/p
 argv[3]には/n:o

がそれぞれ入っている。

ここで、argv[0]に入っている内容は、コマンドプロンプト上でこのコマンドを実行させるために入力した入力文字本体となっている。

さて、このmainの引数、実際Windows環境で実行するときにどのような働きをするのだろうか?それは後ほどゆっくりと解説していく。



3.環境変数とは?



環境変数とはOS自体が持っている変数のことで、システムの属性などが記録されている。

環境変数は通常「変数名=値」の形で保存されている。

最も有名でよく使われる環境変数は恐らく「パス」であろう。今回はパスについての説明は省略するので、疑問に思ったらパソコン用語辞典等で調べていただきたい。

環境変数のパスについて知りたければ、コマンドプロンプトまたはシェルで以下のようにして調べることが出来る。

DOSの場合


echo %PATH%



UNIXの場合


printenv PATH



また、WindowsのGUI上では「マイコンピュータを右クリック」→「プロパティー」-「詳細」-「環境変数」等でも調べることが出来る。

mainの第三引数が環境変数文字列であることは、先程述べた。では、以下のプログラムを実行してみよう。


#include <stdio.h>

int main( int argc , char * argv[] , char * envp[] )
{
   int i;

   for( i=0 ; envp[i] ; i++ )
   {
     printf( "envp[%d] = %s¥n", i , envp[i] );
   }

   return 0;
}



実行結果は自分のコンピュータの内部情報を暴露してしまうので、ここでは省略するが、実行すると環境変数が一覧でコマンドライン上に表示される。


環境変数文字列配列の終端はNULLとなっているため、envp[i]がNULLになればこの条件から抜けることになる。

環境変数にはシステム内の様々な情報が含まれているため、プログラムを実行する際に有効に使用することが出来る。ただし、「変数名=値」の形の文字列で記述されているため、変数名をキーとして検索を行うには多少のプログラムを書く必要があり、この形では多少使いづらい。

それで、個別の環境変数名などが分かっている場合には、stdlib.hで定義されているgetenv関数を使用した方が良いため、mainの引数として環境変数をとって使う場面は少ないかも知れない。


#include <stdio.h>
#include <stdlib.h>

int main( )
{
   int i;

   printf( "path = %s¥n" , getenv( "path" ) );

   return 0;
}



ちなみに、Web上で実行できるプログラムであるCGIにも「環境変数」と言うものがある。コンピュータ用語辞典などでは違うものとして区別して書かれることもあるが、実際は本質は同じであり、WebサーバーがCGIプログラムを呼び出す際に、環境変数に特定の値をセット(ブラウザ種別や相手先IPアドレスなど)してから呼び出して、CGIプログラム側でそれらを活用できるようにしているため、「環境変数」と言う呼び名が残っている。


詳しく知りたければ「CGI」「環境変数」と言うキーワードで検索してみると良い。



4.mainの戻り値はどう使うのか?



mainの戻り値はどのような値を指定すればよいのか?あるいはmainの戻り値は誰が受け取ってどのように使うのか?と言う疑問も出てくる。

 関数の戻り値は関数の呼出元が使用するのに用いられるように、main関数の戻り値もその関数の呼出元が受け取り、呼出元が使用する。

 では、main関数の呼出元とは誰だろうか?mainはプログラムの最初の関数であるから、プログラムの呼出元がすなわちmain関数の戻り値の受け取り先と言うことになる。プログラムの呼出元は、基本的にはOSである(プログラムがバッチファイルや他のプログラムから起動されたときには、そのバッチファイルやプログラムに値が戻ることになる)

関数の戻り値を受け取るDOSのバッチファイルの例を一つ記述する。


rem 削除のテスト
echo off

del temp.txt
if errorlevel 1 (echo "削除に失敗しました") else (echo "削除に成功しました")

echo on



上記ではdelコマンドを使用して「temp.txt」を削除している。次に続く(if~errorlevel~else)はdelの戻り値が1以上ならば「削除に失敗しました」をそれ以外なら「削除に成功しました」と表示するためのコマンドとなる。

 実行すると、temp.txtが同一ディレクトリにある場合は、そのファイルは削除され、「削除に成功しました」と言うメッセージを表示し、ファイルが無いなどの理由でdelコマンドが失敗した場合には「削除に失敗しました」と言うメッセージを表示する。delコマンドが成功した場合に0を、失敗した場合に1以上の値を返す(mainの戻り値がそのようになっている)プログラムなので、このような判断が出来るのである。

 ちなみに、余談となるがdelコマンドはファイルが読み取り専用で削除できなかった場合などはエラーを返さないため、実際にファイルが消えたかどうかを判断するためには以下のようにする必要があるかも知れない。


rem 削除のテスト
echo off

del temp.txt
if errorlevel 1 (goto viewerror) else (goto checkdelete)
goto end

rem 確かに消えているかのチェック
:checkdelete
if exist temp.txt (goto viewerror)
echo "削除に成功しました"
goto end

:viewerror
echo "削除に失敗しました"

:end

echo on



さて、次に他のプログラムから別のプログラムを起動した際の戻り値を見る方法を紹介しよう。Windows環境で以下の2つのプログラムを組んでみる。


/* SimpleCalculator.exe */

#include <stdlib.h>


int main( int argc , char * argv[] )
{
   if( argc != 4 ){
     return 0;
   }

   int a = atoi( argv[1] );
   int b = atoi( argv[3] );

   switch( *argv[2] )
   {
     case '+': return a + b;
     case '-': return a - b;
     case '*': return a * b;
     case '/': return a / b;
   }

   return 0;
}




/* CalcController.exe */
#include <stdio.h>
#include <string.h>
#include <windows.h>

int main( void )
{
   PROCESS_INFORMATION procInfo;
   STARTUPINFO startupInfo;
   ZeroMemory( &startupInfo , sizeof( STARTUPINFO ) );
   startupInfo.cb = sizeof( STARTUPINFO );

   char szCalcParam[ 256 ];
   DWORD dwRet;

   strcpy( szCalcParam , "SimpleCalculator.exe 10000 - 1234" );
   CreateProcess( NULL , szCalcParam , NULL , NULL , FALSE ,
         NORMAL_PRIORITY_CLASS , NULL ,
         NULL , &startupInfo , &procInfo );
   WaitForSingleObject( procInfo.hProcess , INFINITE );
   GetExitCodeProcess( procInfo.hProcess , &dwRet );
   CloseHandle( procInfo.hThread );
   CloseHandle( procInfo.hProcess );
   printf( "%s => %d¥n" , szCalcParam , (int)dwRet );

   return 0;
}



作成したプログラムをそれぞれ「SimpleCalculator.exe」「CalcController.exe」とし、ふたつを共に同じディレクトリに入れて(もちろんパスが通っていれば同一ディレクトリでなくても良い)、カレントディレクトリをそのディレクトリとし、「CalcController.exe」を実行する。

 SimpleCalculator.exeは'10000' '-' '1234'と言う3つの引数で呼び出されるため、10000から1234を減じた8766を戻り値としてmain関数を終了する。

 CalcController.exeではWaitForSingleObjectを使用してSimpleCalculator.exeの終了を待った後、GetExitCodeProcessを使用して、SimpleCalculator.exeの戻り値を取得し、printfで表示させている。

 このプログラムを実行した際の実行結果は以下の通りとなる。


SimpleCalculator.exe 10000 - 1234 => 8766



 勿論、int型しか返せないmainの戻り値で計算プログラムを実装しようと言うのは現実的なことではないが、上記の事からmainの戻り値がどのように活用できるかを知ることが出来る。

 実行したプログラムが成功したのか失敗したのかのみならず、なぜ失敗したのかなどのエラーコードを織り交ぜることも出来るし、成功した場合のステータスなどを戻り値に持たせて呼出側のプログラムで活用することも出来るだろう。

 上記はUNIXのシェルでも有効である。makeファイルなどを作る場合にも、makeの途中で成功したか失敗したかなどは、全てコマンドの戻り値(つまりmainの戻り値)によって判断されている(makeファイルはコマンドの戻り値が0で無ければ失敗と見なす)。プログラムを組む際にはmainの戻り値も意識して組むようにするべきだろう。



5.Windowsにおけるmainの引数



コマンドプロンプトでのmainの引数は理解できたとして、GUI上ではmainの引数はどのように利用されているのだろうか?我々がWindows上のプログラムを利用している上で殆ど意識していないがmainへの引数は頻繁に使われている。

以下のソースを参照していただきたい。


/*ViewArgs.exe*/

#include <string.h>
#include <windows.h>

int main( int argc , char * argv[] )
{
   char szBuffer[1024] = "";
   int i;

   for( i=0 ; i<argc ; i++ )
   {
     if( i!= 0 ){
       strcat( szBuffer , " " );
     }
     strcat( szBuffer , argv[i] );
   }

   MessageBox( NULL , szBuffer , NULL , MB_OK );

   return 0;
}



コンパイルして出来上がった実行ファイル(ViewArgs.exeとする)のアイコンをダブルクリックしてみよう。
メッセージボックスが表示され、実行したプログラムのフルパスが表示される。

次に、何かのファイルをこのアイコンにドラッグしていただきたい。メッセージボックスには実行したプログラムのフルパスとドラッグしたファイルのパスが表示されたことだろう。


次に複数のファイルをこのアイコンにドラッグしてみよう。メッセージボックスには実行したプログラムのフルパスとドラッグした全てのファイルのパスが表示される。

あまり調子に乗って大量にドラッグしてしまうとバッファがオーバーしてしまう(上記プログラムでは1024文字しか表示しないことを前提としているため)ので、やりすぎには注意していただきたい。

ファイルをドラッグしたらそのファイルのフルパスがmainの引数として渡されていることが確認できたところで、次に、適当なファイルを作成し、「test.testdayo」と言うファイル名で保存してみよう。中身は何でも良い。

「拡張子」を「testdayo」にすることだけ忘れないように(エクスプローラの設定によっては「登録している拡張子は表示しない」と言う設定になっている場合があり、その際には拡張子の変更は容易でないので注意が必要)。

次に出来上がったファイルをダブルクリックしてみよう。(Vistaを使用している場合は、一度拡張子とソフトウェアの関連づけを行った場合は削除するのが困難(レジストリを直接操作する必要が出てくる)ので、注意が必要)。当然、プログラムが何も関連付いていないので、プログラムの選択ダイアログが表示される。
 
ここで、先ほど作成したプログラム(ViewArgx.exe)をこのファイルを開くプログラムとして登録する。こうすることで、拡張子とファイルの関連付けが出来る。「test.testdayo」ファイルをダブルクリックしてプログラム(ViewArgx.exe)を起動すると、mainの引数として、「test.testdayo」ファイルのフルパスが送られていることが分かる。

この他、ファイルによってはメニューを右クリックしたときに「印刷」とか「プレビュー」とか表示されるものがある。これはレジストリ内にそのファイルに対して「印刷」が実行されたときの動作、「プレビュー」が実行されたときの動作が定義されているため可能なのであるが、これも突き詰めていけばどのプログラムをどう言った引数で実行するかに行き着く(一般的には「印刷」は該当プログラムに「/p」と言う引数をつけて実行することで実装されている)。

 このように、WindowsのGUI環境においてもコマンドライン引数は重要な役割を担っていることを知ることが出来る。



6.WinMainで引数を得るには



WindowsAPIベースでプログラムを作成する場合、最初に実行される関数はmain関数ではなく、WinMain関数である。

では、このWinMain関数内で、渡されたコマンドライン引数を取得するにはどうすればよいか?WinMain関数もコマンドラインを引数として持っているが、多少取り扱いが厄介である。
 
まずは、以下のプログラムを実行していただきたい


#include <windows.h>

int WINAPI WinMain( HINSTANCE hInstance , HINSTANCE hPrevInstance , LPSTR lpCmdLine , int nShowCmd )
{
   MessageBox( NULL , lpCmdLine , NULL , MB_OK );
   return 0;
}



プログラムを単純に実行すると、何も書かれていない[OK]ボタンが付いたメッセージボックスが表示されるはずである。次に、出来上がった実行ファイル(WinMainArgs.exeとする)に適当なファイルをドラッグしてみよう。

lpCmdLine引数にコマンドラインの引数が渡され、メッセージボックスにファイルのフルパスが表示されたはずである。

このようにしてコマンドライン引数を取得することは可能なのではあるが、複数のコマンドライン引数を取るプログラムを作成するときには注意が必要となる。と言うのは、複数のコマンドライン引数を指定するとき、通常はスペース区切りとなるのであるが、ファイル名などで半角スペースが入った文字列を引数として指定する場合には、その引数をダブルクォートで囲むという規則があるためである。

例えば、デスクトップ上の2つのファイルをプログラム上にドラッグした場合には、lpCmdLineは以下のような内容となる可能性があるだろう。


“c:¥Documents and Settings¥hogehoge¥デスクトップ¥hogehoge.txt”
“c:¥Documents and Settings¥hogehoge/デスクトップ¥hogehoge2.txt”



このような引数を1番目の引数と2番目の引数に分けて使用しようとした場合、strtokやstrchrを使用して分割することは出来ない。単純な半角スペース区切りではないためである。

C言語になれた人なら、迷わず探さず引数分割プログラムを組むところだが、実はこのためのヘルパーもキチンと用意されている(私はそれを知らずに迷わず探さず作ったことが何度もあるが...)

以下のプログラムを参照していただきたい。


#include <windows.h>
#include <tchar.h>

int WINAPI WinMain( HINSTANCE hInstance , HINSTANCE hPrevInstance , LPSTR lpCmdLine , int nShowCmd )
{
  char szBuff[1024] = "";

   for( int i=0 ; i<__argc ; i++ )
   {
     if( i!=0 )
     {
       strcat( szBuff , " " );
     }
     strcat( szBuff , "¥"" );
     strcat( szBuff , __argv[i] );
     strcat( szBuff , "¥"" );
   }
   MessageBox( NULL , szBuff , NULL , MB_OK );

   return 0;
}



2行目でtchar.hをインクルードすることによって、変数「__argc」(argcの前にアンダーバーが2つ)「__argv」(argvの前にアンダーバーが2つ)が使用できるようになる。

この2変数はmain関数のargcおよびargvと全く同じ使い勝手であるため、lpCmdLineをわざわざ展開するプログラムを書かなくても希望の動作が行えるようになるだろう。



7.まとめ



今回は第一回という事で、まさにプログラムの最初の部分で実行されるmain関数に関して長々と語ってみた。

最近のウィンドウのプログラムにおいてはあまり使用することも無いかも知れないmainの引数及び戻り値であるが、そう言ったものが有ること、どのように使うかと言うことが分かっていればそれなりにプログラムの幅が広がるのではないかと思われる。

本来であればLinux系における使い方にももう少し触れておく必要があるかと思われるので、必要が有ればコメントいただきたい。

なお、多少の資料は調べているものの、内容に関しては当方の見識を超えないものであるため、技術的に間違った情報がある場合はご容赦及びご指導いただきたいと思う。

ソフトウェア開発者のブログ開設します

SPOONsoftwareのソフトウェア開発者のブログ開設します。技術的なこと、どうでも良いこと、色々書いていこうと思います。

特にC言語についてはかなり専門的に突っ込んだ内容まで解説していくつもりです。