2009年2月24日火曜日

第三回:ポインタ(2)実用編1-関数とポインタ

 ポインタが実際的にどのような機能なのかという点に関しては、第二回で大体説明できたと思う。第三回以降では、ポインタを実際にどのように使うことが出来るか、またはその際、メモリ内はどのような状態になっているのかを説明していこうと思う。第三回は特にポインタがよく使われる場面である、関数の引数としてのポインタの利用の説明を行う。

1.序章


 ポインタがよく使われる場面として、関数とのやりとりが上げられる。文字列の取得を行ったり、関数にデータを渡したりする際にも度々ポインタを使用することになるだろう。ただ、関数とのインタフェースとしてポインタを使用したときに、何故かは分からないがうまく行かないと言う状況に直面することもある。こういった状況を理解するためにも、関数の引数にポインタを使用した後、どのような動きになるかを理解しておくことは重要である。

2.関数への値渡しとポインタ渡し


 C言語の場合、関数への変数の渡し方には値渡しとポインタ渡しと言う2種類の方法がある(C++ではこれに加えて参照渡しなるものもある)。
 2つの引数の渡し方の説明をする場合、一般的な説明としては、値渡しは「関数内でなされた変数への変更を呼出元に反映させない場合」に使用し、ポインタ渡しは「関数内でなされた変数への変更を呼出元に反映させる」場合に使用する。と言うものだろう。
 この説明は正しいが、「何故?」と言う部分が抜けており、本質的な説明を行っていないため、ポインタの分かりにくさを増す結果になってしまう。第二回で説明したポインタの本質が理解できていれば、上記の関数への引数の渡し方に関しする説明は理解しやすいものとなる。

 まずは、以下の2つのソースを見ていただきたい。


/* test1.c */

#include

void SetData( int a , int b );

int main()
{
   int a = 0 , b = 0;

   printf( "a = %d , b = %d\n" , a , b );
   SetData( a , b );
   printf( "a = %d , b = %d\n" , a , b );

   return 0;
}

void SetData( int a , int b )
{
   a = 10;
   b = 20;
}




/* test2.c */

#include

void SetData( int *pa , int *pb );

int main()
{
   int a = 0 , b = 0;

   printf( "a = %d , b = %d\n" , a , b );
   SetData( &a , &b );
   printf( "a = %d , b = %d\n" , a , b );

   return 0;
}

void SetData( int *pa , int *pb )
{
   *pa = 10;
   *pb = 20;
}



 実行すると以下の通り表示される


test1.cの実行結果
a = 0 , b = 0
a = 0 , b = 0




test2.cの実行結果
a = 0 , b = 0
a = 10 , b = 20



 test1.cは関数に対して変数を値渡ししたもの、test2.cは関数に対してポインタ渡ししたものである。test2.cはmain関数内の変数aおよびbがSetDataでセットされた値に変更されていることが分かる。
 何故このような動きになるのだろうか?
 普段プログラマーが意識することはないだろうが、関数を呼び出した場合にはコンピュータは以下のような動きをする。

1.呼び出された関数が使用する領域(スタック領域またはコールスタック)を確保
2.引数として渡された変数の格納領域をスタック領域内に確保
3.引数として渡された変数の値を②で確保した領域にコピー

 この動きは、引数が値渡しであろうが、ポインタ渡しであろうが、変わることはない。3を見て「ん?」と感じた方もおられると思う。値渡しだろうが、ポインタ渡しであろうが、引数領域に値をコピーすると言う動きに変化はないのだ。

 では、上記の1~3の動きをtest1.cおよびtest2.cのソースで説明してみよう。

 まずは、test1.cの説明から。
 main関数を実行すると、main関数のスタック領域が確保される。変数aおよびbは局所変数であるため、このスタック領域内にデータ領域が確保される。変数aおよびbの値は0となる。



 次に、SetDataが引数aおよび、引数bを伴って呼び出される。SetDataは関数用のスタック領域を確保し、int型の二つの変数a,b用の領域をスタック領域内に確保する。次いで、引数で渡された数値をコピーする



 SetData関数内で、a=10;b=20が実行される。



 SetData関数内で変更を行っている変数はmainの変数とは別物であり、そこで行われた変更は当然main関数内の変数に反映されないことが理解できる。

 次に、test2.cを実行した際の、メモリの動きを確認してみよう。
 まず、main関数を実行した際のスタック領域の確保および変数a,bの領域の確保に関しては、test1.cと同様である。
 次に、SetData関数を呼び出すと、SetData関数用のスタック領域が確保され、ここにポインタ変数であるpaおよびpbが確保される。
 mainの中で、


SetData( &a , &b );



 と言う風にSetDataを呼んでいるので、渡される引数はa,bの確保領域のメモリ番地となる。



 SetData関数内では


