第 5 回 関数

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

5-1. 前回の演習の復習

ファイル処理

前回はファイルの処理と言うことで getc() 関数や fprintf() 関数 を使って文字の入出力を行いました。 演習最後の問題として、 10 文字ずつ区切って出力するという問題を出しまし た。 それを解く上で、段階的に考えるため、以下のようにさらに二問の問題を解い ていくことにします。

  1. ファイルの中の行が何行あるかを数えるプログラムを書きなさい。
  2. ファイルの中の各行がそれぞれ何文字あるかを出力するプログラムを書き なさい。
  3. 入力ファイルに対して、一行が長い場合、 10 文字ずつ折り返すプログラムを 作りなさい。

問: ファイルの中の行が何行あるかを数えるプログラム

まず、ファイル中の行とは、 '\n' で終る文字列です。 厳密性を考えなければこれは '\n' がいくつあるかを数えるプログラムを書け ば良いです。 したがって、このプログラムは次のようになります。


#include <stdio.h>
main(){
  FILE* fh;
  errno_t errorcode;
  int c;
  int n;
  if((errorcode=fopen_s(&fh,"filename.txt","r"))!=0){
    fprintf(stderr,"ファイルを読み込めませんでした。エラーコードは %dです。\n",errorcode);
    return errorcode;
  }
  n=0;
  while((c=getc(fh))!=EOF){
    if(c=='\n'){
      n++;
    }
  }
  printf("%d 行\n",n);
  fclose(fh);
}

では、次の問題です。

問: ファイルの中の各行がそれぞれ何文字あるかを出力するプログラム

各行に何文字あるかを調べるには、 '\n' が来るまで文字数を数えてそれを出 力します。 そして、次の行をまた 0 から数えます。 但し、「'\n'が来るまで」ですが、単純に while は使えません。 それは、このプログラムは EOF を読んだら終了しなければならないからです。 そこで、入力の種類によって動きが変わることを表により分析します。

入力の種類 作業内容 次の動作
EOF 終了処理終了
'\n'文字数の出力、カウンタのリセット繰り返しへ
普通の文字カウンタを増やす繰り返しへ

この分析だと、EOF 以外は繰り返しを変化させる条件ではありません。 従って while の条件には EOF だけを書けば良いことがわかります。 あと、EOF が行の途中に来ることを考えるとうまくありません。 その時はそこまでの文字数を出力するのが自然な動きだと思われます。 従って、EOF で繰り返しが終ったあと、行の途中までの文字数を出力するよう にします。 これらをまとめると次のプログラムになります。


#include <stdio.h>
main(){
  FILE* fh;
  errno_t errorcode;
  int c;
  int n;
  if((errorcode=fopen_s(&fh,"filename.txt","r"))!=0){
    fprintf(stderr,"ファイルを作成できませんでした。エラーコードは%dです。\n",errorcode);
    return errorcode;
  }
  n=0;
  while((c=getc(fh))!=EOF){
    if(c=='\n'){
      printf("%d文字\n",n);
      n=0;
    }else{
      n++;
    }     
  }
  if(n>0){
    printf("%d文字\n",n);
  }
  fclose(fh);
}

問: 入力ファイルに対して、一行が長い場合、 10 文字ずつ折り返すプロ グラム

このプログラムでも、文字数を数えます。10文字未満の時と、10文字 になった時で動作が変わります。また、改行文字が来ても動作が変わります。 さらに EOF が来ても変わります。 文字として、改行、EOF、その他の文字が来る場合の三通り、一方、10文字以 下と10 文字の二通りで、六通りの場合が存在しますので、表にして動作を考 えます。

その他の文字改行EOF
10文字より少ない文字を表示、カウンタ増やす改行、カウンタをクリア 終了
10文字改行、文字を表示、カウンタを1 に改行、カウンタをクリ ア終了

まず、最後まで文字を読み続ける一番外側の while 文に関係あるのは、終了 条件である EOF のみです。 また、10 文字という条件に関係なく改行が来たら改行してカウンタをクリア します。 その他の文字が来た場合ですが、 「10 文字」の条件を 「改行、カウンタをクリア、文字を表示、カウンタを更新」と変えると、ちょ うど 10 文字の時、改行してカウンタをクリアした後は通常と同じ処理で済ま せられます。 以上により、このプログラムは次のようになります。


