プロが教えるわが家の防犯対策術!

下記のような入力ファイル(input.txt)を配列に格納するプログラムを作成しています。
下のソースコードは行数・列数が一定の場合のものですが、実際の入力データは行数・列数とも不定です。つまり、行数・列数をプログラムで読み取らなければなりません。この場合、ソースコードをどのようにすれば良いのでしょうか?
大変お手数ですが、教えてください。よろしくお願いします。

x y1 y2 y3 y4
10.0 1.2 1.5 1.0 2.1
10.5 1.3 1.4 1.2 2.2
11.0 1.8 1.2 2.2 3.1
11.5 2.1 1.0 1.2 4.5
12.0 1.9 1.1 1.1 5.5
13.0 2.4 1.1 2.1 4.2

・1行目をchar型配列に格納したい。
・2行目以降はdouble型配列で1列目をX[]、2列目以降をY[][]に格納したい。
・行数は20(2行目以降)まで、列数は10(2列目以降)まで

#include <stdio.h>
#include <stdlib.h>
int main(){
int i,j;
double X[20],Y[20][4];
char x[2],y[10][4];
FILE *fpi;
if((fpi=fopen("input.txt","r"))==NULL){
fprintf(stderr,"Cannot open file.\n");
exit(1);
}
fscanf(fpi,"%s %s %s %s %s",x,y[0],y[1],y[2],y[3]);
printf("%s %s %s %s %s\n",x,y[0],y[1],y[2],y[3]);
for(i=0;i<6;i++){
fscanf(fpi,"%lf",&X[i]);
printf("%4.1lf ",X[i]);
for(j=0;j<4;j++){
fscanf(fpi,"%lf",&Y[i][j]);
printf("%4.1lf ",Y[i][j]);
}
printf("\n");
}
printf("\n");
return 0;
}

A 回答 (9件)

(続き)



// 読み込み全体
int readFile( FILE *fp, data_t *data )
{
 int ret;
 
 // 初期化
 memset( data, 0, sizeof(data_t) );
 
 // 1行目
 if ( (ret = readFileOne(fp,data)) != ERR_SUCCESS ){
  return ret;
 }
 // 2行目以降
 if ( (ret = readFileTwo(fp,data)) != ERR_SUCCESS ){
  return ret;
 }
 return ERR_SUCCESS;
}

// 1行目を読み込む
int readFileOne( FILE *fp, data_t *data )
{
 char **argp = data->head;
 char *token;
 
 if ( fgets(data->buff,sizeof(data->buff),fp) != NULL ){
  for ( token = strtok(data->buff,STR_SPACE) ; token != NULL ; token = strtok(NULL,STR_SPACE) ){
   if ( data->colyMax >= (COLY_MAX + 1) ){
    return ERR_OVER; // ここでも列数オーバー
   }
   data->colyMax++; ←countA の代わり
   *argp++ = token;
  }
  data->colyMax--; ←1つ引くことで列数の読み込み数となる
  *argp = NULL;
  return ERR_SUCCESS;
 }
 return ERR_FORMAT;
}

// 2行目以降を読み込む
int readFileTwo( FILE *fp, data_t *data )
{
 char buff[ 256 ];
 char *token;
 int i, j;
 
 for ( j = 0 ; j < LINE_MAX ; j++ ){
  // 空行ならスキップするための do-while 処理
  do {
   if ( fgets(buff,sizeof(buff),fp) == NULL ){
    return ERR_SUCCESS;
   }
   token = strtok( buff, STR_SPACE );
  } while ( *token == '\0' );
  
  // 1列目
  data->X[ j ] = atof( token );
  
  // 2列目以降
  for ( i = 0 ; i < COLY_MAX ; i++ ){
   if ( (token = strtok(NULL,STR_SPACE)) == NULL ){
    break;
   }
   data->Y[ j ][ i ] = atof( token );
  }
  if ( i != data->colyMax ){
   return ERR_WIDE; // 列数が異なる
  }
  data->lineMax++; // 読み込み行UP
 }
 return ERR_SUCCESS;
}

