ぱぴブログ

さんたろのブログ

ゲーム開発が好きな人のひとりごと

C言語での数値の入力

はじめに

今回はC言語での(int型の)数値の入力について考える。

正しい入力だけに対応するなら
scanf関数を用いても構わないかもしれないが、
様々な入力に対応しようとすると厳しい。
(私が力不足なだけだが)

今回は入力された数字を文字列として格納し、
数値に変換することにより数値の入力を実現しようと思う。

文字列として入力

文字列としての入力に、今回はfgets関数を用いる。
fgets関数は改行コードも読み取るため、
改行コードをNULL文字に変えるためのtrimEnd関数も用意する。

#include <stdio.h>
#include <string.h>

void trimEnd(char str[]){
  int i = 0;
  while(str[i] != '\n' && i < strlen(str)){
    i++;
  }
  str[i] = '\0';
}

int main(void){
  char s[256];
  fgets(s, 256, stdin);
  trimEnd(s);
  printf("%s\n", s);
  return 0;
}

atoi関数

atoi関数は文字列型からint型の数値に変換する関数である。
詳しくはリファレンスを見てほしいが大まかな機能を以下に記す。

・変換する文字列は先頭から見る
・変換できない文字列(先頭の空白類文字を除く)がきた時点で終了する
・数値へ変換できずに終了したとき0を返す
(例)
"123Hello" → 123
"Hello123" → 0
" 3" → 3
"11 11" → 11

まずatoi関数を利用するにあたっての問題点が
0が入力された数値なのか、エラーなのかわからない
ということである。

私はこの問題をstrcmp関数を利用することで対応した。
変換前の入力文字列が"0"かつ、変換後の数値が0なら0。
返還前の入力文字列が"0"でなく、変換後の数値が0ならエラーである。

以下にこれを利用したコードを載せる

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void trimEnd(char str[]){
  int i = 0;
  while(str[i] != '\n' && i < strlen(str)){
    i++;
  }
  str[i] = '\0';
}
  
int main(void){
  char s[256];
  int n;
 
  do{
    printf("0以上100以下の番号を入力してください: ");
    fgets(s, 256, stdin);
    trimEnd(s);
    printf("番号 = %d\n", atoi(s));
  }while((atoi(s) == 0 && strcmp(s, "0") != 0) || atoi(s) < 0 || atoi(s) > 100);
  printf("\n入力成功\n");
 
  return 0;
}

実行してみる

0以上100以下の番号を入力してください: aa
番号 = 0
0以上100以下の番号を入力してください:    af
番号 = 0
0以上100以下の番号を入力してください: A
番号 = 0
0以上100以下の番号を入力してください:
番号 = 0
0以上100以下の番号を入力してください: 101
番号 = 101
0以上100以下の番号を入力してください: -1
番号 = -1
0以上100以下の番号を入力してください: 0
番号 = 0

入力成功

whileの条件式が長くなるが無事予想通りの挙動を見せた。
しかしint型で表せる範囲を超えた数値を入力されたとき、
不定の動きとなる。
また、数字のあとに文字列を入力したときにエラーがでない。
以下にその実行例を載せる

0以上100以下の番号を入力してください: 1213134334333233123123
番号 = -1
0以上100以下の番号を入力してください: 123hello
番号 = 123
0以上100以下の番号を入力してください: -3146124687346244768346
番号 = 0
0以上100以下の番号を入力してください: 0
番号 = 0

入力成功

正直atoi関数とstrcmp関数を組みわせれば問題ないが、
より厳格にするにはstrtol関数を用いる。

strtol関数

strtol関数はatoi関数より複雑だが、エラーしたときにちゃんと対応してくれる。
文章にすると長いので以下のリファレンスとコードを参考にしてほしい。

linuxjm.osdn.jp

以下がサンプルコードである。
上のコードの値の範囲が0から100ではなくなったものである。
なおtrimEnd関数は省略してある。

#include <stdio.h>                                                         
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <errno.h>

void trimEnd(char str[]);

int main(void){
  char s[256];
  long int n;
  char *endptr;

  do{
    errno = 0;
    printf("数字を入力してください: ");
    fgets(s, 256, stdin);
    trimEnd(s);
    n = strtol(s, &endptr, 10);
    printf("n = %ld\n", n);
    printf("endptr = |%s|\n", endptr);
  }while(*endptr != '\0'|| errno == ERANGE || strcmp(s, "\0") == 0);

  printf("\n*正しく入力されました\n");
  printf("n = %ld\n", n);
  printf("endptr = |%s|\n", endptr);

  if(n > INT_MAX || n < INT_MIN){
    printf("%ldはint型に変換できません\n", n);
  }else{
    printf("(int)%d\n", (int)n);
  }
 
  return 0;
}

実行例

数字を入力してください:
n = 0
endptr = ||
数字を入力してください: 11111111111111111111111111111111111111
n = 9223372036854775807
endptr = ||
数字を入力してください: -111111111111111111111111111111111111
n = -9223372036854775808
endptr = ||
数字を入力してください: 123hello
n = 123
endptr = |hello|
数字を入力してください: aa aa aa
n = 0
endptr = |aa aa aa|
数字を入力してください: 1111111111111
n = 1111111111111
endptr = ||

*正しく入力されました
n = 1111111111111
endptr = ||
1111111111111はint型に変換できません

オーバフロー、アンダーフローの判定、
読み込み終了した位置の文字列のアドレスの保存が可能である。
そのため数値への変換が不可能なときatoi関数と同じく0を返すが、
atoi関数と比べ、どのようなエラーか判断がしやすい。

おわりに

正直int型で返ってこないstrtol関数は使いにくい。
よほどでなければatoi関数でいいだろうな。たぶん。