#include <stdio.h>
main(){
  FILE* fh;
  errno_t errorcode;
  int c;
  int n;
  if((errorcode=fopen_s(&fh,"filename.txt","r"))!=0){
    fprintf(stderr,"ファイルを作成できませんでした。エラーコードは%dです。\n",errorcode);
    return errorcode;
  }
  n=0;
  while((c=getc(fh))!=EOF){
    if(c=='\n'){
      printf("\n");
      n=0;
    }else{
      if(n>=10){
        printf("\n");
        n=0;
      }
      printf("%c",c);
      n++;
    }      
  }      
  fclose(fh);
}

5-2. 関数

プログラム中で特定の部分をまとめて一つの関数とすることがで きます。 関数を作ると、関数名を指定することにより何度でもその部分を呼び出して実 行することができます。 例えば数を二乗する関数は次のように作れます。


double square(double x){
  return x*x;
}
double times(double x, double y){
  return x*y;
}
main(){
  printf("%f %f ",square(3.0),square(4.0));
  printf("%f",times(5.0,6.0));
}

最初の double は返す値の型をしめしています。 次の square は関数の名前を示してます。 カッコの中は引数のリストで、型とこの関数の中で使う変数名を対にします。 もし、二つ以上引数があったら double x, double y と型を省略せずにカンマ で区切って列挙します。 なお、引数がない関数は定義時に void を指定します。 関数宣言の時の引数を仮引数と言います。 一方、作成した関数に対して、その関数を式の中で使うと、与えた引数の値 が仮引数にコピーされて、関数のプログラムが実行されます。 この式の中で与える引数を実引数と言います。 なお、C言語ではこのように関数の計算をする際に、必ず仮引数に実引数の値 がコピーされます。 このような呼び出し方式を値呼び出しと言います。 他のプログラミング言語では、値呼び出し以外の呼び出し方式として、変数呼 び出し(参照呼び出し)、名前呼び出しなどの方式があります。 値呼び出し方式の特徴として、関数から変数の値を変更することができません。 なお、C言語では次章で学ぶポインタを使用することで、変数の値を変更させ ることができます。

関数定義の最後に返す値の型と同じ値を return 文で指定して終ります。 return は関数の中にいくつ書いても良く、処理がそこに達したときに、値を 返して、関数の処理を終了します。 なお、何も返さない関数を定義する時は void を宣言し、 return には引数を指定し ません。

例5-1

void hello(void){
  printf("Hello\n");
  return ;
}

main 関数

なお、今まで main() {} の中に C 言語のプログラムを書いていましたが、 これ自身も関数宣言です。 C 言語では main(){} という宣言は、引数なしを示す void が省略され、また、 返す値の型は int であると言う暗黙の規則があります。 したがって、この main(){} という記述は int main(void){} と等価です。 また、本来は return 命令で整数値を返す必要があります。 main 関数が返す値は起動した OS などに渡されます。今まで return を省略 して値を返しませんでしたが、こうすると OS に返される値は不定になってま した。 実際は使ってなかったので、問題はありませんでしたが、今後は本来の決め事 に従って、正式な記法で記述して行きます。 つまり、今後、プログラムを正常に終了させる時には main 関数の終わりに return 0 を書きます。

なお、 main 関数には起動時にパラメータを渡すために、 int main(int argv, char* args[]){}という別の定義もありま すが、この講義では取り上げません。

関数利用例

これを利用すると直角三角形の二辺から斜辺の長さを求めるプログラムは次の ようになります。


#include <stdio.h>
#include <math.h>
double square(double x){
  return x*x;
}
int main(void){
  double x,y;
  scanf_s("%lf %lf",&x, &y);
  printf("直角三角形の斜辺は%f\n",sqrt(square(x)+square(y)));
  return 0;
}

5-3. 参考: Visual Studio .Net を使ったプロジェクトの利用

Visual Studio .Net では関数ごとに別ファイルを作ることができます。 すると同じ関数を使用する複数のプロジェクトを作ることができます。 その方法を学びます。