// 結果表示
int dispData( data_t *data )
{
 int i, j;
 
 // 読み込んだ列数,行数
 printf( "1行目の列数:%d\n", data->colyMax );
 printf( "2行目以降の行数:%d\n", data->lineMax );
 printf( "\n" );
 
 // 1行目のヘッダ
 for ( i = 0 ; data->head[i] != NULL ; i++ ){
  printf( "%s ", data->head[i] );
 }
 printf( "\n" );
 
 // 2行目以降のデータ
 for ( j = 0 ; j < data->lineMax ; j++ ){
  printf( "%4.1f", data->X[j] );
  for ( i = 0 ; i < data->colyMax ; i++ ){
   printf( " %4.1f", data->Y[j][i] );
  }
  printf( "\n" );
 }
 return ERR_SUCCESS;
}

以上。
    • good
    • 0
この回答へのお礼

ご丁寧にソースコードを書いていただきありがとうございます。
すべてを理解するのは時間がかかりそうですが、これでよいプログラムができそうです。

お礼日時:2007/07/02 22:03

★アドバイス


>だと5列でカウントされてしまいます(□はスペース)。
>これはなぜでしょうか?
 ↑
 これはスペースが最後にあるとそれを区切り文字として改行のみが最後の列数と判断されます。
 これは strtok() で区切り文字群をスペースのみにしているからです。
 直すには区切り文字群にスペース、タブ(\t)、改行(\n)を含めれば上手くいきます。
 修正する箇所はすべての strtok() 関数の第二引数を " " から " \t\r\n" の文字群に変更します。
 これで行末に複数の空白文字、タブ文字があっても列数が1つ増えずにすみます。
・あと列数のエラーを追加していますが countA は 1~11列が有効、12列でエラーとすべきです。
 理由は X列と Y列(1-10) の合計が head[] 配列に入るからです。10 ではなく 11 までが有効。
 if ( countA > 11 ){ ←これが正しい。
  printf( "エラー:列数オーバーです。\n" );
  exit( 1 );
 }
 こんな感じ。
>if ( countA - 1 != countC[j] ){
> printf("エラー:列数が異なる行があります。\n");
> exit(1);
>}
 ↑
 ここは countA から 1 を引いているので正しいです。
・空行の場合は行をスキップように処理して下さい。
 main() 関数が長くなりましたので、そろそろサブ関数に読み込み部、表示部に分けたほうが良いかも。
 あと No.6 の最後でデータ変数を構造体でまとめて管理するようにすれば関数間のやり取りが
 楽になると思います。
・下に main() 関数をスッキリさせるためのサンプルを載せておきます。

サンプル:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// break付きのキーワード
#define CASE  break;case
#define DEFAULT break;default

// 区切り文字群
#define STR_SPACE " \t\r\n"

// 最大値定数
#define LINE_MAX (20)
#define COLY_MAX (10)

// エラー定数
#define ERR_SUCCESS (0)
#define ERR_FILE   (-1)
#define ERR_OVER   (-2)
#define ERR_WIDE   (-3)
#define ERR_FORMAT  (-4)

// データ構造体
typedef struct data_t {
 char buff[ 256 ]; // 1行目の char 型配列
 char *head[ COLY_MAX + 2 ]; // 1行目のヘッダ文字列
 double X[ LINE_MAX ]; // 1列目
 double Y[ LINE_MAX ][ COLY_MAX ]; // 2列目以降
 int lineMax; // 読み込んだ最大行数
 int colyMax; // 読み込んだ最大列数
} data_t;

// 関数のプロトタイプ宣言
int readFile( FILE *fp, data_t *data );
int readFileOne( FILE *fp, data_t *data );
int readFileTwo( FILE *fp, data_t *data );
int dispData( data_t *data );

