C言語のその他有用知識

最終更新: 2011/02/10 19:35:50 [| ]  最終更新: 2011/02/10 19:35:50

このページについて

ここまでのところで使用しなかった、C言語のその他の有用な主要文法をここでは扱います。


関数

関数をつくる

C言語のプログラムの特徴の一つに、 関数 があります。 関数は、文字通り、sin(theta)やsqrt()のような数学の関数もありますが、より広い解釈で、
 「なにか値を渡すと、何か結果が帰ってくる」
というものになっています。
一般には、特定の順番で特定の型(int や double など)の値を渡し、特定の型の値が返ってきます。たとえば、一般的なsin()はdoubleな値を渡すと、doubleな値が帰ってきます。 この関数に渡す値(引数、"ひきすう"とよぶ)と、返ってくる値(返(り)値、かえりち)は、関数ごとに定義されています。
※有名な例外はprintfで、これは最初の文字列以外は不特定の引数をもらう仕掛けになっている。普段使わないが、返値は出力した文字数。

この関数は自分で作ることもできます。
一般に関数は「特定の処理をまとめる」ために作ります。 たとえば、同じ手順を、一連の作業の中で何回も使う場合、毎回同じことを書くのは大変なので、一回関数として書いてしまって、必要なときにその関数で処理する、という使い方です(他のプログラミング言語では「サブルーチン」と呼ばれることもある)。
もう一つは、あまりに手順が長くなりすぎてうっとうしくなったときに、仕事のブロックごとに切り分けるという使い道です。

C言語でプログラムを作る、ということは、大抵は自分の望む機能を関数の形で積み上げていって、処理をさせるということです。

普段、プログラムをつくる際の「int main(void) { ... }」も実は「プログラムが実行されると必ず呼び出される関数」です。
返値はint=整数で、プログラムの実行結果などをプログラム終了後に使うことができるようにする値です(とはいえ、整数1個なので、正常終了したかどうかくらい)。
一方、引数は「void」ですが、これは「なにもなし」を意味します。
つまり、このmain関数は、「引数ゼロ個、返値int」という関数です。
※mainには引数として、実行時のオプションをもらう形式もある。

ちなみに、乱数のところでつかった rand()関数、srand()関数は、
  int rand(void) =引数ゼロ、返値int
  void srand(unsigned int seed)
   =引数1個(unsigned=正の数だけの int)、返値なし
です。rand(10)のように引数voidな関数に値を渡すことはできませんし、逆に返値voidな関数を a=srand(10) のように値として使うことはもちろんできません。

なお、引数はいくらでも増やせますが(システム上の制限はある)、返値は一つだけです。複数の値を返したい場合は、別のテクニックが必要ですが、難易度があがるので、ここでは、扱いません。

実際に、プログラムを作って動かしてみます。

// func1.c
#include <stdio.h>

double func(double a)
{
  return a*a;
}

int main(void)
{
  int i;
  double x,y;

  for(i=0;i<10;i++)
    {
      x=i;
      y=func(x);
      printf("%d %f %f\n",i,x,y);
    }
  return 0;
}
$ gcc -o func1 func1.c
$ func1
0 0.000000 0.000000
1 1.000000 1.000000
2 2.000000 4.000000
3 3.000000 9.000000
4 4.000000 16.000000
5 5.000000 25.000000
6 6.000000 36.000000
7 7.000000 49.000000
8 8.000000 64.000000
9 9.000000 81.000000
$
ここでは、数学的な関数 double func(double) 、引数がdoubleな小数1個、返値もdouble、を作っています。
まず、関数は呼び出す前に、その形式が分からないと使えません。 そのため、main で使う前に、その仕様を書いておきます。
※ここでは仕様と中身をまとめて書いているが、仕様だけ先に決めておいて、中身は別途、という書き方もある。

関数は、

