C言語の研究3
第3章 関数
どうだろうか。もうそろそろいやになって来たのではないだろうか。
しかし、ここで挫けるわけには行かない。前進しよう。
C言語の特徴として構造化プログラミングが可能という点がある。
構造化プログラミングとは、大きなプログラムを小さな機能をもつ文、つまりモジュールに分割して設計する方法である。こうすると、修正が生じたとき、その部分だけを修正すればよいから助かる。
C言語の関数はまさにモジュールだ。
1 関数の例
C言語は、mainも関数である。void main(int argc, char **argv)と書いてきたことでも分かるだろう。
つまりmainは、引数として整数argcとポインタのポインタargvをとり、何も返さない(void)関数である。
main関数以外の関数については、前もってコンパイラにプロトタイプを宣言しておくことが必要だ。
つまりこうだ。
# include <stdio.h>
int kansu(int, int);←セミコロンを忘れないでね
void main(int argc, char **argv)
{
int x;
x = kansu(10 , 10);
}
このように、赤色部分で使う関数の型と名前と引数を宣言しておいてやることが大事だ。
すると関数本体は、
int kansu(int a, int b) ←aやbは適当に名づけていいが、並び方を間違えないでね
{
関数の中身を記述
return 整数;
}
void以外の型の関数は必ず戻り型の値をreturnしてやる必要がある。
もちろん、関数なんか使用しなくても、mainの中に全部記述することはできる。実際、今まではそうしてきた。
しかし、長いプログラムになると、やっぱり限界があるだろうね。
次に例として、10個の整数データの平均を求めて、各データの平均との差を表示するプログラムを関数を使って作成してみた。
#include <stdio.h>
int get_ave(int []); /*プロトタイプの宣言*/
void print_data(int[], int); /*プロトタイプの宣言*/
void main(int argc, char **argv)
{
int a[10]={1,2,3,4,5,6,7,8,9,10};
int ave;
ave = get_ave(a);
print_data(a, ave);
}
int get_ave(int a[])
{
int ave=0;
int i;
for(i=0; i < 10; i++) ave += a[i];
ave /= 10;
printf("平均は%dです\n\n",ave);
return ave;
}
void print_data(int a[], int ave)
{
int i, dif;
for(i=0; i < 10; i++){
dif = a[i] - ave;
printf("a[%d] = %d で平均との差は
%dです\n",i ,a[i] ,dif);
}
}
どうだろうか? main関数がこんなにコンパクトになって感じがいいね。ん?
2 変数の記憶域とスコープ
今まで、変数がどの範囲で有効かなんて全く気にせずに生きてきた、バカなおれだった。
これから関数を多用するから、変数がどの位置で宣言されたかによって、その有効範囲が変わってくる。今からこれを考えよう。
(1) 記憶域
変数を宣言するときに、constと宣言すれば値を変化できなくなると前に言った記憶がある。
これとは別に、値は変化させられるが、一度与えた初期値をずっと維持しているやつや、関数のようにブロック内だけで宣言できて、そのブロックが呼ばれるたびに初期化されるやつがある。ちょっと見てみよう。
a. 静的記憶域期間 static
こいつは、最初の初期値をずっと覚えている野郎だぜ。ちょっと試してやろう。
# include <stdio.h>
void func();
void main(int argc, char **argv)
{
func();
func();
func();
}
void func()
{
static int a = 0;
a++;
printf("a = %d\n",a);
}
この結果はa=1,a=2,a=3となっている。
b. 自動記憶期間 auto
これはデフォルトで、関数内だけで有効なやつである。autoは省略可能である。ちょっと見てみよう。
# include <stdio.h>
void func();
void main(int argc, char **argv)
{
func();
func();
func();
}
void func()
{
int a = 0;
a++;
printf("a = %d\n",a);
}
この結果は、a=1,a=1,a=1となる。
c. 外部結合 extern
こいつは、同じ名前の静的記憶域をもつ変数や関数などが、別の翻訳単位で宣言されており、それを参照するというやつだ。Visual C++でちょっと試してみよう。
#include <stdio.h>
extern int func(int);
int b;
void main(int argc, char argv)
{
int a = 100;
b=100;
a = func(a);
printf("a = %d\n",a);
}
まずこいつをワークスペースを作ってコンパイルしてみよう。
へてから、新規のCソースを次のように作成しよう。
#include <stdio.h>
int func(int a)
{
extern b;
a *= b;
return a;
}
こいつに何か適当な名前を入れて保存しておこう。それでさっきのワークスペースのプロジェクトを右クリックして、新規ファイルの追加として、このCソースを追加して、実行させましょうね。
Visual C++以外の場合は、これら2つのCソースを1つのプロジェクトに入れて、ひとつずつコンパイルしてから、リンクしよう。
すると、「a = 1000」と表示される。
これは、変数bや関数funcが外部の翻訳単位で宣言されていることをコンパイラに知らせる方法です。
(2) スコープ
スコープとは、「ギャバンスコープ!」と宇宙刑事ギャバンが敵を探していたが、それと同じことで、見えるという意味である。
a. ファイル有効範囲
これはプログラムの最初に、main関数の前に変数を書くと、ファイル全体で有効になるということだ。
たとえば、
#include <stdio.h>
int b;←こいつはファイル全体で有効だ
void main(int argc, char argv)
{
int a = 100;
b=100;
a = func(a);
printf("a = %d\n",a);
}
void func()
{
int a = 0;
a++;
printf("a = %d\n",a);
}
b. ブロック有効範囲
これは、ブロック内というか関数内で宣言された変数は、基本的にそのブロック内で有効というやつだ。
同じ名前の変数については、ブロック内の方を優先される。次の例を見て確認しておこう。
#include <stdio.h>
int a=100;
void func();
void main(int argc, char **argv)
{
printf("a = %d\n",a);
func();
}
void func()
{
a=10000;
printf("a = %d\n",a);
}
この結果は、a=100とa=10000と表示される。関数func内は、閉じた社会で、a=10000しか見えないのだろう。
c. プロジェクト内で有効な変数や定数
これは、ヘッダーファイルで宣言するとプロジェクト内の全ファイルで有効になる一例だが、ちょっと紹介しよう。
#define akr "Computer Pell"
extern int i=100;
まず、こいつをワークスペースにakr.hと命名してヘッダーファイルとして追加しよう。
そして、さっきのプロジェクトに追加する方法で、次のソース
#include <stdio.h>
#include "akr.h"
void main(int argc, char **argv)
{
extern int pell(int);
i=pell(i);
printf("%s %s %d\n",__TIME__,akr,i);
}
これを作成してコンパイルしてみよう、
さらに次のCソースを適当な名前でプロジェクトに追加しよう。
int pell(int m)
{
m +=100;
return m;
}
さて、これで実行してみよう。
時刻に続いてComputer Pell 200と表示されただろうか。
i と akrはヘッダーファイル"akr.h"で定義されているから、プロジェクト内で有効ということになるんだね。
3 再帰関数
C言語は関数の中から、その関数を別の関数として呼び出すことができる。それを再帰関数というが、私はこれが多少苦手であるとまず正直に言っておこう。
例として、1から10までの階乗の和を求めるプログラムを考えてみよう。
10の階乗は10×9×8×7×6×5×4×3×2×1、
9の階乗は9×8×7×6×5×4×3×2×1
nの階乗は n × n-1× n-2 ・・・・・ × 1
つまり、n! = n * (n-1)!という部分が共通点だから、再帰関数を使えるということである。
しかし、なんかややこしいな。とりあえず、プログラムを考えてみるか。
#include <stdio.h>
long int kaijo(int);
void main(int argc, char **argv)
{
int i;
long int a=0;
for(i=1; i <=10; i++){
a += kaijo(i);
printf("%dの階乗は%ldです\n",i,kaijo(i));
}
printf("\n");
printf("10までの階乗の和は%ldです\n",a);
}
long int kaijo(int n)
{
if(n==1)
n = 1;
else
n *= kaijo(n-1); /*これが再帰呼び出し*/
return n;
}
どうだろうか?ちゃんと表示されただろうか。
コツとしては、if〜else〜文を使い、余計なことは考えず指定されたとおりに記述するだけでいい。
if〜else〜文以外のループ文なんかは絶対禁物である。
そして、この場合は、n==1のときのように、抜け道を作っておくこと。
別に再帰関数を絶対使わなければならないというものでもない。この例題を再帰関数を使わずに書こうというなら、
#include <stdio.h>
long int kaijo(int);
void main(int argc, char **argv)
{
int i;
long int a=0;
for(i=1; i <=10; i++){
a += kaijo(i);
printf("%dの階乗は%ldです\n",i,kaijo(i));
}
printf("\n");
printf("10までの階乗の和は%ldです\n",a);
}
long int kaijo(int n)
{
int i;
long int a=1;
if(n > 1)
for(i =1; i <= n; i++) a *= i;
else
a=1;
return a;
}
このようにすればいいのじゃないだろうか。
最後に、再帰関数の絶好の例題である「フィボナッチ数列」を紹介しよう。
フィボナッチ数列とは、
f(1)=1
f(2)=1
f(n)=f(n-1) + f(n-2)
の性質を持つ数列の第n項までの総和 s(n) は、
s(1)=1
s(n)=s(n-1) + f(n) (n>=2)
により求められるというものである。
このフィボナッチ数列の第10項までの総和を求めてみよう。
#include <stdio.h>
#define SU 10
long int f(int);
long int s(int);
void main(int argc, char argv)
{
int i;
int n= SU;
for(i=1; i<=n; i++)
printf("f(%d)の値は%ld\n",i,
f(i));
printf("\n");
printf("%dまでの総和は%ld\n",n, s(i));
}
long int f(int a) /*f(n)を求める関数*/
{
long int ret;
if(a==1 || a==2)
ret = 1;
else
ret = f(a-1) + f(a-2);
return ret;
}
long int s(int a) /*s(n)を求める関数*/
{
long int ret;
if(a==1)
ret = f(1);
else
ret = s(a-1) + f(a);
return ret;
}
どうだろうか、再帰関数をC言語でプログラミングするには、与えられたとおりにif〜else〜文で、理屈を考えずに記述してくとOKということが、分かっていただけただろうか。