Cプログラミング入門 第4回


1.9 文字配列

 C言語は,システム記述用の言語として発達したので,文字列を扱うことが 多いということは,はじめに述べたとおりです。このような場合, 長い文字列をどこかに格納する必要が出てきます。このときに, 『文字配列』を使います。

 いつものように,This is a pen. という,文字列を考えます。この文字列は,
       \n

ですから,あらかじめこれだけのメモリ領域(17バイト)を,文字配列 x[ ]と して用意する必要があります。
                   

そのための,配列の宣言はこうです。

char x[17];
それぞれの領域には0〜16までの番号がついていますから,例えば, x[8]には'a'という文字が入っていることになります。任意の長さの文字列に 対応するためには,予想され得る最大長(例えば教科書では 1000 )だけ 配列を切っておけば良いでしょう。


< 問題 >
一連の行を読み込んで,一番長い行をプリントするプログラムを書け。 (行の長さでなくその内容をプリントする)

 いまのところ私達が出来るのは,簡単な関数を書くことと,教科書で やってきたことくらいのものです。簡単なことしか出来ません。それならば, 与えられた問題も簡単化してやれば良いのです。いきなりプログラム全体を 書きはじめると,玉砕は必至です。この例題をもとに,プログラムの組み方を 説明します。

 教科書にはこう書いてあります(少し変えてありますが)。

while(別の行がある)
  if(以前に一番長かった行よりも長い)
	その長さを格納する
	その行を格納する
一番長い行をプリントする

これで,次のページのプログラムがすぐに書けるわけではありませんが, これがスタートです。これを吟味するとつぎのような改良点(問題点?) と解決策が出てきます。

 ここまで書くと,次のような全体像(すなわちmainの中身)が見えてきます。

#標準入出力ファイルの読み込み
#入力可能な行長を1000文字としよう。

getline(現在の行,リミッタ) → 戻り値は,現在の行の長さ
copy(出力 ,← 入力) → 戻り値は,不要

現在の行の長さ        int len
今までに一番長かった行の長さ int max
現在の入力行         char line
今までに一番長かった行    char longest

今までに一番長かった行の長さを0にリセット
while((getlineで現在の行長を計る)これが0以上なら)ループへ
  if(現在の行長>今までに一番長かった行の長さ)ならば
	max(一番長かった行の長さ)を現在の行長でおきかえて
	copyで今までに一番長かった行に現在の入力行をコピー
if(行が存在した)ならば
  結果(今までに一番長かった行)をプリントする

ここで,「if(行が存在した)ならば」というのを入れたのは, 改行を一度もせずにEOFが来た場合に対応するためです。これで次のような プログラムが書けます(教科書と同じ)。

#include
#define MAXLINE 1000

int getline(char line[], int maxline);
void copy(char to[], char from[]);

main()
{
  int len;
  int max;
  char line[MAXLINE];
  char longest[MAXLINE];

  max = 0;
  while( (len = getline(line,MAXLINE) )>0 )
    if (len > max){
    max = len;
    copy(longest, line);
    }
  if (max > 0)
    printf("%s", longest);
  return 0;
}

ここで,ありもしない関数getlineやcopyをつかってプログラムを 書いていることに注意! Cでは,機能さえ分かっていれば関数は使い放題, ということを思い出して下さい。後で作れば良いのです。変数もmainの中で 使うものだけを考えておけば良いのです。


 さあ次は,getlineを作りましょう。最小限の機能としては,文字数の カウントプログラムを使えばいけます。

  1. 配列を用意しておく
  2. genchar( )で1文字づつ取る
  3. 文字数を数える
  4. 文字配列に,文字を入れていく
  5. '\n'がきたらおしまい
  6. 文字数を返す
これをもとに,関数getlineを書けば,

int getline(char s[ ], .....)
{
  int c, i;

  for(i=0; (c = getchar())!='\n'; ++i)
    s[i] = c;
  return i;
}

となります。パラメータの char s[ ]に1000が入っていないのは,mainで既に, 定義済みだからです。第1近似ではこれで良いのですが,教科書と少し違いますね。 教科書では,

  1. 文字列の最後にヌル文字 '\0' をつける。Cでは,文字列の終りを '\0' で 認識するため,これにあわせた,ということです。
  2. リミッタを越えた場合に,エラーが起きないように入力を止めて, ヌル文字 '\0' をつける。
  3. 改行なしでいきなりEOFになった時にも対応できる。