返値の型 関数名(引数1の型 引数1の名前, 引数2の型 引数2の名前, ...)
{
    関数の中身
}
という記述で作ります。もし、引数がなければ、voidと、一つ以上あれば、それを書き並べます。引数の名前、は、関数が使われたときの1番目の値、2番目の値...を、この関数の中で処理するときにつける名前です。 この関数が使われるときに、この名前の変数を引数にしないといけない、わけではありません。
関数から値を返すときは、
  return 値;
とします。その値が関数を使った側に返値として戻ります。
なお、returnのあとはいくら関数に内容が書いてあっても実行されません。 返す値を決めたけど、まだ後始末がある、という場合は、適当なところに値を補完しておいて、後始末をして、その後でreturnする必要があります。
※処理の内容に応じて、returnは複数あってもかまわない
※returnせずに関数を最後まで実行すると、そこで自動でreturnする
※ただし、返値が変になるのvoid以外の場合は必ずreturnする

関数を使うときは、

  r=関数名(引数1, 引数2, ...);
とします。rには返値が入ります。また、式の中で直接「関数名(引数..)」を変数と同じように使うこともできますし(これまで、sin()などを使ってきたように)、別の関数の引数にいきなり使うこともできます。 また、返値が不要なら、上の「r=」もいりません(printf();のように)。

上のプログラム例では、main()のなかで y=func(x); という形で、関数が使われています。 ここで、mainの手順は一旦置いておいて、値xを引き継いでfuncを実行します。
funcに入ったら、渡された値(mainではxだった)がaで使えます。

注意点としては、渡された値である、aをfuncの中で別の値にしても、main側のxには何の影響もありません。一般に便利ですが、不便に感じることもあります。

やってみよう


自分自身をつかう関数

上で説明したように、関数に渡された値は、その関数の中だけのものです。
同じように、関数のなかで作った変数は、その関数のなかだけで有効です。 「引数の名前」で作られるものも、その関数の中だけで有効な変数の一種です。

もうすこし厳密には、関数は自分が呼び出されると、真っ先にメモリ上に自分が今、作業するのに必要なメモリを確保します。 そこに変数をおいたりして作業をします。
関数の処理が終わると、そのメモリを捨てて終わります。

「呼び出されたときに」確保するため、「自分で自分を呼ぶ」ということもできます。 まず呼ばれたときに、作業場所を確保します。その作業途中で自分自身を呼び出すと、それまでの場所とは別に新たに場所を確保して作業をします。
これを「再帰呼び出し」といい、ときどき使い道があります。

ここでは、階乗
n!=1\times2\times3\times\cdots\times(n-1)\times n
を求めてみます。

まず、n!を計算する関数を fact(n)とします。
ここで、
n!=(1\times2\times3\times\cdots\times(n-1))\times n
なので、
  fact(n)=fact(n-1)*n
です。加えて、
  1!=1 (そのまま)、0!=1 (期待される性質的に規定)
なので、fact(n)のすべき処理は、

となります。これをプログラムにすると、以下のようになります。
//func2.c
#include <stdio.h>

int fact(int n)
{
  if(n<0)   // 往々にして、エラーへの対応に手間がかかる
    {
      printf("fact error\n");
      return 0;
    }

  if((n==0)||(n==1)) return 1;  // 0!=1, 1!=1, "||"は「もしくは」
  else return fact(n-1)*n;      // n!=(n-1)!*n
}

int main(void)
{
  int i;
  for(i=0;i<10;i++)
    {
      printf("%d!=%d\n",i,fact(i));
    }
  return 0;
}
$ gcc -o func2 func2.c
$ func2
0!=1
1!=1
2!=2
3!=6
4!=24
5!=120
6!=720
7!=5040
8!=40320
9!=362880
$
この中の、
  return n*fact(n-1);
は、実際には、
  1. 自分自身を使って、(n-1)!=fact(n-1)を計算する。→一時的に「今の」fact(n)の処理は中断
  2. n* の計算をする
  3. その結果をreturnする
