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.まとめ



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

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

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