といった機能を備えているため,上のものより少し長くなっています。

 copyはもっと簡単です。ある文字配列 from[ ] から別の文字配列 to[ ] に行をコピーするということは, from[0] の文字を to[0] に代入し, from[1] の文字を to[1] に代入し,.....で, from[i] の文字を to[i] に 代入していくということです。終りはもちろん,ヌル文字 '\0' です。 教科書に対抗して,for 文で書いてみましょう。

void copy(char to[ ], char from[ ])
{
  int i;

  for(i=0; (to[i] = from[i]) != '\0'; ++i)
    ;
}

戻り値がない時には,明示的に 'void' と書きます。

 これで,できあがりです。このように大きな所から,少しづつプログラムを 完成させていくのが,Cの面白い所です。最終的には,1画面に入る程度の 長さの関数の群れとして,プログラム(main関数)が完成するはずです。

ちょっと余談
現場での(比較的大きな)開発作業では,
  1. 基本設計(外部スペックなど)
  2. 詳細設計(機能わけ,状態遷移の設計,部品である関数の スペック決定など)
  3. 個々の関数の設計(アルゴリズム)
  4. テストケースの作成
  5. コーディング
  6. 関数毎のテスト
  7. 関数を組み合わせたテスト
  8. システム全体でのテスト
  9. 第三者によるテスト
  10. 客先での実地テスト
てなぐあいで進行して行くそうです。当然,各工程毎に会議をして, 設計に誤りが無いかの第三者チェック(通常はゼミ形式の会議を行う) を行います。このような手順を「ウォーターフォールセオリー」と言うそうで, 日本に紹介したのがJAIST情報の「落水」先生であります。


1.10 外部変数と通用範囲

 ここも簡単に話をします。今までのプログラムの書式を思い出して下さい。

#include

#記号定数の定義

プロトタイプ
  :
  :
main( )
{        }
関数1( )
{        }
  :
  :

main やそれぞれの関数の中で,変数が使われているのは今まで やってきたとおりです。この変数の性質をもう一度まとめると, つぎのようになります。引数と混同しないように。

  1. それぞれの関数が呼ばれた時のみ存在する(関数間で独立)。
  2. 関数から制御が離れると消える。
  3. 関数の入口でセットする必要がある(さもないと不定値が入る)。
3. については今まで指摘しませんでしたが,そのつもりで見なおすと, それぞれの関数の始めの方で,必ず初期値が入っていることに 注意して下さい。for 文などを使っている場合もあります。
 特に,1. と 2. の性質から,これらの変数は『局所変数』とか 『自動変数』と呼ばれます。

 これと対象的に,関数の外で定義される『広域変数』とか『外部変数』と 呼ばれる変数があります。例えば次のようにすると,mainを含む 全ての関数で使用できる『広域変数』が定義できます。

#include

#記号定数の定義

『広域変数の定義』

プロトタイプ
  :
  :
main( )
{        }
関数( )
{        }
  :
  :

このやり方は小さいプログラムの時はある程度有効ですが,『広域変数』は, 関数から制御が離れても消えてなくならないため,プログラムの至る所で (好むと好まざるとに関わらず)値を変えていく可能性があります。 このように「扱いにくい」変数を用いるとデバッグも大変です。『広域変数』 はなるべく使わないようにしましょう。
 いわゆる,良くないプログラムなので,39ページのは説明しません。

 関数の外で定義されるからといって,『記号定数』と『広域変数』を 混同しないこと! 前者はもともと定数であるという,決定的な違いが あります。『記号定数』はどんどん使用しましょう。

ちょっと余談
 ちなみに,K&Rは既に古典となっており,実状(=最近の流行)とは, あわないところが出てきているそうです。
 プロトタイプ宣言が先頭にあって,main() が関数の先頭にあるのも, K&Rの流儀で,最近は, main() はいちばん最後ということが 多いようです。プロトタイプは, #include で読み込んで, 他のファイルの定義との整合を取るために使います。

 将来,Cを使う時には柔軟に対応して下さい。


次へ進む
C入門もくじへ
小矢野のホームページへ

このページの御感想・御意見は koyano@jaist.ac.jp まで。