2003年度後期 IT教育基礎論演習B

第5回: 手続き型プログラミング(3) ライブラリの利用

概要と目標

■ 概要

C言語におけるライブラリの利用方法を学ぶとともに、 各種具体的なライブラリの使用方法を習得する。

■ 目標

目次


演習内容

■ ライブラリの利用方法

C言語では、自分で新しく関数を定義できるだけでなく、 各種数値計算用の関数や、ネットワークを利用した通信のための関数など、 様々な関数が、予めライブラリとして用意されている。

C言語で提供されているライブラリを利用するためには、 そのライブラリで提供する関数のプロトタイプ宣言などが記述されている インクルードファイルをソースファイルの中から読み込み(インクルード)、 また、プログラムのコンパイル時に、 ライブラリを明示的に組み込む(リンク)必要がある (注: 標準入出力関数などのライブラリは、コンパイル時に自動的にリンクされる)。

□ インクルードファイルのインクルード

関数のプロトタイプ宣言等が記述されたインクルードファイルを読み込むには、 以下のように記述する。

#include <include-file>

インクルードファイルは、ヘッダファイルとも呼ばれ、 通常、拡張子として".h"が付けられている。 インクルードファイルを読み込む場合、 "/usr/include"等のディレクトリに置かれた システムが標準で提供しているインクルードファイルは、 "<"と">"で囲み、指定する。 システムの標準ではないインクルードファイルを読み込む場合、 「"(ダブルクォート)」で囲む。 例えば、数学ライブラリのためのインクルードファイルを読み込むには、 これは、システムが標準で提供しているため、 以下のように記述する。

#include <math.h>

□ ライブラリのリンク

コンパイル時にライブラリをリンクするには コンパイラのオプションとして"-l"を使用し、 以下のように記述する。

$ gcc -o <executable-file> <source-file>   -l<library>

ライブラリは、例えば数学ライブラリの場合、 "/usr/lib"ディレクトリ上に "libm.a"もしくは"libm.so"という ファイル名で用意されている。 ライブラリ名は、 このファイル名から"lib"と拡張子を除いたものとなる。 従って、数学ライブラリをリンクするには、以下のようにコンパイルする。

$ gcc -o <executable-file> <source-file>   -lm
演習

以下のプログラムを入力し、実行結果を確認せよ。

数学ライブラリを利用し平方根を計算するプログラム: square-root.c
/* square-root.c */

#include <stdio.h>
#include <math.h>

int main(int argc, char **argv)
{
  double x, r;

  printf("Input number: ");
  scanf("%lf", &x);

  r = sqrt(x);
  printf("Square root of %f is %f\n", x, r);
}

square-root の実行結果
./square-root
Input number: 4
Square root of 4.000000 is 2.000000

■ 各種ライブラリの利用

UNIXシステム上でC言語によるプログラム開発を行う場合、 ファイル入出力や日時の取得など、 広く一般に利用される関数のライブラリである標準Cライブラリや、 各種数学関数のライブラリである数学ライブラリなどを利用することができる。 以下、標準Cライブラリおよび数学ライブラリで提供される関数の利用方法を学習する。

なお、標準Cライブラリで提供される関数をを利用する場合、 その関数のプロトタイプ宣言や関連するマクロが記述された インクルードファイルを読み込む必要はあるが、 標準Cライブラリはプログラムのコンパイル時に自動的にリンクされるため、 コンパイル時にライブラリを明示的にリンクする必要はない。

□ ファイル入出力

UNIXシステムでは、インクルードファイル"stdio.h"を読み込むことで、 標準Cライブラリで提供される機能である、 ストリーム(stream)と呼ばれるバッファを利用した ファイルへのデータの書き込みやファイルからのデータの読み込みなどの ファイル入出力を行うことができる。

ファイルのオープンとクローズ

ストリームを利用してファイル入出力を行うには、先ず、 fopen()関数を利用し、 対象となるファイルを開く(オープンする)必要がある。 fopen()関数によりファイルをオープンするには、 ファイルのパスを示す文字列、 およびオープンするモードを示す文字列を指定する。 このとき、オープンするモードには、 表1に示すように読み込み、書き込み、追加などがある。

