探索

今回の資料には,理解の確認や考察の幅を広げるための「QUIZ(参考)」をいくつか設けています.あくまで,参考として示しているものです.特別な指示がない限り,最初は,読み飛ばしても結構です.

落穂拾い

これまで,断片的に出てきたであろう内容を少し整理しておきましょう.トピックは:

  • 関数の宣言と定義

  • 外部ファイルの取り込み

  • 整数型と文字列型の相互変換

  • 同型の変数の比較

の復習です.いずれも,これまでの解説資料や解答例において,暗黙的あるいは明示的に示されてきた内容です.

関数の宣言と定義

関数であっても,変数であっても,利用される前に,その存在を示しておく必要があります.ただし,関数については,関数の定義によって,宣言を兼ねることも可能でした.

したがって,以下の2つのソースコードは,いずれも正しい動作が期待できます.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int get_line(char* line)
{
   return 0;
}

int main()
{
   char buffer[1024];
   return get_line(buffer);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int get_line(char* line); // int get_line(char*); とも書ける(K&R p.32末尾)

int main()
{
   char buffer[1024];
   return get_line(buffer);
}

int get_line(char* line)
{
   return 0;
}

後者の例における,

int get_line(char* line);

のような記述を,(関数)プロトタイプの宣言,あるいは,単に(関数)プロトタイプと呼びます.(教科書K&R p.32)

プロトタイプの宣言をする場合は,後に出てくる定義と不一致が起こらないよう,注意する必要があります.(教科書K&R p.88)

注意すべきことは増えますが,任意の位置に関数の宣言を書くことができる,というメリットがあります.このことは,特に大規模なプログラムを書く際の大きなメリットになりえます.(教科書 K&R pp.99-100)

QUIZ(参考):

後者のような書き方で,プロトタイプの宣言を忘れると,どんなエラー(あるいは,警告)が起こるでしょうか?

外部ファイルの取り込み

これまでに何回も出てきた#include行について,もう少し考えてみましょう.

#include行の機能は,指定されたファイルをその位置に取り込む,だけです.(教科書K&R pp.105-106)

関数の宣言の話も踏まえると,以下のような事も理解できます.

例えば,4章の例malloc()を用いたプログラムがありました.この関数を利用するためには,冒頭で,#include <stdlib.h>が必要でした.

つまり,以下のような書き方をしていましたが:

1
2
3
4
5
6
7
8
9
#include <stdlib.h>

int main()
{
   int *five_numbers;
   five_numbers = (int *) malloc( sizeof(int) * 5 );
   free(five_numbers);
   return 0;
}

もしも,malloc()関数の戻り値や引数を正しく覚えているのであれば,次のように書くこともできます:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
typedef unsigned long size_t;
void* malloc(size_t);
void free(void*);

int main()
{
   int *five_numbers;
   five_numbers = (int *) malloc( sizeof(int) * 5 );
   free(five_numbers);
   return 0;
}

ここでは,内部的に何が起こっているのか,という点のみ押さえておけば十分です.

実際のところ,ライブラリ関数のように適切なヘッダファイルが提供されている場合,後者のような書き方をする必要はありません.

その他,標準的なC言語で利用可能なライブラリ関数の一覧を知りたい人は,教科書K&R「7.8節 雑関数」や教科書巻末の「付録B 標準ライブラリ」を参照してください.

QUIZ(参考):

#include行も,プロトタイプの宣言も無しにして,コンパイルするとどんな警告(あるいは,エラー)が出るでしょうか?(参考:教科書K&R 4.2節,特に p.88 の説明文)

整数型と文字列型の相互変換

文字列型から整数型への変換(1)

文字列を整数に変換するには,atoi()関数が定番です.これは,第3回解答例「練習3-3 構文解析プログラムの簡単な動作テスト」でも出てきました.利用には,適切なヘッダファイルが必要です(man 3 atoiで調べましょう).

ただし,気を付けるべき入力もあります.利用例から,注意点を把握しておきましょう.

(クリックすると利用例を開きます)
Listing 16 atoiの利用例 p06-test_atoi.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>

void atoi_test1()
{
   int i;
   char *strings[] = {
      "077", "100abc", "abc100", "abc",
      "10a0", "", NULL
   };
   for (i = 0; i < sizeof(strings)/sizeof(strings[0]); i++) {
      printf("str \"%s\" -> int %d\n", strings[i], atoi(strings[i]));
   }
}

int main()
{
   atoi_test1();

   printf("Program is finished?\n");
   return 0;
}

実行結果:

str "077" -> int 77
str "100abc" -> int 100
str "abc100" -> int 0
str "abc" -> int 0
str "10a0" -> int 10
str "" -> int 0
Segmentation fault
QUIZ(参考):

なぜ,このような実行結果になるか説明できますか?(ヒント:教科書K&R p.53 や p.75 に,ライブラリ関数のatoi()と等価な関数の実装例が示されている.)

文字列型から整数型への変換(2)

あるいは,sscanf()関数を利用した,文字列から整数への変換も考えられます.

sscanf()は,第1引数で指定したアドレスで指定された文字列から,第2引数で指示したフォーマットに基づいて,第3引数以降で指示したポインタが示すアドレスに書き込んでくれる関数です.

端的に言えば,sscanf()関数は,第2引数以降の書き方は,scanf()と同じです.

1行入力関数 get_line() の実装に関する検討で見てきたように,scanf()関数には注意点も多数あります.しかし,用途によっては,あるいは,正しく仕様を理解していれば,大きなメリットが得られるかもしれません.

利用例としては,教科書K&R「7.4 書式付き入力 --- Scanf」,特に pp.193-194 の実装例を読むのが分かりやすいでしょう.

整数型から文字列型への変換

整数を文字列に変換するには,sprintf()を使うのが簡単です.これは,第4回解答例「練習4-5 print_profile() 関数の作成とテスト」でも利用していました.利用には,適切なヘッダファイルが必要です(man 3 sprintfで調べましょう).

sprintf()は,第1引数で指定したアドレスに,第2引数以降で指示したフォーマットの文字列を書き出してくれる関数です.第2引数以降の書き方は,printf()と同じです.

sprintf()関数を用いる場合,変換後の文字列の格納先を事前に確保する必要があります.この点は,get_line()関数やnew_profile()関数の実装を考えた時と同様です.

ですから,第1引数に指定すべきアドレスは,多くの場合,文字配列へのポインタ,あるいは,malloc()関数で適切にメモリ確保されたポインタ,のいずれかを渡すケースがほとんどになるでしょう.

以下,sprintf()関数の利用例です.

(クリックすると利用例を開きます)
Listing 17 sprintfの利用例 p06-test_sprintf.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <stdio.h>
#include <stdlib.h>

void sprintf_test1(int x, int y)
{
   char buf1[16];
   sprintf(buf1, "(x,y)=(%d,%d)\n", x, y);

   printf(stderr, "Debug: %s\n", buf1);
}

char* sprintf_test2(int x, int y)
{
   /* これは適切ではない利用例 */
   char buf1[16];
   sprintf(buf1, "(x,y)=(%d,%d)\n", x, y);
   return buf1;
}

void sprintf_test3(char* buf, int x, int y)
{
   sprintf(buf, "(x,y)=(%d,%d)\n", x, y);
}

char* sprintf_test4(char* buf, int x, int y)
{
   sprintf(buf, "(x,y)=(%d,%d)\n", x, y);
   return buf;
}

int main()
{
   char *p2, *p4;
   char buf2[16];

   printf("***** (1) Test sprintf1:\n");
   sprintf_test1(1, 99);

   /*
   printf("***** (2) Test sprintf2:\n");
   p2 = sprintf_test2(2, 98);
   printf(p2);
   */

   printf("***** (3) Test sprintf3:\n");
   sprintf_test3(buf2, 3, 97);
   printf(buf2);

   /*
   printf("***** (4a) Test sprintf4:\n");
   sprintf_test4(p4, 4, 96);
   printf(p4);
   */

   printf("***** (4b) Test sprintf4:\n");
   printf("Result = %s\n", sprintf_test4(buf2, 6, 94));

   printf("Program is finished?\n");
   return 0;
}
QUIZ(参考):

コメントアウトしている (2) と (4a) は,いずれも不適切な実装や利用例です.何が問題か,説明できますか?(コンパイル時の警告メッセージにも注目してください.それでも問題が把握できない場合は,教科書K&R p.38,pp.49-50, p.116 の復習をしたほうがよいでしょう.)

その他,詳しい使い方は,教科書K&R「7.2 書式付き出力 --- Printf」,特に pp.188-189 を読むか,man 3 sprintfを読みましょう.

同型の変数の比較

整数型の比較

数値であれば,演算子==により,等価か否か,が判定できますね.大小判定が必要ならば,不等号記号を使うことも考えられます.(教科書K&R 「2.6 関係演算子と論理演算子」)

文字列型の比較

文字列どうしを比較する際,演算子==による比較では不適切である,ということは既に説明しました.(参考:比較演算子 == の扱い

strcmp()関数は,これから何回も使います.改めて,仕様を理解しておくのも良いでしょう.例えば,教科書K&Rの pp.129-130 や,man 3 strcmpが参考になります.

%Fコマンド(cmd_find() 関数)の検討

それでは,探索のためのコマンド実装を考えていきます.

まずは,演習課題説明資料 (p2-theme-all.pdf)を読み,%Fの仕様をよく確認しておきましょう.

そして,db-sampleで実装されている%Fコマンドの挙動も,事前に確認しておくと良いでしょう.(db-sampleは,必要最低限の実装となっており,十分な実装ではない場合がある.)

基本方針の検討

%Fコマンドで実装すべき機能は,%Pコマンドのそれと類似しています.(参考:練習5-3 cmd_print() 関数の実装方針の検討練習5-4 cmd_print() 関数の実装 (%Pコマンドの実現)

そこで,%Pコマンドの範囲指定機能を省いたシンプル版:

/* Global variables */
int profile_data_nitems;
struct profile profile_data_store[10000];

/**
 * Simple %P
 */
void cmd_print()
{
  int i;
  for (i = 0; i < profile_data_nitems; i++) {
    print_profile(&profile_data_store[i]);
    printf("\n");
  }
}

を起点として,実装を考えていきましょう.

%Fによる探索結果の出力形式は,%Pと同じですから,print_profile()もそのまま使えます.

QUIZ(参考):print_profile()関数のプロトタイプ宣言として,どのようなものが考えられますか?上記のソースコードだけを見て,答えてください.分からない,という人は復習が必要です:

  • C言語の関数(教科書1.7節「関数」,特に p.32)

  • ポインタと配列(教科書5.3節「ポインタと配列」,特に p.122)

  • 構造体の配列・ポインタ(教科書6.3節「構造体の配列」,6.4節「構造体へのポインタ」)

%Fとしての機能を実現するためには,「探す」ための処理の実装が必要です.「探す」ためには,「何かと何かを比較して,同じかどうか判定する」方法を考える必要があります.

ところで,profile_data_store[i]は,struct profile型ですから,これを文字列wordと直接比較することはできません(型が違います).さらにいえば,そもそも,構造体は,それ自体を何かと比較することすらできません.(参考:比較演算子 == の扱い

したがって,構造体として保持しているデータと,文字列wordの比較方法について,さらに詳細に検討する必要がありそうです.

基本方針の詳細化

「探す」処理として,配列profile_data_storeの要素各々について,与えられた文字列wordchar*型)に一致する構造体メンバがあるか調べる処理を追加することを考えます.例えば:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/**
 * Command %F word
 */
void cmd_find(char *word)
{
  int i;
  for (i = 0; i < profile_data_nitems; i++) {
    if (/* profile_data_store[i] のメンバの1つが word と一致すれば */) {
      print_profile(&profile_data_store[i]);
      printf("\n");
    }
  }
}

といった具合です.

この追加したif文の条件文としては,構造体struct profileが持つ全てのメンバについて,wordと比較して一致するかどうか,を確認しなければなりません.struct profileは,以下の形をしているので,wordと比較すべきメンバは,idnamebirthdayaddresscommentの5つです.

struct profile {
  int         id;
  char        name[MAX_STR_LEN+1];
  struct date birthday;
  char        address[MAX_STR_LEN+1];
  char        *comment;
};

この構造体の宣言から,以下のような書き方が思いつきます:

if (profile_data_store[i].id       と word が等しい ||
    profile_data_store[i].name     と word が等しい ||
    profile_data_store[i].birthday と word が等しい ||
    profile_data_store[i].address  と word が等しい ||
    profile_data_store[i].comment  と word が等しい)

||は OR (もしくは) の意味でした(教科書 K&R p.26, p.51).

では,ここで,

profile_data_store[i].id       と word が等しい ||

は,

profile_data_store[i].id == word ||

でいいのでしょうか?

・・・いいえ,話は,そんなに簡単ではありません.

例えば,ユーザが"%F 3"と入力すると,wordは,文字列(char*型)の"3"を指しています.ここで,profile_data_store[i].idが数値(int型)の3だった場合,両者は等しい,としたいわけです.

しかし,実際には,下図の状態になっていて,うまくいきません.

../_images/p06-pointer-and-integer.svg

Figure 14 ポインタと整数の比較

51は,'3'の文字コードです.この図の意味が分からない人は,解説「文字,文字列,配列,ポインタ」を復習してください.

つまり,

profile_data_store[i].id == word

は,

3 == 2000

と同義なので,当然真理値は偽です.(右辺の200051だと思った人は,まだ理解が足りません.解説「文字,文字列,配列,ポインタ」を復習してください.)

あるいは,文字列ならばどうでしょうか?例えば,

// profile_data_store[i].name     と word が等しい ||
profile_data_store[i].name == word ||

という書き方です.しかし,この書き方も,私たちが求めている機能の実装とはならないでしょう.

QUIZ(参考):この比較方法が,どのような結果になるか,先ほどと同じような図を描いて説明できますか?図が描けない,図を描いても何が問題か分からない,という人は,やはり,解説「文字,文字列,配列,ポインタ」を復習してください.

方針の整理とさらなる詳細化

ここで,理解(確認)して欲しいことは,

  • 型の異なる値(オブジェクト)同士を比較することはできない,

  • 文字列同士(あるいは,構造体同士)は,==で比較できない,

ということです.もっと一般化すると,データ型に応じて比較方法が異なるということです.

ですから,%Fの実装にあたっては,各メンバのデータ型に応じて,文字列wordに対する比較方法を,それぞれ検討する必要があります.ここまでに検討したデータ型の観点を踏まえれば,メンバを以下の3種類の型に分類して考えればよさそうです.

文字列 (char*) 型

name,address,comment

数値 (int) 型

id

構造体 (struct date) 型

birthday

さあ,ここから,どのような詳細化をしましょうか?

「型が異なると比較できない」としても「両者が比較できるような型に変換してやれば,比較でき」そうです.構造体の比較については,記憶力が良い人は見覚えがあるかもしれません.

実装と動作確認

以上の解説を踏まえつつ,皆さん自身の手で(頭で?),探索のための方針を検討し,実装してみましょう.

動作確認も忘れずに.

まずは,名簿管理プログラムとは別に,簡単な検証用のプログラムを書いてみて,その効果を比較・検討することから始めると,効率的に実装を終えられるでしょう.

また,今回の資料では,QUIZ(参考)をいくつか設けています.一部のQUIZ(参考)については,その回答を検討することが,上記の実装方針の検討のヒントになるかもしれません.