VC++のscanf()の文字入力で、入力済みの変数が変更されてしまう。

解決


C言語初心者  2005-05-21 08:10:50  No: 57482

#include <stdio.h>

int main() {
  char ch;
  int i , j;
  printf("i=");
  scanf("%d",&i);
  printf("j=");
  scanf("%d",&j);

  printf("i=%d,j=%d\n",i,j);
  fflush(stdin);
  printf("1文字=");
  scanf("%1s",&ch);
  printf("i=%d,j=%d,ch=%c\n",i,j,ch);

  return 0;
}

上のプログラムをVC++でコンパイルして実行すると、文字入力することで、iの値だけが、必ず  0  にリセットされてしまいます。
同じソースをボーランドでコンパイルすると、このようなことは起こりませんでした。
scanfで入力された段階でメモリーにそれぞれの変数が値を持って確定されているでしょうし、新たにstdinで文字を入力することで、これらの変数が影響を受けてしまうのが腑に落ちません。

理由や対処方法をご存知の方がおられましたら、よろしくお願いいたします。


C言語初心者  2005-05-21 08:13:51  No: 57483

環境はWindowsXP, VS.NET2003.
コマンドラインでvsvars32.batを実行してから、clコマンドでコンパイルしています。


まきじ  2005-05-21 08:34:43  No: 57484

>scanf("%1s",&ch);

は、scanf("%c",&ch); ですね。


RiSK  2005-05-21 08:36:35  No: 57485

>上のプログラムをVC++でコンパイルして実行すると、文字入力することで、iの値だけが、必ず  0  にリセットされてしまいます。
>同じソースをボーランドでコンパイルすると、このようなことは起こりませんでした。

バッファオーバーフローが起きています。

>scanfで入力された段階でメモリーにそれぞれの変数が値を持って確定されているでしょうし、新たにstdinで文字を入力することで、これらの変数が影響を受けてしまうのが腑に落ちません。

問題なのはこの部分

>    char ch;
>(snip)
>    scanf("%1s",&ch);

s 指定子は規格によると

>文字列の終わりを示すナル文字を自動的に付加する。

のでサイズを1と指定しても実際には2バイト書き込まれます。
ch は1バイトしかありませんから,たまたま隣にあったiにまで
影響が及んだのでしょう。


RiSK  2005-05-21 08:46:04  No: 57486

あぁそうだ。

>    fflush(stdin);

これは意味がないと思います。

以下,独り言

私の環境ではオプションデフォルトの場合
VC6(IDE), VCTK2003 では bcc 同様,一見正しく動作しているように見える。
VC2005β2(prompt) では i が 0 になる。

やっぱりバッファオーバーフローは怖いと思った次第。


C言語初心者  2005-05-21 14:19:56  No: 57487

まきじさん、RiSKさん、ありがとうございます。

ご指摘通り、%cにすることで、きちんとVC++でも動きました。.NET2003ドキュメントで、%sはnull文字を追加するというのは、確かにそれとなく見ていたのですが、まさか、1byte違いの直ぐお隣さんだったとは。。  またscanfの指定子は只でさえ色々あって、頭が混乱していました。

そもそも、

#include <stdio.h>

int main() {
  char ch;
  int i , j;
  printf("演算します。式を入力してください>");
  scanf("%d %[+-*/] %d" , &i , &ch , &j);
  printf("i=%d operator=\'%c\' j=%d\n",i,ch,j);

  switch(ch) {
  case '+':
    printf("答えは %d です" , i + j);
    break;
  case '-':
    printf("答えは %d です" , i - j);
    break;
  case '*':
    printf("答えは %d です" , i * j);
    break;
  case '/':
    printf("答えは %d です" , i / j);
    break;
  default:
    printf("入力に誤りがあります");
  }
  return 0;
}