という処理になります。

試してみよう


組合せの数

「人が5人います。そこから3人を選び出す組合せは何通りあるでしょうか。」

(1,2,3)(1,2,4)(1,2,5)(1,3,4)(1,3,5)(1,4,5)
(2,3,4)(2,3,5)(2,4,5) (3,4,5)
の10通り

このような組合せの計算は、主に受験数学の「問題のための問題」に使われることが多いのは確かですが、「すべてチェックする」的な作業をするとき、最初に総数を確認しておいて、チェック漏れがないかを確認することにも使えます。
ここでは、「複数の関数をつくる題材」として、計算してみます。

一般に、n個のものから、m個を選び出す組合せの数は、
_nC_m=\frac{n\times (n-1)\times \cdots \times(n-m+1)}{m\times \cdots \times 1}=\frac{n!/(n-m)!}{m!}=\frac{n!}{m!(n-m)!}
で計算されます。それを計算する関数 combi(n,m)を作ってみましょう。

//func3.c  (factは同じ)
#include <stdio.h>

int fact(int n)
{
  if(n<0) 
    {
      printf("fact error\n");
      return 0;
    }

  if((n==0)||(n==1)) return 1;  // 0!=1, 1!=1
  else return fact(n-1)*n;      // n!=(n-1)!*n
}

int combi(int n,int m)
{
  if((m>n)||(m<1)||(n<1)) 
    {
      printf("combi error\n");
      return 0;
    }
  return fact(n)/(fact(m)*fact(n-m));
}

int main(void)
{
  int i;
  for(i=1;i<6;i++)
    {
      printf("C(5,%d)=%d\n",i,combi(5,i));
    }
  return 0;
}
$ gcc -o func3 func3.c
$ func3
C(5,1)=5
C(5,2)=10
C(5,3)=10
C(5,4)=5
C(5,5)=1
$
ここまで来れば、説明するまでもありません。
複数の引数を持った関数をつくれますし、そこから別の関数を何度も使うこともできます。
このプログラムまで無事に動作したら、このプログラムを課題番号r07として提出してください。
この際、自分なりの改造(改造と言えそうな改造)がしてあれば、プラスα評価します。


配列・構造体

実際にいろいろな処理をしていくと、データの扱い方が重要になります。

たとえば、30個の値をまとめて扱いたいという場合に、いちいちバラの変数をa1,a2,a3...のように作っていくのは面倒です。 また、ベクトルの要素や座標のように(x,y,z)は常にセットで使う、という場合もあり、これもまとめておきたいところです。
以下では、そういうデータの扱い方を2種類示します。

配列

たとえば、同じ意味を持ったデータ、50個の部品の長さを測った値のようなものにまとめて一つの名前を付けてしまうのが配列です。

といっても、特殊なことはなく、

    int/double 変数名[個数];
のように、変数をつくるときに、直後に"[個数]"をつけます。使うときは、
    変数名[番号]
のように、"[番号]"をつけます。
この番号は0〜「最初に指定した個数−1」です(ゼロから始まるので、最後が−1される)。
負の値も、指定した個数以上の値もNGです。
それらを使うと、たいていの場合プログラムの誤動作の原因になり、プログラムが強制終了する場合もあります(むしろ、強制終了してもらった方が間違いに気づきやすい)。

いまはメモリが潤沢なので、とりあえず使いそうな上限よりも多くの個数をとっておくのは一つの手です。
(ただし、それを越えたら危険なので、厳密には各種チェックが必要。それを放置したものが、いわゆる「セキュリティホール」の最大の原因)
負の値、たとえば、「−100〜100」が欲しいという場合は「201個」を確保しておいて、使うときに常に「番号+100」しておけばよいでしょう。

//elm.c
#include <stdio.h>