*pa = 10;
*pb = 20;



 と言う処理が行われる。これは、paのメモリ番地が指す値の中身を10に、pbのメモリ番地が指す値の中身を20にすると言う意味であるから、結果的にmain関数内のaおよびbの中身(値)が書き変わることになる。



 結局は、SetDataに渡している引数(pa、pb)がアドレスであり、pa、pbにではなく、そのアドレスが指し示す先に対して、内容の変更を行っているため、結果的にmain関数の値が書き変わっているだけであり、ポインタ渡しと言っても何か変わったことを行っているのではないことが理解できる。

3.まとめ


 今回は、関数にポインタを渡すときのメモリ内の動きに関して最も単純なケースを用いて説明を行った。今後、配列や構造体のポインタの動きを説明してから、より複雑なポインタ渡しに関しても説明していこうと思う。

2009年2月10日火曜日

(箸休め)時間管理ルーチン・シリアル値の特異点

(箸休め)時間管理ルーチン・シリアル値の特異点

 今日、C言語の時間管理ルーチンをいじってて気づいた点があった。
 シリアル値の頭が1234になってる!
 だから何?と言う声が聞こえそうなので、もう少し詳しく説明しておこう。

 C言語の時間管理ルーチンはtime_tというシリアル値を用いて行っている。
 UNIXの仕様に端を発しているのだが、このシリアル値は多くの処理系では1970年1月1日0時0分0秒(標準時)からの経過秒数である。シリアル値はtime_tという型に入れて使用するが、この型が32ビット負号無し整数に割り当てられている処理系であれば、2038年1月19日3時14分7秒(標準時)に桁あふれを起こしてしまうため、使用できなくなってしまう。これは2038年問題と言われ結構深刻な問題だが、まだ先の話であるため「コンパイラが何とかしてくれるんじゃない?」ぐらいの感じで流されている(が、実際は一時騒がれた2000年問題よりも深刻という話もある)。

参考(2038年問題):
Wikipedia:2038年問題

 で、今日プログラムをいじってて、シリアル値の頭が1234になっていることに気づいたので、調べてみたところ


2009年2月13日23時31分30秒(標準時)
2009年2月14日8時31分30秒(日本時間)


 で、シリアル値が


1234567890


 になる事が分かった!!

 一応、確認したソースを上げておく


#include <stdio.h>
#include <time.h>

int main()
{
      time_t timeRen = 1234567890;
      struct tm tmUTC;
      struct tm tmJPN;

      tmJPN = *localtime( &timeRen );
      tmUTC = *gmtime( &timeRen );

      printf( "(UTC)%d/%d/%d %d:%d:%d¥n"
            , tmUTC.tm_year+1900 , tmUTC.tm_mon+1 , tmUTC.tm_mday
            , tmUTC.tm_hour , tmUTC.tm_min , tmUTC.tm_sec );

      printf( "(JPN)%d/%d/%d %d:%d:%d¥n"
            , tmJPN.tm_year+1900 , tmJPN.tm_mon+1 , tmJPN.tm_mday
            , tmJPN.tm_hour , tmJPN.tm_min , tmJPN.tm_sec );

      return 0;
}


 テンション上がるよね!!
 え?俺だけ?理解できない?ま、確かにそうかも...

 まあ、シリアル値に関しては閏年は計算に入れているけど、たまに入る閏秒は計算に入れていないから、実際の経過秒数とは若干違うんだけど、1970年1月1日からひたすら刻んできた時計が、「1234567890」って言う連番になる瞬間を見届けるのもいいのではないでしょうか?

参考(閏秒):
Wikipedia:閏秒

追記
この記事を書いた後、色々調べていたら、既出でしたね(当然と言えば当然ですが)
Wikipedia:time_tパーティー

2009年2月2日月曜日

第二回:ポインタ(1)ポインタを理解する

ソフトウェア開発者コラム、第二回はポインタに関するあれこれを扱っていこうと思う。

ポインタと聞いて苦手意識を持つ人は少なくない。このコラムが苦手意識を払拭するための一助となれば幸いであるが、もしかしたら苦手意識を増してしまう人もいると思われるので、読むかどうかは自己責任でお願いしたい。

ポインタを概念的に理解したい人と言うよりはポインタとはこういうもので有るというしっかりとした定義を知りたい人向けの説明を行っていこうと思う。

1.序章



「ポインタ」の仕組みを考えた人間は天才だとは思うが、天才の考えが一般人に分かりにくいこともまた真理である。

 実際、ポインタの神髄を理解できるかどうかがプログラマの資質に関連してくると思う。また、最近のJAVAやVB等でも表面的にはその姿を隠しながらもポインタの概念が根強く残ってしまっているため、理解しづらい「規則」が出来上がってしまっているのも確かである。

 「ポインタ」を分かりやすく説明するため、様々な試みがなされてきた。箱にものを入れてその番号を渡すとか、ロッカーにものを入れてその鍵を渡す。等の表現で、現実世界の事柄での説明を通してポインタを理解しようとする試みが多くなされている。

