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

ヒント文字列の更新バグの修正(C/C++)


今回は前回の最後で触れた「ヒント文字列の更新」機能に含まれるバグについてです。
まずは該当部分のコードを貼っておきます。(全体は前回をご覧ください)
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
      //機能「ヒント文字列の更新」
      
ans_len=strlen(question[sel_question][1]);//(1)正解文字列の長さを取得します
      
player_len=strlen(player_ans);//(2)プレイヤーの回答文字列の長さを取得します
      
for(i=0;(i<ans_len)&&(i<player_len);){//(3)回答と正解文字列を比較していきます
         
if(SJISMultiCheck(player_ans[i])){//日本語文字の場合
            
if((player_ans[i]==question[sel_question][1][i])&&
               (player_ans[i+1]==question[sel_question][1][i+1])){
//(4)2バイトの一致を調べ
               
hint_str[i]=question[sel_question][1][i];//(5)2バイト単位で処理する
               
hint_str[i+1]=question[sel_question][1][i+1];
            }
            i+=2;
         }
         
else{//半角文字の場合
            
if(player_ans[i]==question[sel_question][1][i]){
               hint_str[i]=question[sel_question][1][i];
            }
            i++;
         }
      } 
さて、なんで「富士山」に「xxmmRR」と入れるとおかしくなるかと言えば、
「富士山」の日本語第2バイトがそれぞれ富=x、士=m、山=Rになっていることによります。

つまりどういうことかというと、入力が半角文字だったため、半角文字の判定(13〜18行)が実行されるわけですが、
ここで各文字の日本語第2バイトと入力文字を比較すると、一致してしまうわけです。

その結果として hint_str は正解文字列の日本語第2バイトだけがコピーされることになってしまいます。
これが文字化けの原因というわけです。


それでは対策を考えてみます。
原因は先ほど示した通り、正解文字列の日本語バイトの一部と比較してしまうことにあります。
これは正解文字列の文字境界(2バイト文字の2バイト目)を気にせずに比較処理を組んだことに原因があるので、
正解文字列側でも文字境界の判定を追加することにします。

<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
< 26>
< 27>
< 28>
< 29>
< 30>
< 31>
< 32>
< 33>
< 34>
< 35>
< 36>
< 37>
< 38>
< 39>
< 40>
< 41>
< 42>
< 43>
< 44>
< 45>
< 46>
< 47>
< 48>
< 49>
< 50>
< 51>
< 52>
< 53>
< 54>
< 55>
< 56>
< 57>
< 58>
< 59>
< 60>
< 61>
< 62>
< 63>
< 64>
< 65>
< 66>
< 67>
< 68>
< 69>
< 70>
< 71>
< 72>
< 73>
< 74>
< 75>
< 76>
< 77>
< 78>
< 79>
< 80>
< 81>
< 82>
< 83>
< 84>
< 85>
< 86>
< 87>
< 88>
< 89>
< 90>
< 91>
< 92>
< 93>
< 94>
< 95>
< 96>
< 97>
< 98>
< 99>
<100>
<101>
<102>
<103>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

int SJISMultiCheck(unsigned char c){
   
if(((c>=0x81)&&(c<=0x9f))||((c>=0xe0)&&(c<=0xfc)))return 1;
   
else return 0;
}