int main(void)
{
  int x[10]={5,6,7,8,9,10,1,2,3,4};
  int i;

  x[7]=77;
  
  for(i=0;i<10;i++)
    {
      printf("x[%d]=%d\n",i,x[i]);
    }
  return 0;
}
$ gcc -o elm elm.c
$ elm
x[0]=5
x[1]=6
x[2]=7
x[3]=8
x[4]=9
x[5]=10
x[6]=1
x[7]=77
x[8]=3
x[9]=4
$
この例では、最初に変数xを10個の配列としてつくります。
さらに、最初に一括で値を代入してしまいます(注:一括代入ができる場合は限られる)。
その後、x[7]だけ、別に値を代入して値を変えてしまっています。

余裕があれば、forの値の範囲を変えて、「扱いが悪かったとき」どうなるかを試してみると良いでしょう。

なお、このように配列として作った変数x[]は、単にxとして値を参照することはできませんし、同じように作ったy[], z[]に対して、z=x*yとして全部の要素の計算をしてくれたりもしません。forなどで繰り返し計算する必要があります。

構造体

「同じ性質のもの」を複数束ねるのが配列であるのに対して、「複数種の値をひとまとめにしておく」ときにつかうのが構造体です。

構造体は、多少やっかいです。

構造体の形式を決める(プログラムで冒頭で1回あればよい):

struct 構造体の形式名
{
   int なんたら;
   double かんたら;
     等、セットにする変数をならべる
};

構造体の実体をつくる(必要な数だけ変数の実体をつくる):

   struct 形式名 変数の実体の名前;

構造体を使う

   変数の実体の名前.構造体内部の変数の名前
実際のプログラムを触ってみた方が、具体的に分かると思います。
//struct.c
#include <stdio.h>

struct point
{
  double x,y,z;
};

int main(void)
{
  struct point p[3]=
    {
      { 1,2,3 },  // p[0]のx,y,z
      { 4,5,6 },
      { 7,8,9 },
    };
  int i;

  for(i=0;i<3;i++)
    {
      printf("p%d = (%f,%f,%f)\n", i, p[i].x , p[i].y, p[i].z);
    }
  return 0;
}
$ gcc -o struct struct.c
$ struct
p0 = (1.000000,2.000000,3.000000)
p1 = (4.000000,5.000000,6.000000)
p2 = (7.000000,8.000000,9.000000)
$
この例では、「座標を表すための変数」として point という形式を定めます。
point には、double で、x,y,z という変数を含みます。
main()では、このpoint形式の変数 p を、3個セットの配列として作っています。
つまり、変数pは、p[0], p[1], p[2]各々にx,y,zが入った状態になっています。
このプログラムでは、ただ値を表示しているだけですが、もちろん式の中で使ったり、代入などもできます。

実際に実用的なプログラムを作る場合には、構造体を構造体に含めることもあります。
たとえば、構造体「三角形」{頂点1,頂点2,頂点3}のようになります。この場合、「.」で順次「中身」を触ることができます。


より深く学ぶには

この講義では、敢えて、「基本的な文法」「基本的な事項」を置き去りにして、「目的優先」のスタイルをとりました。
細かな文法は、言語が変われば変わりますし、実際に使うときに細かいことを勉強し直せばよいという発想で、むしろ「コンピュータでこういうことができる」という体験を優先したためです。

表計算については、この上で学ぶべきは「どんな関数があるか」だと考えます。 実際にやりたい作業を表現する関数を知らなければ、その作業ができないか、大幅に効率が悪くなります。これは、体験あるのみでしょう。

C言語については、

あたりが重要かと考えます。

この講義で、「コンピュータに計算させること」に興味を持ったならば、2年生以降のコンピュータ演習系科目を受講するなり、C言語の参考書を一冊全部試してみるなり(多くの参考書のプログラムは、この講義での方法で、演習室では試せるはず)、実際に「やってみる」のがいいと思います。


熊谷正朗 [→連絡]
東北学院大学 工学部 機械知能工学科 RDE
[| ]