.c ファイルの追加

関数ごとに別々のファイルにするにはソリューションエクスプローラでプロジェ クトを右クリックして「追加」→「新しい項目の追加」と、新規にプログラム を作る際と同様の手順で新たに .c ファイルを追加します。

.h ファイル

単純に関数が別ファイルだとコンパイル時に関数が未定義であるとエラーが出 ます。 そのため、各ファイルには関数の入出力の仕様を書く必要があります。 これをプロトタイプと言います。 上記の square であれば、次のように関数宣言で実際の定義部の代わりにセミ コロンで終るものを書きます。


double square(double x);

これをこの square 関数を使うファイルで必ず宣言します。 多くのファイルで同様の宣言をするのは煩瑣なので、これを一つのファイルに まとめます。 共通の宣言をまとめたファイルを C 言語では ヘッダファイルと 呼び、拡張子 .h のファイルとして作成します。 そして、ファイルの先頭で #include "ヘッダファイル名"と "" 記号でファイル名を指定する include 文を書きます。

例5-2

上記の直角三角形の二辺から斜辺の長さを求めるプログラムをこの手順で関数 毎に分割してみましょう。

  1. プロジェクトを一つ作ります(名前はなんでも良いです)
  2. ソリューションエクスプローラのソースファイルフォルダを右クリックし、 「追加」→「新しい項目の追加」を選びます。 C++ ファイルを選んで、ファイル名を「main.c 」にします。
  3. main.c には次のプログラムを入れます。
    
    #include <stdio.h>
    #include <math.h>
    #include "square.h"
    int main(void){
      double x,y;
      scanf_s("%lf %lf",&x, &y);
      printf("直角三角形の斜辺は%f\n",sqrt(square(x)+square(y)));
      return 0;
    }
    
  4. 次に、再びソリューションエクスプローラのソースファイルフォルダを右 クリックし、「追加」→「新しい項目の追加」を選びます。 C++ ファイルを選んで、ファイル名を「square.c 」にします。
  5. square.c には次のプログラムを入れます。
    
    #include "square.h"
    double square(double x){
      return x*x;
    }
    
  6. 次に、再びソリューションエクスプローラのヘッダーファイルフォ ルダを右クリックし、「追加」→「新しい項目の追加」を選びます。 ヘッダーファイルを選んで、ファイル名を「square.h 」にします。
  7. square.h には次のプログラムを入れます。
    
    double square(double x);
    
  8. これでビルドすると 3 つのファイルを結合して一つのプログラムが作られま す。

例5-3

次に作成した関数をテストするための別のプログラムを追加することを考 えましょう。 別のプログラムを作成して実行することになるので、新たなプロジェクトを作 ることになります。

  1. ソリューションエクスプローラの中のソリューションを右クリックし、 「追加」→「新しいプロジェクト」を選ぶ。 プロジェクト名を適当につける。
  2. テストしたい関数の C ファイルや H ファイルをプロジェクトに追加する。 C ファイルは「ソースファイル」フォルダで右クリックして、「追加」→「既 存項目の追加」を選び、該当ファイルを選択する。 H ファイルは「ヘッダファイル」フォルダで右クリックして、「追加」→「既 存項目の追加」を選び、同様に該当ファイルを選択する。

    なお、 Windows のエクスプローラを起動し、作成した C ファイルや H ファイルをソリューションエクスプローラの中の新しいプロジェクトの中のソー スファイルフォルダ、ヘッダーファイルフォルダにそれぞれドラッグ&ド ロップすることでも追加できる。

  3. ソースファイルフォルダを右クリックして「追加」→「新しい項目の追加」を 選び C++ ファイルを追加する。
  4. 追加したファイルにテスト用のプログラムを書く。 インクルードファイルの指定は次のように指定する。
    1. #include ""を打つ
    2. インクルードしたいヘッダファイルを選択し、プロパティ欄の「相対パス」を 左クリック、Ctrl-C を打つ
    3. #include ""の""の真中にカーソルを移動して Ctrl-V を 打つ
    残りのコードは以下のようになる。
    
    #include <stdio.h>
    #include "..\最初のプロジェクト名\square.h"
    int main(void){
      int in[]={0,1,2,3,-1};
      int out[]={0,1,4,9,-1};
      int i,kekka;
      for(i=0;in[i]!=-1;i++){
        kekka=square(in[i]);
        if(kekka==out[i]){
          printf("Ok\n");
        }else{
          printf("NG\n");
        }
      }
      return 0;
    }
    
  5. ソリューションエクスプローラ中の新しいプロジェクト名を右クリックして「スター トアップ プロジェクトに設定」を選ぶ。
  6. あとは通常通りビルドと実行を行えば良い