// メイン関数
int main( void )
{
 data_t data; ←これがデータ構造体
 char *msg;
 FILE *fp;
 int ret;
 
 // 読み込み処理
 if ( (fp = fopen("input.txt","r")) != NULL ){
  if ( (ret = readFile(fp,&data)) == ERR_SUCCESS ){
   ret = dispData( &data );
  }
  fclose( fp );
 }
 else{
  ret = ERR_FILE;
 }
 // エラー表示
 switch ( ret ){
  CASE ERR_FILE: msg = "エラー:ファイルが見つかりません。\n";
  CASE ERR_OVER: msg = "エラー:列数オーバーです。\n";
  CASE ERR_WIDE: msg = "エラー:列数が異なる行があります。\n";
  CASE ERR_FORMAT:msg = "エラー:データ構造が正しくありません。\n";
  DEFAULT: return 0;
 }
 fprintf( stderr, msg );
 return ret;
}

(次に続く)
    • good
    • 0

(1)今見てて気づきましたが、strtokのセパレータがまずいですね。


fgets()は改行コード(0x0a)も含めて文字列として読み込むはずですので,strtok(...," \n")という風に空白と改行コードの両方を指定しないとまずいです。
たぶん、これが原因で2番目の問題が起きていると思います。

(2)空白行を許す仕様にするなら、1列目の処理のときにtokenがNULLになっているはずですので、その行をパスするように処理してください。
その場合は、配列に代入されないのにcountBとjがカウントアップするとまずいので、カウントアップするのをやめてください。tokenの処理後にカウントアップさせます。そのときcountBとjってほとんど同じなので、1つの変数にまとめましょう。

(3)エラーガードですが、配列範囲外を破壊後にガードしているので、破壊前にガードするようにしてください。どこに行に問題があったかレポートしないと分からないと思います。

(4)今後のことを考えて定数化をお勧めします。
#define LINE_MAX (20)
#define COLY_MAX (10)

(5)変数名が意味を持っていないのでメンテナンス性が悪いと思います。countAよりcountColYの方が後々メンテするとき助かりますよ。
    • good
    • 0
この回答へのお礼

何度もありがとうございます。
多くのご指摘・アドバイス助かります。
No.8,9のご回答と合わせて修正していきます。

お礼日時:2007/07/02 22:00

★回答者 No.4 です。


・回答者 No.5 さんのご指摘の通りです。
 Y 配列は初期化しているのに X 配列を初期化していませんでした。
 次の1行を追加して下さい。
 『memset( X, 0, sizeof(X) ); // Xの初期化』
・今日、自分の回答を良く見たらいっぱいバグが潜んでいました。
 適当なサンプルを回答としてしまい申し訳ありませんでした。
>for ( i = 0 ; (i < 10) && (X[j][i] != 0.0) ; i++ ){
>を
>for ( i = 0 ; (i < 10) && (Y[j][i] != 0.0) ; i++ ){
 で正しいです。X 配列ではなく Y 配列ですね。
>もうひとつの問題は値に0.0を許していないことです。
>0.0を有効無効のフラグに使うのでは無く、有効数の値を持っていれば済む話だと思います。
>Xの行数とYの列数をカウントして、結果表示に使えば0.0で判定する必要はなくなります。
 ↑
 これは回答者 No.5 さんの『回答』の言うとおりです。
 『結果表示』の部分だけ次のように修正してみて下さい。
 これで 0.0 の数値も扱えます。→最大の20行×10列を表示します。
 
 // 結果表示
 for ( i = 0 ; head[i] != NULL ; i++ ){
  printf( "%s ", head[i] );
 }
 printf( "\n" );
 for ( j = 0 ; j < 20 ; j++ ){
  printf( "%4.1f", X[j] );
  for ( i = 0 ; i < 10 ; i++ ){
   printf( " %4.1f", Y[j][i] );
  }
  printf( "\n" );
 }
・あと細かいエラーチェックは追加して下さい。
>それと今後もちゃんと利用するプログラムにするのであれば、このままではエラーガードが弱すぎます。
>1行目が11個以上tokenがあったらエラーにすべきで、2行目以降が1行目の個数より多かったり
>少なかった場合にはエラーにすべきです。
 ↑
 回答者 No.5 の『回答』を参考に付けてみて下さい。
 機能追加して利用する際には main() 関数のみではなくてサブ関数を作り分かりやすくして下さい。
 例えば
 (1)データを読み込む関数(次の(2)(3)を呼び出す)
 (2)1行目を読み込む関数
 (3)2行目以降を読み込む関数
 (4)結果表示の関数(引数に head[],X[],Y[] を渡すなど)
 最低でも上記の関数に分けて、それを main() 関数などで呼び出すようにすれば管理や修正などが
 しやすくなると思います。
・また、必要なデータ変数は構造体で一括管理するのも良いかも。
 // データ変数
 typedef struct data_t {
  char buff[ 256 ];   // 1行目の char 型配列
  char *head[ 12 ];   // 1行目のヘッダ文字列
  double X[ 20 ];    // 1列目
  double Y[ 20 ][ 10 ]; // 2列目以降
 } data_t;
 ↑
 こんな感じで。

この回答への補足

(お礼の続き)
// 結果表示
printf("1行目の列数:%d\n",countA);
printf("2行目以降の行数:%d\n",countB);
for(i=0;i<20;i++){
printf("%d ",countC[i]);
}
printf("\n");
for ( i = 0 ; head[i] != NULL ; i++ ){
printf( "%s ", head[i] );
}
printf( "\n" );
for ( j = 0 ; j < countB ; j++ ){
printf( "%4.1f", X[j] );
for ( i = 0 ; i < countA - 1 ; i++ ){
printf( " %4.1f", Y[j][i] );
}
printf( "\n" );
}
fclose( fp );
return 0;
}
fprintf( stderr, "Cannot open file.\n" );
return 1;
}

