- - PR -
C の localtime() の引数は混乱を招く。
投稿者 | 投稿内容 | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
投稿日時: 2004-11-11 16:40
C++は最初、Cのプリプロセッサで記述されていた。
| ||||||||||||||||||||
|
投稿日時: 2005-01-14 02:52
> 確かに、大半のバグはコードを書く側に問題があるのでしょう。
>しかし、皆さん関数の引数に '*' 付き変数があったとして、果たしてそれが >ある型のポインタ変数を指すものなのか、それともある型の実体の領域のアドレスを指す >のか瞬時に見分けられますか? ん?仮引数に*付き変数があったとしたら、それはきっとポインタなんでしょう。 それはつまり、呼び出す側に対して「こには(ある型の)アドレスを入れてね」と言っているワケです。例えば、「func(char *p_a)」なる引数の宣言は、「char型のアドレスをひとつ下さい」と言っていて、そして、暗に「そのアドレスにアクセスさせてもらいますよ」と言っているわけです。だとしたら、呼び出す側は「じゃあここの部分にアクセスして下さい」といってどこかのアドレスを渡すだけだと思います。 >ある型のポインタ変数を指すものなのか、それともある型の実体の領域のアドレスを指す >のか瞬時に見分けられますか? 良くわからないのですが、関数の仮引数にポインタが宣言されていた場合に、呼ぶ側から関数に渡せるのはアドレスなんですから、関数側はそのアドレスを信じて処理を進めるだけですし、呼ぶ側は関数にどこかのアドレスを教えてあげるだけです。 間違っていたら失礼なんですが、コブラさんは、 ・Cの関数の呼び出しが値渡しであるということ。 ・仮引数のポインタには、呼び側からはアドレスが渡されるということ。 の2点を理解されていない様に見えます。そしてそれゆえ、 ・関数呼び出しの際に、呼び出し側でポインタ変数を記述すると、ポインタ変数自身が渡される場合がある と誤解しているのではないでしょうか?? Cでは関数の呼び出しは値渡しです。変数が渡るという事はありません。なので、話題のlocaltime()に関して言うと、 struct tm *p_tm; time_t *p_time; //←初期化していないので謎の値 p_tm = localtime( p_time ); と呼び出した場合に何が起こるかというと、localtime(const time_t *timer) のポインタtimerには、謎の値が書き込まれるということです。そして、localtime()の内部では、その謎の値をアドレスだと考えて処理を行います。別に謎の値を渡すことは、Cの文法上誤りではありません。実際上はおそらくバグでしょうが。 #ポインタを使って縦横無尽にメモリにアクセスできるのはCに備わった大きなパワーです。そして #そのパワーゆえに使うものは注意を払わなくてはなりません。 関数を呼び出す際に、引数として初期化しない変数を記述すると、コンパイルエラーにはならないがバグるというのは、別にポインタに限った難しい話ではなくて、↓のような激しく単純な関数についても同様です int plus(int a, int b){ return a + b; } この関数のプロトタイプはきっと int plus(int a, int b); でしょう。(const演算子省略^^;)このプロトタイプだけを見て、「int型の変数を二つ渡せば良いんだな!」と考えてこんな記述をしたとしたらどうでしょう。 void hoge(void){ int x,y,z; //初期化しない z = plus(x, y); printf("%d\n", z); } zがどんな値になるかは不明です。x,yが確保された領域のメモリの状況次第で、その二つを足した値がzに入るでしょうが、おそらくはバグでしょう。ポインタと違う点は、おかしなアドレスにアクセスしないので実行時にもエラーにはならない点です。ちゃんと結果も表示されます。なので、ポインタの時より発見しにくい分、より深刻なバグと言えるでしょう。コンパイルももちろん通ります。ただ、オプションによっては「初期化しないで使ってるよ」のwarningが出るでしょう。 ただ、不注意ならともかく、意図してこんな使い方をしたとしたら、おかしいですよね!?意味が無いと思いません?普通はこんなことはしないで、足し算したんなら、二つの値を確定させてから呼び出しますよね。 int x = 0; int y = 1; int z; z = plus(x, y); もしくは、 int z; z = plus(0, 1); こうすれば、zには0+1の結果1が格納されるでしょう。 >まぁ、Cの標準関数に実体の無いポインタ変数だけ渡すなど、殆どあり得ない、と判って > はいても、私はいまだにできないでいる (プ ここの「ポインタ変数だけ渡す」という表現が引っかかります。上記で書いたように、Cは値渡ししかできないので、「ポインタ変数」を「渡す」ことはできません。呼ぶ時に関数の引数に「ポインタ変数」を書いたとしたら、渡るのは「ポインタ変数自身」ではなくて、そのポインタ変数に書かれている「値」です。なので「ポインタ変数」が初期化されていなければ、「謎の値」が渡ることになりますが、それは上記で定義したplus関数を呼び出す例でも見たとおり、別にポインタに限った話ではありません。 つまり、第一に「Cでは関数の呼び出しは値渡しで行われるのでポインタ変数が渡ることはない」。第二に「関数側では仮引数でポインタとして宣言されている場合、そこにはアドレスが渡されてくることを期待しているが、呼び側でどのような形式で渡すかは問わない」つまり、適当な定数を書こうが、変数に&演算子を作用させてアドレスを表現しようが、配列名を書いて配列の先頭アドレスを表現しようが、ポインタを書こうが、何でもOKなワケです。呼び出された関数側では仮引数(ポインタ)に代入された値をアドレスと解釈しますが、それが正当なものか(ちゃんと確保された領域なのか。アクセスしても問題ないか)を調べる手段はありません。逆にどんな値でも無邪気に信じるわけです。つまり、呼び出し側は責任を持ったアドレスを関数に渡してあげる必要があります。 なので。。。 > 最近では特に、この localtime()。 > > struct tm *localtime( const time_t *timer ); > > こんな風にプロトタイプ宣言されていたら、間違いなくポインタ変数渡してしまう。 この記述は妙です。まず、「ポインタ変数渡してしまう」の意味が「ポインタ変数自身を渡す」つまり参照渡しの意味だとしたら、それは単純に間違いです。Cでは参照渡しはできません。そうではなくて、「ポインタ変数を引数に記述する」の意味だとしたらそれは全然問題ありません。いいんじゃないでしょうか。そのどちらでもなく、「初期化していないポインタ変数を引数に記述する」という意味だとしたら、それは文法上間違いではないにせよ、きっとバグります。「適当なアドレスを渡すと、関数はその適当なアドレスにアクセスする。」 からです。これはポインタに限らず当たり前の話で、例えば、次の記述は妙だとは思いませんか? ---------------------------------------------------------------------------- 最近では特に、この plus()。 int plus(int a, int b); こんな風にプロトタイプ宣言されていたら、間違いなくint型変数渡してしまう。 ---------------------------------------------------------------------------- なんか妙でしょう?さっきの解説を踏襲すると、 まず、「int型変数渡してしまう」の意味が「int型変数自身を渡す」つまり参照渡しの意味だとしたら、それは単純に間違いです。Cでは参照渡しはできません。そうではなくて、「int型変数を引数に記述する」の意味だとしたらそれは全然問題ありません。いいんじゃないでしょうか。そのどちらでもなく、「初期化していないint型変数を引数に記述する」という意味だとしたら、それは文法上間違いではないにせよ、きっとバグります。「適当な値を渡すと、関数はその適当な値で計算する。」 ポインタじゃないから当たり前ですかね?では、次の例はどうでしょう?plusのポインタバージョンです。引数は、整数型へのポインタを期待しています。もちろんポインタにする積極的な意味は全くありません^^; ---------------------------------------------------------------------------- 最近では特に、このplus()。 int plus( int *p_a, int *p_b); こんな風にプロトタイプ宣言されてたら、間違いなくポインタ変数渡してしまう。 ---------------------------------------------------------------------------- これ、妙だと思いません? > 実体のアドレスかただのポインタ変数か、明確になっていないところに落とし穴がある 何度も繰り返しになりますが、ポインタ型の仮引数に代入されるのは、呼び側で記述したアドレスです。そして、呼び側でどのような記述がなされたのかは、関数側では判りません。初期化していないポインタを書いて謎の値が渡ってくるのか、ある変数に&演算子を作用させてその変数のアドレスが渡ってくるのか、関数側ではわからないと言うことです。そして、受け取る関数の側では、その値の正当性を判断できないので、全面的に信用します、呼び側で責任を持って設定してください。ということです。これはplus(int a, int b )関数で、aやbの値について、それがちゃんと設定されたものか否か、plus側では全然判らないのとまったく同じ理屈です。 >ような気がしますが、戻り値だけはポインタで来るんですな、これが。 これはまた別の話題ですね。返り値の型がポインタであるということと、引数がポインタであるということはまったく無関係ですが、この戻り値のポインタがどこを指しているか?を考えると、ちょっと嫌な感じがしますよね。 > こういうのがバグ発生の確率を上げてしもてるように思いますが、皆さんどうやって >見分けておられるのでしょうか? そういうワケなので、おそらく別に見分ける必要はなくて、皆さん、渡したいアドレスを渡しているだけだと思いますよ。この類のバグを盛り込まないようにするためには、コブラさんの場合は、まず関数の呼び出しとポインタに関する正しい知識を身につけること。加えて、一般的な話ですが、コンパイラのwarningレベルを上げて、変数の初期化漏れを無くすようにすること。この二点を守ればOKだと思います。 あと、Solarisでエラーにならないのは、「Solarisの処理系の方が、プログラマを信頼しているから」もしくは「Solarisの処理系は、面倒なので、深刻じゃない範囲においてはメモリのアクセス制限をしていないから」もしくは「スタックの積み方が上手だから???」だと思いますよ(?)。 。。。さて、それにしても判らなかったのは、なぜ、localtime()の引数に初期化しないポインタ変数を書いてしまったのか?そしてなぜ、「それは関数のマニュアルの記述が不十分だからだ!」と考えてしまったのか?ということでした。いろいろ書いているうちになんとなく判ってきました。以下は推察です。 1.localtime()なる関数を使う必要に迫られた 2.man localtimeしてみた 3.struct tm *localtime( const time_t *timer ); と書いてあった 4.time_t *timerって何だ?よくわからんけど、要はtime_t型へのポインタを渡せば良いんだな 5.コーディングしてみた struct tm *p_tm; time_t *timer; p_tm = localtime(timer); 6.コンパイル。。。OK 7.実行。。。動いた! 8.しばらくして。。 9.バグ発見!(もしくは、異常終了した) 10.調査すると、localtime()には、あるtime_t型変数のアドレスを渡す必要があることが判明した。 11.プロトタイプの記述が不十分なのが悪いと思った。 12.状況を改善すべく、 @IT会議室 > Linux Square 会議室 に投稿した。 ですが、これ、妙ですよね!?例えば 1.plus()なる関数を使う必要に迫られた 2.plus()のプロトタイプ宣言を見てみた 3.int plus(int a, int b);と書いてあった 4.int aとint bって何だ?よくわからんけど、要はint型の変数を渡せば良いんだな 5.コーディングしてみた int x, y, z; z = plus(x, y); 6.コンパイル。。。OK 7.実行。。。動いた! 8.しばらくして。。 9.バグ発見!(異常終了は多分しない) 10.調査すると、plus()には、ある足し算したいint型整数の値を渡す必要があることが判明した。 11.プロトタイプの記述が不十分なのが悪いと思った。 12.状況を改善すべく、 @IT会議室 > Linux Square 会議室 に投稿した。 って妙でしょう?plusを他の関数に変えてもやっぱり妙です。 長々と失礼しました。。。 _________________ | ||||||||||||||||||||
|
投稿日時: 2005-01-14 10:08
>・関数呼び出しの際に、呼び出し側でポインタ変数を記述すると、ポインタ変数自身が
> 渡される場合がある Call By Reference があるから、元の変数自身のアドレスは渡るでしょう。 >ここの「ポインタ変数だけ渡す」という表現が引っかかります。上記で書いたように、Cは >値渡ししかできないので、「ポインタ変数」を「渡す」ことはできません。 こう書いてる傍から、
実引数に p_time というポインタ変数を渡しておるではないですか? 後、本当の問題はエラーにすべき処でエラーが出ない事で、原因が潜在化してしまい不確定 要素が増えてしまう事です。少なくとも RedHat ではちゃんとエラーが出た。 改善の要望はその点です。 | ||||||||||||||||||||
|
投稿日時: 2005-01-14 11:48
・・・ガーーン!!!CではCall By Referenceは「ありません」よ!!C++ではCall By Referenceもありますが、それにしたって関数のプロトタイプを見れば、Call By Value なのか、Call By Referenceの区別はできます。CにせよC++にせよ、
とプロタイプ宣言してあったら、Call By Valueしかあり得ませんし、Cに限定すれば、考えるまでもなく、Call By Valueしかありません。
はい、確かにポインタ変数を渡しています。問題は、この「ポインタ変数を渡す」の意味するところです。CではCall By Valueしかできないので、その事を知っていれば「ポインタ変数を渡す」の意味が、「呼び出し側で記述したポインタ変数に書いてある「値」を、関数側の仮引数で用意するポインタ変数にコピーする」の意味だと了解できます。しかし、コブラさんは、CでCall By Referenceができると思ってらっしゃるようでした(実際そのようですね)。となると「ポインタ変数を渡す」が、「呼び出し側で記述したポインタ変数への「参照」を、関数側に渡す」という意味である可能性がありました。つまり、ポインタ変数p_time自身に、localtime()がアクセス可能になると。だとしたら、それは「できません」と言っているわけです。localtime()は引数がconst宣言されているために実際にはアドレスの指す先を書き換えませんが、もし書き換えられるとしたら、それは引数に記述したポインタ変数p_timeではなくて、p_timeに書いてあったアドレスの領域です(そこがどこかは分かりません)。ポインタ変数p_timeの内容を関数で書き換えて欲しかったら、「p_time自身のアドレス」を関数に渡す必要があります。「p_timeに書かれているアドレス」と「p_time自身のアドレス」の違いは了解されますでしょうか。。。?もしかしたら、そこが誤解を解くキーかも知れません。 繰返しになりますが、Cでは、参照渡しはできません(C++ではできますが、localtime()のプロトタイプ宣言はC++であってもやはり参照渡しではありません)。なので、Cでは、呼び出し側で書いた引数の値を関数が変更することはありません。呼び元で宣言している変数を、関数側で変更してもらうためには(返り値としてもらう以外には)、その変数のアドレスを関数に渡してあげれば良いです。しかし、これも渡るのは変数そのものではなくて、変数のアドレスが値渡しされているだけです。そして、関数側では、ポインタでそのアドレスを受ける。 おそらく、コブラさんは「ポインタは参照を意味する」「仮引数にポインタが宣言されている」「参照渡しだ」 と考えたのではないでしょうか?? 上記例でlocaltime関数に渡るのは「ポインタ変数p_timeのアドレス」ではなくて、「ポインタ変数p_timeに書いてあるアドレス」です。そして、ポインタ変数p_time自身は、localtimeによって全くアクセスされません。アクセスされるのは「ポインタ変数p_timeに書かれてあったアドレス領域」です。繰返しになりますが。localtime()は、p_timeというポインタ変数のことは全然知りません。どこにあるのかも知りません。分かるのは、仮引数のポインタに書き込まれてきた値(それはポインタ変数p_timeに書いてあったもののコピーです)だけなのです。 この辺は、ポインタと関数呼び出しにまつわる、大変ありがちな誤解ですので(古来より何度も繰り広げられてきましたよね!?>Cプログラマな方)、無理もないかと思いますが。。。何とかご理解いただきたく、繰返しが多い文章(←ダメなプログラム)になってしまいました。私の力不足です。むにぅ。
そういう意味では、Solarisの処理系よりもRedhatの処理系の方が改善されていると言えます。Solarisの処理系では、「まぁ幸いconstで宣言されているアドレスだし、読むだけで別にメモリ破壊するわけじゃないからアクセスしてもいいんじゃない?いちいち範囲チェックするの面倒だし」とチェックをサボっているのでしょう。一方、RedHatの処理系はまじめに「ここはお前の触っていい領域じゃない!帰れ!」という感じでエラーになるんでしょうね。。。(あくまで推測) しかし、もしかしたら、Redhatの処理系は危険なメモリ領域にはアクセスできないのでつまらない。Solarisの処理系の方が危険な香りがして好き。。。な人もいるのかもしれません。 いずれにせよ、前回も述べたとおり、ポインタと関数呼び出しについての正しい知識を持ち、コンパイル時のwarningレベルを上げて初期化忘れを取り除けば、(危険な香りのするSolarisの処理系でも)ほぼこのバグ(未初期化のポインタが指す領域へのアクセス)は取れると思いますよ。 Cとは本来危険な香りのする(それ故パワフルな)言語なのですが、自由と引き換えにやはりちょっと人間に多くを期待しすぎているのかも知れませんね。Redhatの処理系は自由をちょっと売り渡すけど、それによってフレンドリーになっているということだと思います。(←売り渡したのは、多くの人にとっては、そんな自由は要らないよという類の自由でしょうけど^^;)Solarisの処理系が今後どうなるかは分かりませんが。。。フレンドリーさはJavaなどに任せて、Cはひたすら危険な方向でいく。。という風に棲み分けができたりして!? それとも、危険なことはアセンブラでやってよっていう感じですかねぇ。 | ||||||||||||||||||||
|
投稿日時: 2005-01-14 12:36
実引数と仮引数が全く独立しており、何の関係も無くなってしまえば、もう
関数何か造る意味なくなってしまいますから。 やはり、アドレスの指定先(ポインター)は、その指した番地の内容(値)を変えれる 訳ですから。これは、ポインターを参照してますね。 何とか問題を矮小化しようと努力されるのは判らんでもないですが、 自分でも
こういうコードを書いておられるという事は、受け取った側で実引数と 同じアドレス(ポインター)の内容(値)を変えれるという認識があってのコー ディングなのではないですかな? これを、 Call By Reference と言います。 | ||||||||||||||||||||
|
投稿日時: 2005-01-14 12:59
>そういう意味では、Solarisの処理系よりもRedhatの処理系の方が改善されていると言えま
>す。Solarisの処理系では、「まぁ幸いconstで宣言されているアドレスだし、読むだけで別 >にメモリ破壊するわけじゃないからアクセスしてもいいんじゃない?いちいち範囲チェック >するの面倒だし」とチェックをサボっているのでしょう。一方、RedHatの処理系はまじめ >に「ここはお前の触っていい領域じゃない!帰れ!」という感じでエラーになるんでしょう >ね。。。(あくまで推測) Solaris にせよ、 RedHat にせよ、チェックのレベルはともかく、結果として core を吐くとか結果が間違っておれば一目瞭然なのですが、Solaris の場合 「正しく」動いてしまうのが問題だと言う事で。 何か、過去にもそういう人がおりましたが、「内面で解決した」のと、 「客観的に解決した」事は別でして。問題である事が問題なのではなく、 問題でない事が問題な訳です。一つ '!' 条件入れるだけですが、プログラムに 詳しい方ならそんなに理解は難しくは無いと思うのですが。。。 | ||||||||||||||||||||
|
投稿日時: 2005-01-14 13:14
>CではCall By Referenceは「ありません」よ!!
これは、日本語の体系若しくは日本語と英語の対応を変えてしまうという 事ですか? | ||||||||||||||||||||
|
投稿日時: 2005-01-14 13:22
こんにちは。
これは難しい問題ですね。 変数レベルで見ると、Cは値渡ししかありません。そのため、ポインタ変数の値を渡してやることにより、擬似的に参照渡しを行っています。 つまり、本当は「参照の値渡し」なんですよね。これをCall By Referenceと呼ぶかどうかは人によって意見が分かれそうです。 |