5-4. ローカル変数、グローバル変数

C 言語では関数の内部で宣言された変数は、その関数の外部(他の関数など)か らはアクセスできません。 これをローカル変数と言います。 一方、関数の外側でも変数を宣言できます。 これをグローバル変数と言います。 グローバル変数へはどんな関数でもアクセスできます。


#include <stdio.h>
int g; /* グローバル変数 */
int incg(void){
  return ++g; /* グローバル変数はどこからでもアクセス可 */
}
int main(void){
  int i; /* ローカル変数 */
  g=0; /* グローバル変数はどこからでもアクセス可 */
  for(i=0;i<5;i++){
    printf("%d\n",incg());
  }
  return 0;
}

なお、グローバル変数と同じ名前の変数をローカル変数として宣言可能です。 おなじ名前になった時は、グローバル変数にはアクセスできず、ローカル変数 にだけアクセスできます。 また、関数の宣言の時に宣言する引数は、値を読み出すことはできますが、そ れは値が与えられたローカル変数なので、書き換えた値は呼び出した側には影 響しません。

但し、グローバル変数の利用には注意が必要です。 グローバル変数は全ての関数からアクセスが可能なので、プログラムミ スなどによりグローバル変数の値がおかしくなった時など、原因の追求が大変 です。

またファイルをまたいでグローバル変数を使用する場合、 extern と宣言する必要がある。 通常この宣言はインクルードファイルに入れる。

演習5-1

次のプログラムがどのように動くか予想し、また、実際に動かして動作を確か めなさい。


#include <stdio.h>
int i;
int a(int i){
   i++;
   return i;
}
int b(int j){
   i++;
   return i;
}
int c(int k){
  int i;
  i=0;
  return i;
}
int main(void){
  i=1;
  printf("%d\n",i);
  printf("%d ",a(i));
  printf("%d\n",i);
  printf("%d ",b(i));
  printf("%d\n",i);
  printf("%d ",c(i));
  printf("%d\n",i);
  return 0;
}  

演習5-2

square 関数を利用して、 1 の 2 乗、 2 の 2 乗 ... 10 の 2 乗を表示する プログラムを書きなさい。


double square(double x){
  return x*x;
}

なお、square が double 型の変数を取り扱うことが都合が悪い場合、別の square 関数(isquare) を定義しても良い。

演習5-3

  1. square 関数を利用して、円の面積を求める関数 circle を作りなさい。

    
    double square(double x){
      return x*x;
    }
    double circle(double x){
     /* この部分を作る */
    }
    
  2. そして、半径が 1, 2, ... ,10 の円の面積を表示するプログラムを書きなさい。

演習5-4

グローバルに定義されている配列 double x[]={1.0,3.0,5.0,2.0,-1.0} のうち -1.0 は番兵とする。

  1. この配列を y 倍して表示させる関数 void larger(double y) を作りなさい。 また、この larger をテストするため、 larger に x をそれぞれ 3.2 倍、 5.0 倍したものを表示させる main 関数 を書き、動作を確かめなさい。
  2. この配列の値を y 倍する関数 void times(double y) を作りなさい。 また、 times に x を 3.2 倍させ、さらに 5.0 倍させる main 関数を 書き、動作を確かめなさい。 なお、表示には larger(1.0) を使用して良い。

なお、関数ごとにファイルに分割する場合、次のヘッダファイルを使用してく ださい。


extern double x[];
void larger(double y);
void times(double y);

そして、 main 関数の直前で配列と初期化を宣言してください。


坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科