第 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;
  char c;
  int n=0;
  if((fh=fopen("filename.txt","r"))!=NULL){
    while((c=getc(fh))!=EOF){
      if(c=='\n'){
        n++;
      }
    }
    printf("%d 行\n",n);
    fclose(fh);
  }else{
    fprintf(stderr,"ファイルを作成できませんでした\n");
  }
}

では、次の問題です。

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

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

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

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


#include <stdio.h>
main(){
  FILE *fh;
  char c;
  int n=0;
  if((fh=fopen("filename.txt","r"))!=NULL){
    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);
  }else{
    fprintf(stderr,"ファイルを作成できませんでした\n");
  }
}

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

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

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

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


#include <stdio.h>
main(){
  FILE *fh;
  char c;
  int n=0;
  if((fh=fopen("filename.txt","r"))!=NULL){
    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);
  }else{
    fprintf(stderr,"ファイルを作成できませんでした\n");
  }
}

5-2. 関数

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


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

最初の double は返す値の型をしめしています。 次の square は関数の名前を示してます。 カッコの中は引数のリストで、型とこの関数の中で使う変数名を対にします。 もし、二つ以上引数があったら double x, double y と型を省略せずにカンマ で区切って列挙します。

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


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

また、関数は常に値を返す必要はなく、一定の処理をさせるだけということも 可能です。 例えば与えた文字を指定した回数だけ表示させる関数は次のように書きます。


void reppr(char c, int n){
  int i;
  for(i=1; i<=n; i++){
    printf("%c",c);
  }
  return ;
}

ここで void は値を返さない意味です。

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

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

.c ファイルの追加

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

.h ファイル

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


double square(double x);

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

演習5-1

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

  1. プロジェクトを一つ作ります(名前はなんでも良いです)
  2. ソリューションエクスプローラのソースファイルフォルダを右クリックし、 「追加」→「新しい項目の追加」を選びます。 C++ ファイルを選んで、ファイル名を「main.c 」にします。
  3. main.c には次のプログラムを入れます。
    
    #include <stdio.h>
    #include <math.h>
    #include "square.h"
    main(){
      double x,y;
      scanf("%lf %lf",&x, &y);
      printf("直角三角形の斜辺は%f\n",sqrt(square(x)+square(y)));
    }
    
  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-2

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

  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(){
      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");
        }
      }
    }
    
  5. ソリューションエクスプローラ中の新しいプロジェクト名を右クリックして「スター トアップ プロジェクトに設定」を選ぶ。
  6. あとは通常通りビルドと実行を行えば良い

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

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


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

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

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

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

演習5-3

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


#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;
}
main(){
  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);
}  

5-5. ポインタ

C 言語では、変数の値の格納しているメモリの番地を取り扱うことができます。 変数 x に対して、その変数の値のメモリの番地は &x で表します。 また、この番地を変数に代入して処理することもできます。 int 型の変数の番地を扱う変数の型は int * で表します。 int *x,y; と宣言すると x は int の変数の番地を取り扱うこと ができます。 x = &yで y の番地を代入できます。 また、 *x とするとこれは x に格納されているメモリの番地に入っている値 を表します。 y=1 とすると *x は 1 になります。 このメモリの番地を扱う変数を ポインタ と言います。 このメモリの番地やポインタには様々な利用法があります。

関数とのデータのやりとり

C 言語では関数の中で宣言された変数はローカル変数なので、 何の工夫もなく別の関数からその変数の値を変更することはできません。 一つの変数の値だけなら、 a=f(a) のように関数の戻り値を代 入することで f から a の値を変更できますが、二つ以上の変数に対しては行 えません。 例えば、変数 a と変数 b の値を交換することを考えます。 関数さえ呼び出さなければ次のようにすればできます。


int a=4,b=1;
int c;

c=a;
a=b;
b=c;

この処理はしばしば出てくるので、これを関数としたいところです。 ところが次のようにしても思うように動きません。


#include <stdio.h>
swap(int x, int y){
  int z;
  z=x;
  x=y;
  y=z;
  return;
}
main(){
  int a,b;
  a=2; b=3;
  swap(a,b);
  printf("a=%d, b=%d\n",a,b);
}

関数の引数の宣言での x, y はローカル変数です。 従って、呼び出す側の変数と値は同じですが、既に別の変数ですので、代入し ても呼出側の変数の値は変わりません。