ただ、ポインタの概念ではなく、幅や深さを解説する上ではそう言った例えでの説明では破綻を起こす場合が少なくないため、今回はあえて直接的な表現での説明を試み、その後、実際にポインタを使用した様々な話題に触れていこうと思う。


2.変数とポインタに関する直接的説明



プログラム上で値を格納するために使用される識別子を「変数」と呼ぶ。一般的なプログラム言語の場合、変数は「型」を持つ。

 では、変数は実際にどのように格納されているのだろうか?

 ここで、情報処理の初期で勉強した、2進数や16進数の知識が必要になってくる。記憶に無い人は勉強し直して貰いたい。

 コンピュータが数値を保存する場合、データは全てビット単位で保存される。つまり、2進数で保存される。例えば、10進数の123は2進数で表すと


1111011



となる。

これは、コンピュータには判断しやすいかも知れないが、人間が見てもよく分からないので、通常はこれを16進数で表現する。2進数の4桁(4ビット)を16進数だと1桁として扱うことから、2進数から直接変換するのにやりやすいものとしてよく使われる。

16進数で10進数の123を表すと


7B



となる。

 この16進数で2桁(つまり8ビット)を1バイトと呼ぶ。通常はデータはバイト単位で扱うのが容易であるので、一般的な説明はバイト単位で行うことにする。

 この前提条件を元に変数がどのようにコンピュータ上で管理されているかを見ていこう。一番分かりやすいところで、「整数」について扱う。

 1バイトの領域を使って整数を扱うとしたら、どの位の範囲の整数を扱うことが出来るだろうか?

 16進数の00~16進数のFFまでが扱えることになるので、単純に考えて10進数で0~255までが扱えることになる。整数は正の整数だけでなく負の整数も扱わなくてはならないため、負号付き整数の場合には頭のビットが1のものは負の数と見なし、1バイト整数では10進数で-128~127が扱えるようになっている。

日常生活で使う上で-128~127の範囲の整数だけでは子供のお小遣いすら計算できないため、通常は整数を扱う場合には2バイト整数、4バイト整数とバイトを増やして扱える範囲を広げて使うことになるだろう。
 
2バイト整数では-32,768~32,768までが
 4バイト整数では-2,147,483,648~2,147,483,647までが扱える。
 最近はニーズに合わせて8バイト整数などを用意しているコンパイラもある。

 10進数の1,234,567,890を16進数で表現すると


49 96 02 D2   (便宜上1バイト毎に区切ってある)



となる。

 C言語でのint型が何バイトかはコンパイラによって決定されている。DOS時代ではMS-CやBorlandCのint型は2バイトだったが、Windows時代になってからはintで変数を宣言すると4バイトが確保されているように記憶している(恐らくOSが32ビットとなり、CPUも32ビットCPUとなったためと思われる)。今後、int型について触れられる時には4バイトの整数型であるものとする。

 さて、main関数内で以下のように定義したとしよう。


int main()
{
    int a = 1234567890;

    return 0;
}



main関数が実行される際、main関数が使用する領域がメモリ内に作成される。main関数に限らず他の関数が実行される際にも、実行されるたびにその関数用の領域がメモリ内に確保されるが、この領域のことを「スタック領域」とか「コールスタック」と呼ぶ。

 関数内で定義された変数(上記の場合は「a」)はこのスタック領域内にその変数用の格納領域を確保する。先ほどの前提条件で述べたとおり、intは4バイトで有るので、スタック領域内のどこかに4バイト分のメモリが確保され、その中に10進数の1234567890が格納されることになる。

 イメージとしては以下のようになるであろう。



上記の図はスタック領域のイメージである。小さな枠1つが1バイト分のメモリ領域を表している。
 
メモリ内の場所を表現するために、メモリには1バイト毎にメモリ番地(アドレスとも呼ぶ)と言う通し番号がついている。上記の図で左側に書いている数字がメモリ番地の2桁目以降を表しており、上側に書いている数字がメモリ番地の1桁目を表している。

 上記の例では、変数aはメモリ番地??????33~??????36に確保されたと言うことが出来る。

 10進数1234567890を16進数に変換すると49 96 02 D2 となることは前に述べた。上記の例では、下位バイトから順にメモリ内に格納してあり、見た目逆転しているように見える。

Intel系のCPUでは「リトルエンディアン」を採用しているため、このように下位バイトからの書き込みが行われているのであるが、この部分は深く触れないようにしておく。ネットワーク関係の話題でいずれお話しすることになると思う。

 さて、話題を元に戻そう。上記のように変数aがメモリの??????33番地を先頭に4バイトで格納されることが分かった。このメモリ番地「??????33」が実はポインタそのものとなる。

