[前へ]   [目次へ]   [次へ]

ポインタ(6)(C/C++)


今回は、ポインタ系バグの発生の瞬間を見ていきます。
前回も触れたとおり、小規模なプログラムではなかなかキレイに発生はしてくれません。
なので意図的にそうなるように作ってみました。
また、ローカル変数の配置はコンパイラに任されているので、
全てのコンパイラで同じコードで同じ結果を得ることはできません。
・・・が、iとnの定義順を逆にするとできる可能性が高いです。

<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
#include <stdio.h>

int main(void){
   
int i;
   
int n[10];
   
int *p;
   p=n;
   
for(i=0;i<=10;i++){//(1)
      
p[i]=i-10;//(2)
   }
   
//終了待ち
   
getchar();
   
return 0;
条件次第では、このプログラムは永遠に終わりません。
内容としては「無効ポインタ参照:範囲外のポインタを参照」から
「通常ループを無限ループ化」を発生させる布石が入っています。
コンパイラが狙った通りにローカル変数を配置すれば、無限ループが生まれるようになっています。

さて、ソースコードを一見しただけでは、無限ループがあるようには見えません。
ここから無限ループに入っていく様子を、狙い通りに行くコンパイラを使ったとして見ていきます。


まず、ローカル変数が作成され、ポインタへの代入を終えた7行目終了時点から始めます。

この時点までを実行した時の状況の一例
実体ID(絶対)実体の型保持値所属
0x0012FF50int*0x0012FF54(main::n)main(p,line 6)
0x0012FF54int[10]main(n,line 5)
0x0012FF54int不定(未初期化)main(n[0],line 5)
0x0012FF58int不定(未初期化)main(n[1],line 5)
0x0012FF5Cint不定(未初期化)main(n[2],line 5)
0x0012FF60int不定(未初期化)main(n[3],line 5)
0x0012FF64int不定(未初期化)main(n[4],line 5)
0x0012FF68int不定(未初期化)main(n[5],line 5)
0x0012FF6Cint不定(未初期化)main(n[6],line 5)
0x0012FF70int不定(未初期化)main(n[7],line 5)
0x0012FF74int不定(未初期化)main(n[8],line 5)
0x0012FF78int不定(未初期化)main(n[9],line 5)
0x0012FF7Cint不定(未初期化)main(i,line 4)
mainブロック 確保位置ID:0x0013FF50

(1)8行目、 i に0を代入し、ループ開始です。
そして(2)9行目で p を介して n[0] に i-10 、 i は0なので-10を代入します。
ループは n の各要素に順次、値を代入しながら進んでいきます。
10回目のループまでは問題ないのでそこまで飛ばします。

この時点までを実行した時の状況の一例
実体ID(絶対)実体の型保持値所属
0x0012FF50int*0x0012FF54(main::n)main(p,line 6)
0x0012FF54int[10]main(n,line 5)
0x0012FF54int-10main(n[0],line 5)
0x0012FF58int-9main(n[1],line 5)
0x0012FF5Cint-8main(n[2],line 5)
0x0012FF60int-7main(n[3],line 5)
0x0012FF64int-6main(n[4],line 5)
0x0012FF68int-5main(n[5],line 5)
0x0012FF6Cint-4main(n[6],line 5)
0x0012FF70int-3main(n[7],line 5)
0x0012FF74int-2main(n[8],line 5)
0x0012FF78int-1main(n[9],line 5)
0x0012FF7Cint9main(i,line 4)
実行位置:9行終了(main)
mainブロック 確保位置ID:0x0013FF50


続いて8行目に戻り、 i++ が実行されます。
さらに i<=10 も真なので、ループは続行します。

この時点までを実行した時の状況の一例
実体ID(絶対)実体の型保持値所属
0x0012FF50int*0x0012FF54(main::n)main(p,line 6)
0x0012FF54int[10]main(n,line 5)
0x0012FF54int-10main(n[0],line 5)
0x0012FF58int-9main(n[1],line 5)
0x0012FF5Cint-8main(n[2],line 5)
0x0012FF60int-7main(n[3],line 5)
0x0012FF64int-6main(n[4],line 5)
0x0012FF68int-5main(n[5],line 5)
0x0012FF6Cint-4main(n[6],line 5)
0x0012FF70int-3main(n[7],line 5)
0x0012FF74int-2main(n[8],line 5)
0x0012FF78int-1main(n[9],line 5)
0x0012FF7Cint10main(i,line 4)
実行位置:8行終了(main)
mainブロック 確保位置ID:0x0013FF50


ここでポイントは i が n の次に配置されていることです。
そして、次の(2)9行目の式が無限ループ生成の瞬間です。

A B C B' D E F G
p [ i ]  = i - 10


通し記号種別
Aint*0x0012FF54(main::n,int[10])変数(p)
B演算子( [] )(その他、優先16、結合→)
Cint10変数(i)
B'演算子Bの終点
D演算子(=)(2項、算術、優先2、結合←)
Eint10変数(i)
F演算子(-)(2項、算術、優先12、結合→)
Gconst int10定数
まずは演算子B(目標 変数A、座標 変数C)です。
ここでの計算式は 0x0012FF54+10*4 で、計算すると 0x0012FF7C になります。
というわけで、処理後はこうなります。

A      B C D E
vtemp1 = i - 10


通し記号種別
Aint&10一時変数(参照先:0x0012FF7C(main::i))
B演算子(=)(2項、算術、優先2、結合←)
Cint10変数(i)
D演算子(-)(2項、算術、優先12、結合→)
Econst int10定数
先の表から、 0x0012FF7C の絶対IDは i です。
配列アクセスの領域違反の結果、「別の変数を参照」する状況が発生しています。

続いて、演算子Dが処理されます。

A      B C
vtemp1 = vtemp2


通し記号種別
Aint&10一時変数(参照先:0x0012FF7C(main::i))
B演算子(=)(2項、算術、優先2、結合←)
Cint0一時変数
そして、代入されます。

この時点までを実行した時の状況の一例
実体ID(絶対)実体の型保持値所属
0x0012FF50int*0x0012FF54(main::n)main(p,line 6)
0x0012FF54int[10]main(n,line 5)
0x0012FF54int-10main(n[0],line 5)
0x0012FF58int-9main(n[1],line 5)
0x0012FF5Cint-8main(n[2],line 5)
0x0012FF60int-7main(n[3],line 5)
0x0012FF64int-6main(n[4],line 5)
0x0012FF68int-5main(n[5],line 5)
0x0012FF6Cint-4main(n[6],line 5)
0x0012FF70int-3main(n[7],line 5)
0x0012FF74int-2main(n[8],line 5)
0x0012FF78int-1main(n[9],line 5)
0x0012FF7Cint0main(i,line 4)
実行位置:9行終了(main)
mainブロック 確保位置ID:0x0013FF50


もうお分かりかと思いますが。
i が0に戻ったということは、次のループの継続判定は当然「続行」です。
そして i が10になるたびに同じことが起こります。

こうして、無限ループは完成されました。
他にもパターンはあるのですが、今回は分かりやすいパターンを出してみました。
今回はポインタ型変数を介して行いましたが、直接配列変数に同じことをしても同じ結果になります。
今回ポインタ型変数を用いたのはこの方が絶対IDに強く焦点があたると考えたからです。



今回ほどキレイに現象が発動してくれることはあまりありませんが、
他の変数を書き換えてしまうという事態は割と簡単に発生することがわかったと思います。

このように、どの変数を書き換えたかによって、
ソースに記述されていないような実行ルートを取ることもあります。

数をこなしているとだんだんポインタ系バグに起因するかどうかを判断できるようになってきますが、
そのためには「書いているコード上で起きうる異常パターン」を把握できる必要があります。
そして、ポインタ系バグと踏んだら 第65回 から今回までの内容を使って判断していきましょう。

実際には、「書いているコード上で起きうる異常パターン」ではない現象はポインタ系バグであることが多いです。
また、「書いているコード上で起きうる異常パターン」もポインタ系バグのパターンを含めて把握しておけば、
さらにデバッグ精度は向上します。

「書いているコード上で起きうる異常パターン」を把握するにはまず、
自分自身がどういうミスをするのかをある程度知っていなければいけません。
過去に自分が無意識にやったミスというのは繰り返しやすいので、
自分のミスパターンそのものを把握しておくと結構便利です。
そりゃ、ミスそのものをなくせればそれに越したことはないんですけどね

過去の自分のミスと、その結果起きた現象を収集していけば、パターンはつかめてくるものです。
万人がやるミスというのも結構あるのですが、細かい部分は個人によって異なると思うので、
それなりには自力でやっていただくしかないと思います。


次回は「変更不可なポインタ」 const ポインタについてです。

[前へ]   [目次へ]   [次へ]

プログラミング講座 総合目次

最終更新 2008/11/07