そこで、ポインタを使います。 呼び出し側の変数の番地をサブルーチンに渡し、関数では、その番地に値を書 き込むのです。 次のようにすると思った通りの動きになります。


#include <stdio.h>
swap(int *x, int *y){
  int z;
  z=*x;
  *x=*y;
  *y=z;
  return;
}
main(){
  int a,b;
  a=2; b=3;
  swap(&a,&b);
  printf("a=%d, b=%d\n",a,b);
}

なお、C++ 言語では 他の関数の中で呼び出す側の変数の使用を許可することができま す。 これを、変数呼び出し(C++ 用語では参照呼び出し)と呼びます。 関数定義の際、引数の定義で変数の前に & を付けると参照呼び出しであ ることが指定できます。 次のプログラムは C 言語ではコンパイルできませんが、 C++ ではコンパイル でき、予想通りの動作をします。


#include <stdio.h>
void swap(int &a, int &b){
  int c;
  c=a;
  a=b;
  b=c;
  return;
}
main(){
  int a=3,b=4;
  printf("%d, %d\n",a,b);
  swap(a,b);
  printf("%d, %d\n",a,b);
}

配列の要素を指す

C 言語での配列名は実はポインタです。 配列 a の 0 番目の要素が格納されているメモリ上の番地が a になっていま す。つまり、 &a[0] と a は等しいです。ですから、a[0] と *a は等し いです。


#include <stdio.h>
main(){
  dpouble a[]={1.0,2.0,3.0,-1.0};
  printf("&a[0]=%d, a=%d\n",(int)&a[0],(int)a);
  printf("a[0]=%f, *a=%f\n",a[0],*a);
}

では a[1] と a の関係はどうでしょうか? double のように一文字では表せないようなデータは、複数の番地に渡って格 納されているため、 a の隣の番地はまだ a[0] の内容の続きが入っています。 sizeof 演算子を使うと、その型が何バイトの記憶領域を使うかが 分かります。 筆者の環境では sizeof (double) の値は 8 になっています。 つまり、一つの double 型の変数を格納するのに 8 バイトの記憶領域が必要 になってきます。 ですから、 a[1] の番地は a[0] の番地から 8 バイト先の位置に記憶されて います。 結局、 C 言語の配列というのは次のようにして計算された値になります。

a[i] = * ( a + i*sizeof(a[0]))

ところが、ポインタの計算では、何の型のポインタかが明らかなため、 +1 を すると、次の要素を指すように値が増えます。つまり、その型のサイズ分だけ 増えます。 結局 a[1]=*(a+1) となります。


#include <stdio.h>
main(){
  double a[]={1.0,2.0,3.0,-1.0};
  printf("&a[1]=%d, a=%d, sizeof double = %d\n",
	 (int)&a[1],(int)a,sizeof (double));
  printf("a + 1 * sizeof a[0] = %d, a + 1 =%d \n",
	 (int)a + 1*sizeof a[0], (int)(a+1)); 
  printf("a[1]=%f, *(a+1)=%f\n",a[1],*(a+1));
}

これを使うと、配列の値を次々指し示す場合に ++ などのインクリメントが使えま す。 またループ変数を無くすこともできます。


#include <stdio.h>
main(){
  double a[]={1.0,2.0,3.0,-1.0};
  double *x;
  for(x=a; *x != -1; x++){
    printf("%f\n",*x);
  }
}

5-6. 演習問題

演習5-4

ポインタを使って、文字配列 x[] の内容を文字配列 y[] にコピーするプログ ラムを書きなさい。 但し、下の例において、これ以上変数の宣言をしないこと。


#include <stdio.h>
main(){
  char x[]="abcdef";
  char y[50];
  char *p,*q;

  /* p と q を使って x の内容を y にコピーする */
  printf("%s をコピーした結果 %s となる\n",x,y);
}

演習5-5

文字配列の内容を別の文字配列にコピーする関数を書き、 x[] の内容を y[] にコピーしなさい。 但し、下の例において、これ以上変数の宣言をしないこと。


#include <stdio.h>
void copy(char *a, char *b){
  /* a の内容を b にコピーする */
}
main(){
  char x[]="abcdef";
  char y[50];
  copy(x,y);
  printf("%s をコピーした結果 %s となる\n",x,y);
}

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