つまり、変数を使用するには、変数が使用する領域をメモリ内に確保する必要が出てくるが、この確保されたメモリの先頭のメモリ番地の事をポインタと呼んでいるのである。

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


int main()
{
    int a = 1234567890;
    int *p = &a;

    return 0;
}



上記の例では、int型のポインタ変数pを定義し、ここに&aを代入している。(ここで「int *p」と書いているのが、しばしば勘違いと複雑さを増す原因になることもある。と言うのはポインタの中身を表示する場合にも「*p」と言う記号を使うからだ。ポインタの中身を参照するための「*p」と変数宣言に出てくる「*」は別物であると考えた方がよい。「int *p」はあくまで変数pをint型のポインタを格納するための変数として定義すると言う記号なのである)

 変数の前に&(アンパサンド)をつけると、その変数のメモリ番地を参照できる。ここでは、メモリ番地を格納するためのポインタ変数pを定義し、そこに変数aのメモリ番地を格納している。
 
このとき、メモリ内は以下のようになるであろう。



変数aの格納領域が??????33~??????36に確保され、変数pの格納領域が??????3C~??????3Fに確保されたものとする。

 変数pは&aの値になる。つまり、変数aが格納した領域の先頭番地となるので、この部分には??????33が入ることになる。ポインタと言えどただの変数である。代入すればその値が入るのは当然のことで、何か特別なものではない。

 さて、上記の様に見ていくと、ポインタ変数pも領域を確保しているわけだから、そのメモリ番地も取得できるのでは?と言う気になる。確かに出来る。ソースで表すと以下のようになる。


int main()
{
    int a = 1234567890;
    int *p = &a;
    int **pp = &p;

    return 0;
}



ここで「int **pp」と変数ppを定義しているが、これは、int型のポインタ変数(int*)のポインタを格納するための変数で有ると言う意味になる。

 このように定義した場合、メモリ内は以下のようになるであろう。



となると、変数ppのポインタも取得できるのではないだろうか...きりがないので、この辺りで止めておこう。

 ここで扱いたいのは、「ポインタ」と難しく言っているが、実は値を格納するための変数に過ぎない。と言うことだ。

 一応、ここまでの説明を終えるに当たり、動作するソースを紹介しておこう。


/* PointerTest.c */

#include <stdio.h>

main()
{
    int a = 1234567890;
    int *p = &a;
    int **pp = &p;

    printf( "a = 0x%08x¥n" , a );
    printf( "&a = p = %p¥n" , p );
    printf( "&p = pp = %p¥n" , pp );
    printf( "&pp = %p¥n" , &pp );

    return 0;
}



実行した結果は以下の通りとなるが、これは各コンピュータによって表示される値はまちまちとなると思われる。


a = 0x499602d2
&a = p = 0xbfbfeccc
&p = pp = 0xbfbfecc8
&pp = 0xbfbfecc4




3.ポインタを利用した簡単な例



ポインタを利用してどのようなことが出来るか、そうした場合の内部的な動きはどうなっているのかを説明していこうと思う。ポインタの実用的な使い方については、次回に回すとして、以下のような単純な例について考えてみよう。


#include <stdio.h>

int main()
{
    int a = 100;
    int *p = &a;

    printf( "a = %d¥n" , a );
    *p += 10;
    printf( "a = %d¥n" , a );

    return 0;
}



上記プログラムの実行結果は以下の通りとなる。


a = 100
a = 110



プログラムを詳しく見てみよう。


int a=100;



と言う文で、変数aを定義し、その中身を100にしている。
 
次に


int *p = &a;



と言う文で、変数aのメモリ番地をポインタ変数pに格納している。

 printfを使ってaを表示し、*pの値を増分してから、再びprintfでaの値を表示している。このプログラムは変数aを直接操作していなくても、変数aの値が変動することを確認することを目的としている。

 そこで、注目できるのは


*p += 10;



と言う部分である。

 pがポインタ変数である場合、*pは「ポインタ変数pが指し示すアドレスにある変数の中身」と言う意味になる。それで、この例においては、*pを操作することは変数aを直接操作することと同じ意味になる。

4.まとめ



今回はポインタの基本に関する説明を、メモリ内の話も含め、直接的に説明した。ポインタとはこういうものと言う程度の説明であるので、これがどのように役立つかは今回の説明だけではピンと来ないと思う。

 次回はポインタを実用的に使用する方法と、ポインタのこの仕組みでなぜそのような動きが可能なのかを述べていく。

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

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言語についてはかなり専門的に突っ込んだ内容まで解説していくつもりです。