補足日時:2007/07/01 15:17
    • good
    • 0
この回答へのお礼

何度もありがとうございます。
下のように修正およびエラー処理追加してみましたが、どうでしょうか?
また、No.5のご回答に入力ファイルについての疑問があります。ごぞんじであれば、教えてください。よろしくお願いします。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main( void ){
int i, j ,countA,countB,countC[20];
char buff[ 256 ];
char *head[ 12 ];
double X[ 20 ];
double Y[ 20 ][ 10 ];
char temp[ 256 ];
char **argp = head;
char *token;
FILE *fp;
// ファイルを読み込む
countA=0;
if ( (fp = fopen("input.txt","r")) != NULL ){
// 1行目を読み込む
memset( X, 0, sizeof(X) );
if ( fgets(buff,sizeof(buff),fp) != NULL ){
for ( token = strtok(buff," ") ; token != NULL ; token = strtok(NULL," ") ){
*argp++ = token;
countA++; /* 1行目の列数カウント */
}
*argp = NULL;
}
if ( countA > 10 ){
printf("エラー:列数オーバーです。\n");
exit(1);
}
// 2行目以降を読み込む
countB=0;
for( i = 0 ; i < 20 ; i++ ){
countC[ i ] = 0;
}
memset( Y, 0, sizeof(Y) );
for ( j = 0 ; j < 20 ; j++ ){
if ( fgets(temp,sizeof(temp),fp) == NULL ){
break;
}
countB++; /* 2行目以降の行数カウント */
// 1列目
X[ j ] = atof( token = strtok(temp," ") );
// 2列目以降
for ( i = 0 ; i < 10 ; i++ ){
if ( (token = strtok(NULL," ")) == NULL ){
break;
}
Y[ j ][ i ] = atof( token );
countC[ j ]++; /* 各行の2列目以降の列数カウント */
}
if ( countA - 1 != countC[j]){
printf("エラー:列数が異なる行があります。\n");
exit(1);
}
}
(文字制限超えのため以降補足へ)

お礼日時:2007/07/01 15:16

一つ目の問題は、


memset( X, 0, sizeof(X) ); // Xの初期化
が無いのはマズイです。
Xの配列に0.0以外の不定値が入っているのが8行目が出力された原因ではないでしょうか?

もうひとつの問題は値に0.0を許していないことです。
0.0を有効無効のフラグに使うのでは無く、有効数の値を持っていれば済む話だと思います。Xの行数とYの列数をカウントして、結果表示に使えば0.0で判定する必要はなくなります。

それと今後もちゃんと利用するプログラムにするのであれば、このままではエラーガードが弱すぎます。1行目が11個以上tokenがあったらエラーにすべきで、2行目以降が1行目の個数より多かったり少なかった場合にはエラーにすべきです。
    • good
    • 0
この回答へのお礼

再びありがとうございます。
アドバイスをもとに修正・エラー処理追加したコードがNo.6のご回答ですが、どうでしょうか?

また、入力ファイルで最後の行が

13.0 2.4 1.1 2.1 4.2
[EOF]

なら問題ない(行数6でカウント)のですが、

13.0 2.4 1.1 2.1 4.2

[EOF]

だと行数7でカウントされてしまいます
([EOF]の前に空行がある)。
また、

13.0 2.4 1.1 2.1 4.2↓

なら、2列目以降4列でカウントされます(↓は改行)が、

13.0 2.4 1.1 2.1 4.2□↓

だと5列でカウントされてしまいます(□はスペース)。
これはなぜでしょうか?
対策方法があれば教えてください。何度も申し訳ありませんが、よろしくお願いします。

お礼日時:2007/07/01 15:29

★アドバイス


・行数の最大は20行、列数の最大は10列なら配列は20行×10列を用意してあとは可変長で
 データを読み込めるようにプログラミングすれば良いでしょう。
 回答者 No.1、No.3 さんが strtok() 関数での方法をアドバイスしています。
 基本的にはこの方法で可変長データを読み込み出来ます。
・strtok() 関数以外にも strchr() 関数などを使っても出来ます。
 重要なのは区切り文字の空白文字を見て読み込むことです。
 下にサンプルを載せておきます。

サンプル:
int main( void )
{
 // データ変数
 char buff[ 256 ];   // 1行目の char 型配列
 char *head[ 12 ];   // 1行目のヘッダ文字列
 double X[ 20 ];    // 1列目
 double Y[ 20 ][ 10 ]; // 2列目以降
 
 // 一時変数
 char temp[ 256 ];
 char **argp = head;
 char *token;
 FILE *fp;
 int i, j;
 
 // ファイルを読み込む
 if ( (fp = fopen("input.txt","r")) != NULL ){
  // 1行目を読み込む
  if ( fgets(buff,sizeof(buff),fp) != NULL ){
   for ( token = strtok(buff," ") ; token != NULL ; token = strtok(NULL," ") ){
    *argp++ = token;
   }
   *argp = NULL;
  }
  // 2行目以降を読み込む
  memset( Y, 0, sizeof(Y) ); // 初期化
  for ( j = 0 ; j < 20 ; j++ ){
   if ( fgets(temp,sizeof(temp),fp) == NULL ){
    break;
   }
   // 1列目
   X[ j ] = atof( token = strtok(temp," ") );
   
   // 2列目以降
   for ( i = 0 ; i < 10 ; i++ ){
    if ( (token = strtok(NULL," ")) == NULL ){
     break;
    }
    Y[ j ][ i ] = atof( token );
   }
  }
  // 結果表示
  for ( i = 0 ; head[i] != NULL ; i++ ){
   printf( "%s ", head[i] );
  }
  printf( "\n" );
  for ( j = 0 ; (j < 20) && (X[j] != 0.0) ; j++ ){
   printf( "%4.1f", X[j] );
   for ( i = 0 ; (i < 10) && (X[j][i] != 0.0) ; i++ ){
    printf( " %4.1f", Y[j][i] );
   }
   printf( "\n" );
  }
  fclose( fp );
  return 0;
 }
 // ファイルが見つからない
 fprintf( stderr, "Cannot open file.\n" );
 return 1;
}

解説:
・1行目の文字列は head[] 配列にセットします。
 この配列の最後には必ず NULL をセットする仕様です。→head[最大数]=NULL
 その他はサンプル・ソースを解読して下さい。
・以上。
    • good
    • 0
この回答へのお礼

ご回答ありがとうございます。
とてもよく分かりました。
結果表示の部分で、
 for ( i = 0 ; (i < 10) && (X[j][i] != 0.0) ; i++ ){

 for ( i = 0 ; (i < 10) && (Y[j][i] != 0.0) ; i++ ){
に変更してコンパイルし実行しました。出力結果の数値データは6行目までですが、7行目以降に変な数値が出力されます。
しかし、入力データの一番最後に空行を入れると改善されます。
これは" Y[j][i] != 0.0 "がうまく機能していないからでしょうか?

お礼日時:2007/06/30 19:22

>strtok() で分割するといくつ分解したのか分かるのでしょうか


>(つまり列数が分かる)?
まず言えることは、関数名がわかればマニュアルを引けるということだ。

------ man strtok より ------
名前
strtok, strtok_r - 文字列からトークンを取り出す

書式
#include <string.h>
char *strtok(char *str, const char *delim);
char *strtok_r(char *str, const char *delim, char **saveptr);

説明
strtok() 関数は文字列を解析してトークンに分割する。 strtok() を最初に呼び出す際には、解析対象の文字列を str に指定する。同じ文字列の解析を行うその後の呼び出しでは、 str には NULL を指定する。

delim 引き数には、解析する文字列をトークンに区切る文字集合を指定する。同じ文字列を解析する一連の呼び出しにおいて、 delim に違う文字列を指定してもよい。

strtok() のそれぞれの呼び出しでは、次のトークンを格納した NULL 終端された文字列へのポインタが返される。この文字列には区切り文字は含まれない。これ以上トークンが見つからなかった場合には、NULL が返される。
-------------

とあるから、

fgets(buffer, buffer_length, file_ptr);
token = strtok(buffer, " ");
X[0] = strtod(token, NULL);
while ( token != NULL ) {
 token = strtok(NULL, " ");
 Y[0][i++] = strtod(token, NULL);
}

のような感じ。もちろん、配列の要素の確保やエラー処理を追加する必要あり。
    • good
    • 0

複雑さの問題を解決するのに有効な手段が、前処理です。


読み取るべき個数が不明なために、処理が複雑になるのなら、前処理を施して、
1)事前に要素数を求める。
2)行を分割して1行単位で処理できるようにする。
――とするとプログラミングしやすくなります、
この手のをCで書くのは無意味なので、Perlあたり(Excel VBAとかでも良いです。)を使います。
こんな感じ:
$\ = "\n";
$_ = <>; chomp; split( /\s+/ );
print $#_+1; print join( "\n", @_ );
my @y = map { [ split( /\s+/ ) ]; } grep { chomp; } <>;
@x = map { shift @$_; } @y;
print $#x+1; print join( "\n", @x );
print $#y+1; print $#{$y[0]}+1;
for ( @y ){ print join( "\n", @$_ ); }

これを実行して、質問のデータを食わせると以下の出力が得られます:

5
x
y1
y2
y3
y4
6
10.0
10.5
11.0
11.5
12.0
13.0
6
4
1.2
1.5
1.0
2.1
1.3
1.4
1.2
2.2
1.8
1.2
2.2
3.1
2.1
1.0
1.2
4.5
1.9
1.1
1.1
5.5
2.4
1.1
2.1
4.2

データの前に要素数を突っ込んでいるので、処理すべき要素数が明らかになります。
課題とかでCで書く必要があるなら別ですが、さっくりとこんな手を使ったほうが手っ取り早いかと。
    • good
    • 0
この回答へのお礼

ご回答ありがとうございます。
前処理の有効性は理解できました。
配列に格納する前に行数・列数を読み取れば、あとはそれをfor文のループ数とすればよいという意味ですよね。
しかし、私はPerlができないのでご回答いただいたソースコードを理解することができません。C言語ではどのようなコードになるのでしょうか?
すみませんが、私は今のところC言語しか分かりません。

※私は学生ではないので、これは課題ではありません。

お礼日時:2007/06/30 00:18

1行読み込んで strtok() などで分割するのが普通の方法じゃないのかな。



行数や、1行中の要素数が不定なら、配列を固定で確保するのではなくて、領域を動的に確保する必要があるかも知れません。
    • good
    • 0
この回答へのお礼

ご回答ありがとうございます。
strtok()ですか。初めてみました。私が使っている初心者用の本には載っていませんでした。 strtok() で分割するといくつ分解したのか分かるのでしょうか(つまり列数が分かる)?

お礼日時:2007/06/30 00:24

お探しのQ&Aが見つからない時は、教えて!gooで質問しましょう!