が、bccだと上手くいくのに、VC++ではNG。  そこから迷走が始まってしまいました。%[+-*/]も、なにかバッファオーバーフローっぽいです。これはあるサイトのコードを少し改変したもので、管理人さんの断り書きにはきちんと、bccのみでの検証と書かれていました。%[A-Z]なんてのもあるからそれを考えて、%[-+*/]と+,-の順序を逆にしてみると、%1sとしたときと、まったく同じ挙動になりました。
大分、スッキリしました。ありがとうございます。
(この書き込みでまだ詰めの甘いところ、興味深い指摘などがありましたら、突っ込んでいただけると嬉しいですm(^_^)m


C言語初心者  2005-05-21 14:38:12  No: 57488

#include <stdio.h>

int main() {
  char ch;
  int i,j;

  printf("i=");
  scanf("%d",&i);
  printf("j=");
  scanf("%d",&j);

  printf("ch=");
  scanf("%[+-*/]",&ch);

  printf("%c %d %d",ch,i,j);
  return 0;

}

だとバファに残留してる改行コードを読み込んでしまってる感じで、

#include <stdio.h>

int main() {
  char ch;
  int i,j;

  printf("i=");
  scanf("%d",&i);
  printf("j=");
  scanf("%d",&j);

  fflush(stdin);

  printf("ch=");
  scanf("%[+-*/]",&ch);

  printf("%c %d %d",ch,i,j);
  return 0;

}

と、fflush(stdin)を追加すると、やはりnull追加で・・みたいになるようです。先にchをscanfして、fflush(stdin)してから、i,jをscanfすると丁度いいみたい。
ややこしいです。


C言語初心者  2005-05-21 14:39:14  No: 57489

長々と失礼しました。  解決です。


C言語初心者  2005-05-21 14:49:38  No: 57490

>先にchをscanfして、fflush(stdin)してから

いや、fflush必要ないですね。失礼。


Ban  2005-05-21 16:55:24  No: 57491

あちこちの掲示板で何度も話題になるのでよくある誤解だと思いますが、

> いや、fflush必要ないですね。失礼。

必要ないというか、入力用のストリームに対する fflush は
言語的に「未定義」ですからそもそも「書いてはいけない処理」です。

VC の場合はたまたま、一見問題がないように見えますが、
規格上は「何がおきてもかまわない/保証しない」ので、
PCが壊れようが、ハードディスクが消去されようが、
(VC のように)一見正常に見えてしまおうがかまいませんし、
エラーになるとも限りません。

<JIS X3010>
7.9.5.2 fflush 関数 (抜粋)
stream が出力ストリーム又は直前の操作が入力でない更新ストリームを
指すとき、fflush 関数は 〜 (中略) 〜 書き込む。
それ以外の時の動作は未定義とする。
</JIS X3010>
※全文は規格書を別途あたってください。


C言語初心者  2005-05-22 05:39:55  No: 57492

Banさん、ありがとうございます。

>言語的に「未定義」ですからそもそも「書いてはいけない処理」です。

未定義はコンパイラー依存と考えてもいいのでしょうか^^;
結構、書籍にも載ってるようなので、いけない処理  だとは思いませんでした。VSのドキュメントを注意して見ると、確かに次のような一節がありました。

メモ   fflush が EOF を返す場合は、書き込み失敗によりデータが失われている可能性があります。重大エラーのハンドラを設定する場合、setvbuf 関数でバッファリングをオフにするか、ストリーム入出力関数ではなく _open、_close、_write などの低水準入出力ルーチンを使用するのが最も安全な方法です。

Cってこういうところも取っ付きにくいですよね。
ありがとうございます、参考になりました。


Ban  2005-05-22 07:34:45  No: 57493

> 未定義はコンパイラー依存と考えてもいいのでしょうか^^;

implementation defined (実装依存)というのは undefined (未定義)
とは別物です。

言語規格からみたイメージは、

実装定義
「僕は決めない/決められないから、定義は処理系毎にベンダが決めるよ」
♯VC なら Microsoft が動作結果を定義する

未定義
「言語のレベルで、この処理を書いたら一切の保証をしないよ。
コンパイラも何も保証しなくていいよ。何がおきてもいいよ。
(理由はいろいろありますが、主にこれをコンパイラが保証しようとすると
ライブラリが重くなったり、弊害が大きすぎる場合です)」


C言語初心者  2005-05-25 04:37:02  No: 57494

BANさん、折角、返信いただいたのに、遅れてしまいました。申し訳ありません。

>「僕は決めない/決められないから、定義は処理系毎にベンダが決めるよ」
>「言語のレベルで、この処理を書いたら一切の保証をしないよ。
>コンパイラも何も保証しなくていいよ。何がおきてもいいよ。

あぁ、すごくわかりやす表現で助かります。
実装定義はヴェンダに選択の余地が残され、未定義は言語やシステムに影響が及ぶリスクがあり、やはり、「使うべきでない」ということですね。
fflush()以外にも未定義のものは注意したいと思います。
ありがとうございました。


tetrapod  2005-05-25 18:33:40  No: 57495

より正確に言うなら

・未定義:誤ったプログラム素片。結果がどうなるかは誰も何も保証しない。
  暴走するかもしれないし、エラーで止まるかもしれないし、
  はたまたプログラマの期待通りに動くかもしれない。

・処理系定義:正しいプログラム素片。ベンダが動作をマニュアルに記述すべきもの。

・未規定:正しいプログラム素片。ベンダが動作を記述しなくて良いもの。

未定義の例:fflush(stdin); // エラーになっても、未処理の入力が消されてもいい。
処理系定義の例:sizeof int や CHAR_BIT などの具体的値。
未規定の例: a=f()+g(); で実際に f() と g() のどちらが先に呼ばれるか。

未定義は「誤ったプログラム」なので、使うべきでないというより
「使ってはいけない」のです。

処理系定義、未規定は正しいプログラムなので、使ってよいのです。
ただしその動作はコンパイラのベンダや種類やバージョンなどによって異なりうるわけです。
そのため注意して使う必要があります。
wc=(getchar()<<8)|getchar(); は未規定のプログラム例です。
プログラマはこれで SJIS 1文字を受理するつもりなのでしょうが、
ある処理系では「プログラマの期待通りの動作」
ある処理系では「プログラマの期待に反した動作」
をするでしょう。しかしどちらも規格書的には「正しい動作」です。
処理系依存性を除くには wc=getchar()<<8; wc|=getchar(); とすべきでしょう。


C言語初心者  2005-05-26 06:25:54  No: 57496

tetrapodさん、ありがとうございます。

>未規定の例: a=f()+g(); で実際に f() と g() のどちらが先に呼ばれるか。

f(),g()がもしグローバル変数を扱う関数だとしたら、どちらが先によばれるかで結果が違ってきますよね。

>処理系依存性を除くには wc=getchar()<<8; wc|=getchar(); とすべきでしょう。

確かに、処理系への依存だけでなく、こちらの方が視認性もいいし、なるほどです。

>未定義は「誤ったプログラム」なので、使うべきでないというより「使ってはいけない」のです。

これ、書籍だと案外、未定義については触れられずに紹介されてたりするので、学習するうえで困るところです。


※返信する前に利用規約をご確認ください。

※Google reCAPTCHA認証からCloudflare Turnstile認証へ変更しました。






  このエントリーをはてなブックマークに追加