表1: ファイルオープンのモード
モード指定文字列機能
読み込み "r" 既にあるファイルからのデータ読み込みのためにファイルを開く
書き込み "w" 新規にファイルを作成し、データを書き込むためにファイルを開く
追加 "a" 既にあるファイルにデータを追加するためにファイルを開く

fopen()関数によりファイルをオープンすると、 ストリームを示す特別な型へのポインタを得る。 ファイルからのデータの読み込み、書きこみには、 このポインタを介し、各種ファイル入出力関数やマクロを利用する。 また、ファイルの使用が終了したら、 fclose()関数によりそのファイルを閉じる(クローズする)。

例えば、データを記録するために新規ファイルをオープンし、 またそのファイルをクローズするには、以下のように記述する。

新規ファイルの作成: create-new-file.c
/* create-new-file.c */

#include <stdio.h> /* ファイル入出力のためのインクルードファイルの読み込み */

int main(int argc, char **argv) {
  FILE *fp; /* ストリームへのポインタ変数の宣言 */

  fp = fopen("./new-file.dat", "w"); /* 新規ファイルを書き込みモードで開く */

  if ( fp == NULL ) { /* もしストリームへのポインタが得られなければ
			 ファイルのオープンに失敗 */
    printf("Cannot open file.\n");
  } else {

    /* ここにファイルへの書き込み処理手続きを記述するが、今回は何もしない */

    fclose(fp); /*
  }
}

このプログラムでは、実際のデータの書き込みは行わないが、 このプログラムを実行すると、以下のように容量0バイトのファイルが 新しく出来ていることが確認できる。

create-new-fileの実行結果
$ ./create-new-file
$ ls -l new-file.dat
-rw-r--r--   1 a1fm1001  student        0 10月 29日  13:22 new-file.dat
ファイルへのデータの書き込み

書き込み用にオープンしたファイルにデータを保存する場合、 基本的に、「文字」を単位としてデータを書き込む。 例えば、標準入力から入力された文字をそのまま コマンドラインで指定されたファイルに保存するプログラムは、 ファイルへの1文字書き込みを行うfputc()関数を利用して 以下のように記述することができる (注: fputc()関数と同様の機能を提供する putc()マクロを利用してもよい)。

ファイルへのデータの書き込み例: write-to-file.c
/* write-to-file.c */

#include <stdio.h> /* ファイル入出力のためのインクルードファイルの読み込み */

int main(int argc, char **argv) {
  FILE *fp; /* ストリームへのポインタ変数の宣言 */
  int one_character;

  if ( argc < 2 ) { /* ファイル名の指定がない場合は終了 */
    printf("Give a file name\n");
    return 1;
  }

  /* もしストリームへのポインタが得られなければファイルのオープンに失敗 */
  if ( ( fp = fopen(argv[1], "w") ) == NULL ) {
    printf("Cannot open file.\n");
    return 1;
  }

  while( ( one_character = getchar() ) != EOF ) { /* 標準入力から1文字づつ読み込む */
    /* 入力を終了するには、改行した後にCtrl-Dを押す */

    fputc(one_character, fp); /* 入力された文字をファイルに出力 */
  }
  fclose(fp);
}

このプログラムを実行した結果、以下のように、 ファイルにデータを書き込むことができたことが確認できる。

write-to-fileの実行結果
$ ./write-to-file write-test.dat
Hello, everybod!
$ cat write-test.dat
Hello, everybod!

なお、fputc()関数では1文字ずつデータを書き込むが、 fputs()関数を利用することにより、 文字配列により定義される文字列をまとめて出力するこもできる。

書式付きファイル出力

整数や実数などの数値をファイルに書き込みたい場合には、 これを文字列に変換して出力する必要がある (注: 数値をそのままデータとして記録することもできるが、 内容を直接参照できなくなる)。 また、何らかのデータ列をある書式に従って記録したい場合もある。 このような場合、書式付きデータ出力のための fprintf()関数を利用するとよい。 例えば以下のプログラムでは、 文字列と整数を1つの組にしてデータを書き込んでいる。

書式付きファイル出力例: formated-write.c
/* formated-write.c */

#include <stdio.h> /* ファイル入出力のためのインクルードファイルの読み込み */

struct _person {
  char name[256];
  int age;
};

int main(int argc, char **argv) {
  FILE *fp; /* ストリームへのポインタ変数の宣言 */
  struct _person list[5] = { /* 構造体変数の宣言と初期化 */
    "Masayuki Torai",		34,
    "Takoshi Yotsuashi",		33,
    "Yuji Tameyama",		33,
    "Yasuto Izumikawa",		32,
    "Yuichi Ogawa",		29
  };
  int i;

  if ( ( fp = fopen("staff-list.txt", "w") ) == NULL ) { /* ファイルのオープン */
    printf("Cannot open file.\n");
    return 1;
  }

  for( i = 0; i < 5; i++ ) {
    fprintf(fp, "%s %d\n", list[i].name, list[i].age); /* 書式付きファイル出力 */
  }
  fclose(fp);
}

このプログラムを実行した結果、以下のように、 文字列と数値をファイルに書き込むことができたことが確認できる。

formated-writeの実行結果
$ ./formated-write
$ cat staff-list.txt
Masayuki Torai 34
Takoshi Yotsuashi 33
Yuji Tameyama 33
Yasuto Izumikawa 32
Yuichi Ogawa 29
ファイルからのデータの読み込み

ファイルに記録されたデータを読み込むには、 そのファイルを読み込みモードによりオープンし、 fgetc()関数により1文字づつデータを読み込めばよい fgetc()関数と同様の機能を提供する getc()マクロを利用してもよい)。 例えば、指定されたファイルに記録された内容を読み込み、 標準出力に出力するプログラムは、以下のように記述することができる。

ファイルからのデータの読み込み例: read-from-file.c
/* read-from-file.c */

#include <stdio.h> /* ファイル入出力のためのインクルードファイルの読み込み */

int main(int argc, char **argv) {
  FILE *fp; /* ストリームへのポインタ変数の宣言 */
  int one_character;

  if ( argc < 2 ) { /* ファイル名の指定がない場合は終了 */
    printf("Give a file name\n");
    return 1;
  }

  /* ファイルを読み込みモードでオープンし、
     もしストリームへのポインタが得られなければファイルのオープンに失敗 */
  if ( ( fp = fopen(argv[1], "r") ) == NULL ) {
    printf("Cannot open file.\n");
    return 1;
  }

  /* ファイルの終わりを示すEOFになるまで、ファイルから1文字づつ読み込む */
  while( ( one_character = fgetc(fp) ) != EOF ) {
    putchar(one_character); /* 読み込んだ文字を標準出力に出力 */
  }
  fclose(fp);
}

先にデータを書き込んであったファイル: write-test.datを指定し、 このプログラムを実行した結果、 ファイルからデータを読み込むことができることを確認できる。

read-from-fileの実行結果
$ ./read-from-file write-test.dat
Hello, everybod!
書式付きファイル入力

文字列の形でファイルに記録された数値を読み込む場合や、 何らかの書式に従って記録されたデータ列を読み込みたい場合など、 予め記録形式がわかっているデータを読み込む場合、 書式付き入力のためのfscanf()関数を利用するとよい。 ただし、fprintf()関数による書式付き出力と異なり、 fscanf()関数では、空白、TAB、 および改行をデータの区切りとみなすため、 空白を含む文字列を1つの文字配列に読み込むことはできない。 例えば、先に文字列と数値の並びの列を記録した ファイル: staff-list.txtの内容を読み込み、 標準出力に出力するプログラムは以下のように記述できる。

書式付きファイル入力例: formated-read.c
/* formated-read.c */

#include <stdio.h> /* ファイル入出力のためのインクルードファイルの読み込み */

struct _person {
  char first_name[128];
  char family_name[128];
  int age;
};

int main(int argc, char **argv) {
  FILE *fp; /* ストリームへのポインタ変数の宣言 */
  struct _person list[5]; /* 構造体変数の宣言 */
  int i;

  if ( ( fp = fopen("staff-list.txt", "r") ) == NULL ) { /* ファイルのオープン */
    printf("Cannot open file.\n");
    return 1;
  }

  for( i = 0; i < 5; i++ ) {
    /* 書式付きファイル出力 */
    fscanf(fp, "%s%s%d\n",	list[i].first_name,  /* 名前と苗字が空白で区切られているので */
			list[i].family_name, /* それぞれ分けて読み込む必要がある */
			&list[i].age);

    /* 読み込んだデータの出力 */
    printf("%s %s(%d)\n", list[i].first_name, list[i].family_name, list[i].age);
	   
  }
  fclose(fp);
}

このプログラムを実行した結果、以下のように ファイルから正しくデータを読み込むことができたことが確認できる。

formated-readの実行結果
$ ./formated-read
Masayuki Torai(34)
Takoshi Yotsuashi(33)
Yuji Tameyama(33)
Yasuto Izumikawa(32)
Yuichi Ogawa(29)

注意!!

予めわかっていないデータを fscanf()関数で読み込んでははならない。 fscanf()関数による書式付きファイル入力により 文字列を文字配列に読み込む場合、 読み込んだ文字列を格納できる十分な大きさの文字配列を 予め用意しておく必要がある。 もし、用意された文字配列の大きさよりも大きい文字列を読み込もうとすると、 Segmentation faultエラーとなる可能性があるだけでなく、 意図しない変数領域にデータを書き込んでしまい、 他の変数の値を破壊し、 いわゆるバッファオーバーフローと呼ばれる セキュリティ上の重大な問題を起こす原因となる。 従って、fscanf()関数を利用してファイルの読み込みを行う場合には、 記録されたデータの内容が予め判っている場合のみに限定する必要がある。 もし、どのようなデータが記録されているか判らない場合には、 1文字づつ、もしくは指定したサイズごとにデータを読み込む関数等を利用し、 読み込んだデータの字句解析をプログラムの中で行う必要がある。

C言語では、メモリ領域の管理を言語レベルで行わないために、 このような問題が発生してしまい、 これはC言語が古いプログラミング言語であることの問題であるとも言われている。 しかしながら、データを格納する適切な領域を確保することは コンピュータプログラミング上の本質的な面でもあり、 また、もし、このようなメモリ領域の管理を言語レベルで自動で行おうとすると プログラムの効率的な実行の妨げともなり、 大量のデータを高速に処理するようなプログラムの記述には 適さなくなるといった問題もある。 今後は、用途に応じて適切なプログラミング言語を選択し、 利用することが望まれる。

標準入出力

UNIXシステムで利用される標準入力、標準出力、 および標準エラー出力の各標準入出力は、 ファイル入出力のためのストリームの一種として定義される。 すなわち、キーボードからの入力や画面への出力は、 仮想的なファイルからの読み込みや書き込みとして扱われる。 標準入出力は、プログラムの実行時に自動的にオープンされ、 標準入力、標準出力、標準エラー出力を示すストリームへのポインタは、 それぞれ、変数"stdin"、"stdout"、 "stderr"に格納される。 これらのストリームへのポインタを介し、 ファイル入出力のための各種機能をそのまま利用することができる。

表2: 標準入出力へのポインタ
  変数名  種類
stdout   標準出力
stdin   標準入力
stderr   標準エラー出力  

また、標準入力および標準出力は使用頻度が高いため、 表3に示すような、汎用的なファイル入出力関数と同じ機能を提供する 専用の関数やマクロが用意されている。

表3: 標準入出力のための関数、マクロ
関数/マクロ機能
putchar() 標準出力への1文字出力
getchar() 標準入力からの1文字入力
puts() 標準出力への文字列の出力
gets() 標準入力からの文字列の入力
printfs() 標準出力への書式付き出力
scanf() 標準入力からの書式付き入力

□ 文字列処理

文字列処理関数

UNIXシステムでは、インクルードファイル"string.h"を読み込むことで、 標準Cライブラリで提供される機能である、 各種文字列処理関数を利用することができる。 C言語では、文字列を直接扱う型がなく、文字配列により文字列を処理するため、 文字列同士の比較などを演算子を用いて単純に行うことができず、 文字配列から1文字づつ取り出して処理する必要がある。 このために標準Cライブラリでは、文字列同士の比較や文字列の結合など、 汎用的に利用される文字列処理関数を提供する。 "string.h"で提供される主な文字列処理関数を表4に示す。

表4: 主な文字列処理関数
関数機能
strcat(d, s) 文字配列sの文字列を文字配列d の文字列の後ろに複製
strcpy(d, s) 文字配列sの文字列を文字配列dに複製
strcmp(s1, s2) 文字配列s1の文字列と 文字配列s2の文字列との辞書順 (文字コード順)による比較
strstr(s1, s2) 文字配列s1の文字列の中で 文字配列s2の文字列が 最初に現れる位置へのポインタの取得
strchr(s, c) 文字配列s1の文字列の中で 文字cが最初に現れる位置へのポインタの取得
strbrk(s1, s2) 文字配列s1の文字列の中で 文字配列s2の文字列中に含まれる 任意の1文字が最初に現れる位置へのポインタの取得
strlen(s) 文字配列sで示される文字列の長さを取得

ただし、strcat()関数やstrcpy()関数により 新たに生成される文字列を格納する文字配列は、 予め十分な領域を用意しておく必要がある。 例えば、2つの文字列を空白で区切って結合した文字列を生成する場合、 以下のようにプログラムを記述する。

文字列の結合例: concatenate-strings.c
/* concatenate-strings.c */

#include 
#include 

#define MAX_STR_LEN 20

int main(int argc, char **argv) {
  char family_name[MAX_STR_LEN];
  char first_name[MAX_STR_LEN];
  char str_buffer[MAX_STR_LEN];

  printf("Family name: ");
  scanf("%s", family_name);
  printf("First name: ");
  scanf("%s", first_name);

  if( (strlen(family_name) + strlen(first_name) + 2) > MAX_STR_LEN ) {
    printf("Length of two strings is too long.\n");
    exit(0);
  }

  strcpy(str_buffer, first_name);
  strcat(str_buffer, " ");
  strcat(str_buffer, family_name);

  printf("Hi, %s!\n", str_buffer);
}

このプログラムの実行結果は以下のようになる。

concatenate-stringsの実行結果
$ ./concatenate-strings
Family name: Yotsuashi
First name: Takoshi
Hi, Takoshi Yotsuashi!
$ ./concatenate-strings
Family name: Juhachiashi
First name: Takoikashi
Length of two strings is too long.
文字列数値変換関数

整数や実数などの数値を表した文字列を実際の数値に変換したい場合には、 標準Cライブラリで提供され"stdlib.h"で宣言されている、 表5に示すような文字列から数値への変換関数を利用すると良い。 これらの関数では、引数として渡された文字配列で示された数値に基づき、 整数、もしくは実数を戻り値として返す。

表5: 主な文字列数値変換関数
関数機能
atoi() 文字配列で示される数値をint型の整数に変換
atol() 文字配列で示される数値をlong int型の整数に変換
atof() 文字配列で示される数値をdouble型の実数に変換

逆に、数値を文字列に変換したい場合もある。 このような場合には、"stdio.h"で提供されている sprintf()関数を利用するとよい。 sprintf()関数は、 fprintf()関数で出力先に ファイルのストリームへのポインタを指定したかわりに、 出力結果として得られる文字列を格納するための文字配列を指定する。

□ 乱数

シミュレーションなどで、乱数を使用したい場合がある。 一般にC言語の入門書等では、 乱数の使用のために擬似乱数を生成するrand()関数が紹介されているが、 この関数で生成される乱数には偏りが大きく、あまりよい乱数を得られない。 これに対し、比較的均一に分布する擬似乱数を生成する関数として、 標準Cライブラリで提供され、"stdlib.h"で宣言されている drand48()関数がある。

drand48()関数を利用するには、 先ず、srand48()関数を利用して乱数の基となるシードを設定する。 その上でdrand48()関数を呼ぶと、 0以上1以下のdouble型の乱数を得るので、 この値を必要に応じて加工するとよい。 例えば、1から6までの整数の乱数を drand48()関数を利用して生成する場合、 以下のようにプログラムを記述することができる。

乱数の生成例: dice.c
/* dice.c */

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

/* 1970年からの経過秒を得るtime()関数を利用するためのインクルードファイル */
#include <sys/types.h>
#include <time.h>

int main(int argc, char *argv) {
  int dice;

  /* 1970年からの経過秒を乱数シードに設定 */
  srand48( (long)time(NULL) );

  /*
    得られた乱数の値を6倍し、小数点以下を切り捨てる形で整数に型変換し、
    1を加えることで、1から6の整数を生成
  */
  dice = (int)(drand48() * 6.0) + 1;
  if ( dice == 7 ) { /* 得られた値が7となる可能性を考慮 */
    dice = 6;
  }
  printf("%d\n", dice);
}

この例では、0以上1以下の得られた乱数を6倍し、 0以上1未満を1、1以上2未満を2、...5以上6以下を6とすることで、 1から6までの整数を均等な確率で得ている (注: 5以上6以下を6とすることで、6となるの確率が高くなるが、 無視できる範囲である)。 このプログラムを実行すると、以下のような結果となる。

diceの実行結果
$ ./dice
3
$ ./dice
2
$ ./dice
6

なお、上記プログラムでは、乱数シードとして現在時刻を利用することにより、 プログラムの実行ごとに異なる乱数を得ているが(秒単位の時刻なので、 1秒以内に実行すると同じ値を得る場合もある)、 シミュレーション等で同じ乱数列を使用したい場合には、 乱数シードに同じ値を指定すれば良い。 逆に、暗号化のための鍵を生成するためなどに乱数を使用する場合、 drand48()関数はあくまで擬似乱数であり、 生成される乱数列が予めわかっているので、 乱数シードを変化させるなどの工夫が必要である。

レポート課題

■ レポート課題1

標準入力より角度と大きさを入力すると、 直行座標系により定義される二次元ベクトルの X成分およびY成分を出力するプログラムを作成せよ。 ただし、入力する角度はラディアンではなく、0から360°とすること。 実行結果には、角度60°、大きさ2を入力した結果を示せ。

■ レポート課題2

乱数を利用し、40%の確率で"大吉"、 30%の確率で"中吉"、20%の確率で"小吉"、 10%の確率で"凶"と出力するおみくじを作成せよ。

■ レポート課題3

学籍番号(文字列)、氏名(文字列)、性別(整数: 0ならば女性、1ならば男性とする)、 年齢(整数)からなる構造体を利用し、 標準入力から入力された学生名簿の一覧を各学生ごとに学籍番号、氏名、 性別(maleもしくはfemale)、年齢の順で標準出力に出力するとともに、 プログラム実行時の引数として指定されたファイルに 保存するプログラムを作成せよ。

次に、このプログラムを改良し、 プログラム実行時の引数として指定されたファイルが存在する場合、 その内容を読み込み、 またファイルが存在しない場合には、初期データなしとして、 以下の機能を選択可能なプログラムを作成せよ。

レポートには、最終的に完成したプログラム、 3名分以上を記録したファイルを読み込み、追加、変更を行い、終了した実行例、 およびこれにより保存されたファイルの内容を示すこと。

参考書籍

  1. B. W. カーニハン, D. M. リッチー 著, 石田 晴久 訳: 「プログラミング言語C 第2版 ANSI規格準拠」, 共立出版(株), ISBN4-320-02692-6, ¥2,800-
  2. 平林 雅英 著:「ANSI C/C++辞典」, ISBN4-320-02797-3, ¥6,500-

Last modified: Mon Nov 03 18:38:02 JST 2003