第5回 標準入出力(5月18日)

■ 今日のポイント

■ 本題に入る前に

前回の練習で作成したファイルを準備しよう。まだ作っていない人は、以下のリンクよりファイルを入手しよう。

また、今回の演習の説明のために、以下のリンクよりファイルを入手しよう。

■ 標準入出力を使ったソート

ここの name_list2.txt は、名前の順序がグチャグチャである。linux では、シェル上にソートコマンドがあるので、整えることができる。

# sort < name_list2.txt

不等号 < は、右側のファイルの内容を sort コマンドに入力するという意味である。キーボードから入力したのと同じ動作をする。

タブで区切られたテキストの場合、sort コマンドは次のように使うことができる。

# ./prac0401 < name_list2.txt | sort -k 2

prac0401 が、前回の練習で行ったとおり、タブ区切テキストに変換する。タブ区切りテキストの2列目を基準にしてソートせよという意味である。

縦棒は、パイプと言う。./prac0401 の画面に出力されるものが、sort への入力となる。

■ qsort (配列をソートする)

C言語には、配列をソートするための関数 qsort が stdlib.h より提供されている。ライブラリを使ったプログラミングは、下手に自分で書くよりも安全かつ高速である。man コマンドで qsort の説明を見てみよう:

#include <stdlib.h>
void qsort(void *base, size_t nmemb, size_t size,
           int(*compar)(const void *, const void *));

各引数の意味は次のとおりである:

第4引数の関数は次の仕様である:

上記を理解するために

上記の説明を理解するためには、ネームカードを要素とする配列を準備する必要がある。

ステップ1:ネームカードを要素とする配列

list0502.c では、NameCard へのポインタの配列を ncarray という変数が扱っている。malloc により確保しているのは、MAX 個の NameCard へのポインタである。次に、最初の for 文では最大で MAX 回の繰り返しをしている。ここでは、NameCard を1つぶん確保して(nc_open)、標準入力からデータを入力している(nc_get)。nc_get に失敗すれば for 文を抜け、成功すれば配列変数に nc のポインタ値を代入する。

2番目の for 文で、配列変数 ncarray を最初から順に表示する。

なお、実行は、「./prac0401 < name_list2.txt | ./list0502」とせよ。
/* list0502.c */

#include <stdio.h>
#include <stdlib.h>
#include "namecard.h"

#define MAX 30

int main()
{
  NameCard **ncarray;
  NameCard *nc;
  int i, sz;

  // NameCard のポインタを格納する配列
  ncarray = (NameCard **)malloc(sizeof(NameCard *) * MAX);
  if( ncarray == NULL ){
    fprintf(stderr,"malloc failed 1\n");
    exit(1);
  }

  // 標準入力 stdin からデータを入力し、配列に格納
  for( sz = 0; sz < MAX; sz += 1 ){
    nc = nc_open();
    if( nc_get(nc, stdin) != 0 )
      break;
    ncarray[sz] = nc;
  }

  // 配列の要素を表示
  for( i = 0; i < sz; i += 1 )
    nc_put(ncarray[i], stdout);

  // 終了
  for( i = 0; i < sz; i += 1 )
    nc_close(ncarray[i]);
  free(ncarray);
  
  return 0;
}

ステップ2:配列の要素を入れ替えてみる

list0502.c で配列の要素を表示する前に、0番目と7番目を入れ替えてみよう。具体的には次の3行を挿入する。

nc = ncarray[0];
ncarray[0] = ncarray[7];
ncarray[7] = nc;

挿入した結果を、list0503.c としてセーブして、実行してみよう。

なお、実行は、「./prac0401 < name_list2.txt | ./list0503」とせよ。

ステップ3:ネームカードの比較

配列の要素の入れ替えを、基準に従って行えば、ソートができる。その基準として、ネームカードのメンバ変数が利用できる。ネームカードのメンバ変数(ドライバーナンバー、名前、年齢)を比較して、カード間を比べる関数を作ろう。

ここで、比較関数の仕様は、既に述べた「第4引数の関数」の仕様とする。具体的には以下のとおりにする(list0502.c の main関数より上で記述)。

int cmp_driver_number(NameCard **nc1, NameCard **nc2)
{
  return nc_get_driver_number(*nc1) - nc_get_driver_number(*nc2);
}

ここで、比較する要素が、NameCard * というポインタ型であるので、ポインタのポインタが引数の型となる。

list0502.c で、「配列の要素を表示」のfor文の後(「// 終了」の直前)に次の4行を挿入しよう。

// 比較
printf("%s と %s のドライバーナンバーを比較すると %d です。\n",
  nc_get_name(ncarray[2]),
  nc_get_name(ncarray[3]),
  cmp_driver_number(&ncarray[2], &ncarray[3]));

挿入した結果を、list0504.c としてセーブして、実行してみよう。

なお、実行は、「./prac0401 < name_list2.txt | ./list0504」とせよ。

ステップ4:qsort によるソート

いよいよ、ソートを行う。qsort の第1引数は、配列の先頭へのポインタなので、ncarray である。第2引数は、要素の数なので、sz である。第3引数は、1要素のサイズなので、sizeof(要素) である。ここで、要素は、NameCard へのポインタなので、sizeof(NameCard *)とする。第4引数は、関数へのポインタなので、関数名 cmp_driver_number を書く。この実行を、「配列の要素を表示」というところの直前で実行する。

ここで、関数へのポインタについて、正しくキャストしなければ、コンパイル時に警告(Warnning)が出る。そこで、qsort の第4引数の関数名 cmp_driver_number の前に次の書式を書く。

(int (*)(const void *, const void *))

■ 小レポート

ステップ4を完成させ、ドライバーナンバーで昇順になるようにソートせよ。そのプログラムを prac0501.c とせよ。実行は、「./prac0401 < name_list2.txt | ./prac0501」として、正しく動作することを確認せよ。

次に、名前をアルファベットの辞書順に並べるための比較関数 cmp_name を作成し、prac0501.c に追加せよ。そして、ドライバーナンバーでソートする部分(qsortの呼出し部分)をコメントアウトしておき、代りに cmp_name でソートするステートメントを追加せよ。

レポートには、最終的な prac0501.c および実行結果を掲載せよ。


(c) Masato TOKUHISA (2007, May 14.)