int main(void){
   
//ソース上に直接書き込んだ問題
   
char question[10][2][64]={
      {
"「プログラム」を英語で書くと?","program"},
      {
"日本で一番高い山は?","富士山"},
      {
"3*5=","15"},
      {
"「library」をカタカナ読みすると?","ライブラリ"},
      {
"日本の都道府県の数は?","47"},
      {
"「檸檬」はなんて読む?","れもん"},
      {
"「万」の上の単位は?","億"},
      {
"「迎撃」はなんて読む?","げいげき"},
      {
"22+33*5=","187"},
      {
"「メモリ」を英語で書くと?","memory"}
   };
   
//選択された問題
   
int sel_question;
   
//入力された答え
   
char player_ans[80]="";
   
//ヒント文字列
   
char hint_str[80]="";
   
//ループカウンタ用一時変数
   
int i,j;
   
//正解文字列の長さ用一時変数
   
int ans_len;
   
//入力文字列の長さ用一時変数
   
int player_len;
   
   
//機能「乱数を使って選択し表示します」
   
srand(time(NULL));
   sel_question=rand()%10;
   puts(question[sel_question][0]);
   
for(i=0;question[sel_question][1][i]!='\0';){
      
if(SJISMultiCheck(question[sel_question][1][i])){
         strcat(hint_str,
"*");
         i+=2;
      }
      
else{
         strcat(hint_str,
"*");
         i++;
      }
   }

   
while(1){
      
//機能「ヒント文字列の表示」
      
printf("ヒント:%s\n",hint_str);
      
      
//機能「その問題の答えをプレイヤーはキーボードから入力」
      
fgets(player_ans,79,stdin);
      
      
//機能「ヒント文字列の更新」
      
ans_len=strlen(question[sel_question][1]);
      player_len=strlen(player_ans);
      j=0;
//(1)正解文字列側の位置保持用変数
      
for(i=0;(i<ans_len)&&(i<player_len);){
         
while(j<i){//(2)今からチェックする位置に合わせます
            
if(SJISMultiCheck(question[sel_question][1][j]))j+=2;
            
else j++;
         }
         
if(i!=j){//(3)境界が合っていない場合、ハズレとして扱います
            
if(SJISMultiCheck(player_ans[i]))i+=2;
            
else i++;
         }
         
else{
            
if(SJISMultiCheck(player_ans[i])){//日本語文字の場合
               
if((player_ans[i]==question[sel_question][1][i])&&
                  (player_ans[i+1]==question[sel_question][1][i+1])){
                  hint_str[i]=question[sel_question][1][i];
                  hint_str[i+1]=question[sel_question][1][i+1];
               }
               i+=2;
            }
            
else{//半角文字の場合
               
if(player_ans[i]==question[sel_question][1][i]){
                  hint_str[i]=question[sel_question][1][i];
               }
               i++;
            }
         }
      }
   
      
//機能「成否を判定します」
      
if(player_ans[0]!='\0')player_ans[strlen(player_ans)-1]='\0';
      
if(!strcmp(player_ans,question[sel_question][1]))break;
      puts(
"違います。もう一度入力してください。");
   }
   
   
//機能「クリア表示して終了」
   
puts("正解です。Enterを押すと終了します。");
   
   
//終了待ち
   
getchar();
   
return 0;
(1)63行で、正解文字列の文字区切り確認位置を格納する一時変数 j を初期化します。
変数定義でも j がさりげなく追加されています。

(2)65行からのループで、正解文字列の文字区切りを合わせていきます。
このループに入る時に調べたいのは i の位置なので、 j が i と等しくなるか超えるまで移動させます。
j の値はループ内でリセットしていませんが、 j の示す位置は必ず文字区切りであることを確認しているので、
j をリセットしない方が処理量を削減することができます。

(3)69行の条件分岐が今回のポイントです。
この条件 i!=j が満たされるというのは、 question[sel_question][1][i] が文字区切りでないことを示しています。
なんで?と思うかもしれませんが、 j が取る値の意味を良く考えてみてください。
(2)のループで、 j は i 以降の最初の文字区切り位置を示しています。
j が i と等しくないということは、 question[sel_question][1][i] は日本語第2バイトであるということになり、
j が i と等しいということは、 question[sel_question][1][i] は日本語第1バイトか半角文字ということになります。

なので、この分岐で日本語第2バイトから比較してしまうバグを修正することができます。
少しややこしい部分ですが、このようなバグは意識していないと見落としてしまいやすいです。
入力パターンの組み合わせを良く考えてみると、この形のバグを見つけやすくなります。


次回は、今回のコードをもう少し見やすいように改良してみる予定です。

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

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

最終更新 2010/05/07