Lecture 5 関数(1)

関数の解説動画

この講義の録画を以下に載せました.聞き逃した人はこちらを確認してください.




今週は,関数の使い方について勉強します.

キーワード:「関数」「引数」「戻り値」


C言語をはじめとするプログラミング言語の仕様には,繰り返し現れる処理や頻繁に使われる処理を一箇所にまとめ, 効率の良い記述を行う方法が用意されています. 今日の演習で取り上げるこの仕組みは関数と呼ばれる.数学でいう関数とかなり近い. (BASICやFORTRANなど他の言語においてもサブルーチン,プロシージャと呼ばれる類似の機能が用意されている)
今までの演習で用いてきたC言語の printf(), scanf() は,実は全て関数でした. これらは「ライブラリ関数」と呼ばれ,C言語のコンパイラ(この演習で用いているのはEmbarcadero (旧Borland) C++ Compiler) にあらかじめ付属しているものであり,必要な#include文の記述により, プログラム中から自由に呼び出すことができる. そしてmain()も実は関数であるが, 「この関数から全体の処理が開始される」特別な関数として規定されている.
したがって,C言語のプログラムは関数の集合であるといえる. 関数はソースコード中においていくつも,何度も呼び出すことができる.


★ 関数は「自分の資産」になります.正しく動く関数を沢山作り貯めよう.

まずは関数の例を見てみよう

関数とは,例えて言えば材料(引数,ひきすう)を渡すと, レシピ(関数の定義)に基づいて調理し, 料理(戻り値,もどりち)を届けてくれるようなものである.

例:べき乗の計算

aのb乗,bのa乗,およびbの7乗をそれぞれ計算して, その和を表示するプログラムを見てみよう.

表5.1 関数を使えばこんなに簡素になるよ!

bekijo_test1.cpp 関数なし bekijo_test2.cpp 関数あり(おすすめ!)
#include <stdio.h>

int main(void)
{
    int a=0, b=0;
    printf("a="); scanf("%d",&a);
    printf("b="); scanf("%d",&b);

    int bekijo1 = 1;
    for(int i=0;i<b;i++){ // abの計算
        bekijo1 *= a;
    }

    int bekijo2 = 1;
    for(int i=0;i<a;i++){ // baの計算
        bekijo2 *= b;
    }

    int bekijo3 = 1;
    for(int i=0;i<7;i++){ // b7の計算
        bekijo3 *= b;
    }

    int wa = bekijo1 + bekijo2 + bekijo3;
    printf("a^b+b^a+b^7=%d¥n",wa);    
}
#include <stdio.h>

int bekijo(int c, int d) // べき乗を計算の関数定義
{
    int keisan=1;
    for(int i=0;i<d;i++){ // cdの計算
        keisan *= c;
    }
    return keisan;    // 値を返すreturn文
}

int main(void)
{
    int a=0, b=0;    // かならず初期化する.
    printf("a="); scanf("%d",&a);
    printf("b="); scanf("%d",&b);

    // 関数を呼び出す
    int wa = bekijo(a,b)  // abの計算
           + bekijo(b,a)  // baの計算
           + bekijo(b,7); // b7の計算
    printf("a^b+b^a=%d¥n",wa);
}

関数なしの場合

aのb乗の計算とbのa乗のようにとてもよく似たプログラムを繰り返し記述していますね. これは無駄で手間であるばかりでなく,似たようなコードを なんども書くことで間違い(=バグ)が入る可能性を格段に増やしてしまいます.

関数ありの場合

その一方で,関数を使うとべき乗を計算する箇所が「bekijo」という名前の関数の 呼び出しになり,一つの記述で何通りもの計算ができます. ここで「bekijo」という名前の関数は,ソースコード上方で定義していて, 整数型の「引数」を2つとり,整数型の「戻り値」を返す関数としています.

関数の定義のしかた,関数の引数と戻り値

ここでは,上の例をもとに整数型の数を2つ受けとり,計算結果を整数型で一つ返す関数を見てみる. このように,戻り値の型,仮引数の個数や型を事前に厳密に示すことを プロトタイプ宣言といいます.

戻り値の型 関数名(型 仮引数名1,型 仮引数名2, ...)
{
    // ここに処理を記述
    return xxx;    // xxxが戻り値
}

関数の書き方のルール

  • 「引数」の個数は 0(なし), 1個, 2個, 3個, ... 任意の数
  • 「戻り値」の個数は 0個または1個,つまり戻り値「有り」「無し」かのどちらか.
  • 関数の名前(関数名)は,変数名と同じく a-z, A-Z, 0-9, _ などの組み合わせ.
  • 関数名は予約語(int, float など)以外,既に定義されている関数名(main, sqrt など)以外のもの.
  • 関数内で宣言した変数(引数を含む)は,その関数内のみで有効.他の関数では使えない.
  • 呼び出し元と呼び出し先の引数の「型」および「個数」を一致させる必要がある.

関数のプロトタイプ

関数を利用するときには,変数を利用する時と同様に,必ず事前に宣言を行う必要がります. これを「プロトタイプ宣言」と呼びます. 以下の例の場合,プロトタイプ宣言がなければコンパイルエラーになります
int wa_keisan(int a, int b);  // プロトタイプ宣言が必要

int main(void)
{
    int x = 1, y = 3;
    int z = wa_keisan(x, y);    // 関数の呼び出し
    ....
}
    
int wa_keisan(int a, int b)    // 実際の関数の定義
{
    return a+b;
}


#include <stdio.h> の呪文は何だ? ついに謎が明らかに!

ヘッダファイル

本来,プロトタイプ宣言は,その関数の定義が同じソースコード内には無いことを前提としています. 例えば,既に便利に利用している関数,printf()などは, その定義は皆さんが作っているソースコードの中にはありませんね? そのかわり,stdio.hというヘッダファイルをインクルードしていますが, このヘッダファイルの中に関数のプロトタイプ宣言が入っているのです. 後で自分でヘッダファイルを作る練習もしてみよう.

プロトタイプは自分で書く? 書かないで済ませる便利な方法

しかし,上の例のように,関数本体の定義が同じソースコード中にある場合, プロトタイプ宣言は二度手間である感は否めません. このような時,関数の定義を呼び出し側より先(ソースコードの上の方)に書きます
int wa_keisan(int a, int b)    // 定義を呼び出し側(main)よりも先に書く
{
    return a+b;
}

int main(void)
{
    int x = 1, y = 3;
    int z = wa_keisan(x, y);    // 関数の呼び出し
    ....
}

この場合,プロトタイプ宣言と関数定義を同時にしたことになり,別途プロトタイプ宣言を行う必要がありません.


関数の呼び出しにともない実行される処理の順序

4_2

    1. 関数の呼び出し元(ここではmain関数内の bekijo(a, b) の部分)から「引数」a, b を伴って関数waに処理が移る.
    2. 関数が呼び出された時点での変数 a, b の値がコピーされ, c, d として受け取る.
    3. 関数内でいろいろな処理をした後,return文に到達すると,値を返す操作が行われ, ここで関数の処理が終了する.(return文以降に記述された処理は実行されない)
    4. 関数の戻り値(ここでは関数bekijo() 内の return で指定された変数 keisan の値)を,main内のwaに代入.

【ヒント】関数は型を持つ(この場合はint).returnされる型(=戻り値の型)と同じでなければならない.
【注】処理の流れ,引数(変数)のコピーや,return による値の戻りをよく理解すること.

戻り値や引数を持たない関数の例

関数の役割を考えた時,引数や戻り値が不要な場合が考えられる. そのような場合は voidというキーワードを使う. 以下に様々な引数,戻り値の組み合わせを例示する.

#include <stdio.h>

// 引数なし,戻り値無し の関数
void moji_hyouji(void)
{
    printf("単なる文字の表示です.¥n");
    return;    //  戻り値「なし」なので,returnのみ記述する.この場合は省略可
}

// 引数あり,戻り値無し の関数
void wano_hyouji(int a, int b)
{
    int x = a + b;
    printf("二つの数の和は,%dです.¥n", x);
    return;    // 戻り値「なし」なので,returnのみ記述する.この場合は省略可
}

// 引数あり,戻り値あり の関数
int wano_keisan(int a, int b)
{
    int x = a + b;
    return x; /* これは絶対必要!
}

int main(void)
{
    printf("一つ目の数を入力してください:");
    int a=0; scanf("%d", &a);
    printf("二つ目の数を入力してください:");
    int b=0; scanf("%d", &b);

    moji_hyouji(); 
    wano_hyouji(a, b);
    int sum = wano_keisan(a, b);
    printf("合計は %d です.¥n", sum);
}







実践! 以下を一緒に作りながら学びましょう

【最重要】
関数を用いた効率的なプログラムの開発方法

高度なコンピュータのプログラムは,複雑になりがちで,バグの無いプログラムを書くことは大変な困難を伴います. 少しでも効率的に開発を行うために,以下のようなやり方を実践しましょう.

最初から全てのプログラム一気にコーディングしようとしないこと

一度に沢山のプログラム(多数の関数)をコーディングしてしまうと,問題が起こったとき, どこに問題があるのかが分かりにくく,デバッグに時間がかかってしまいます. プログラムは少しずつ,一つ(の部品)ずつ作るようにし,かつ, その作ったものを一つずつ,動作チェックをするようにしましょう.

小さな関数に分けると部品単位での作成がやりやすくなります

例えば,マクローリン展開を使って e (ネイピア数)を求めるプログラムの作成を考えてみましょう.

e^x

ここで x=1 ですから,階乗の計算ができれば求めることができることがわかります.

e^x

では階乗の関数と級数を計算する関数を用いる以下の問題を考えてみましょう.

問題:
入力:マクローリン展開の階数n(n>=0)
出力:e(小数点以下6桁まで表示)


どこからプログラムを作り出すべきか?

まずはざっくりとしたプログラムの構造(=問題の答えを得るための戦略)を考えましょう.
中身が空のプログラムの「器」をつくる
#include <stdio.h>

float bekijo(int n) {	// nはべき乗の階数
	// べき乗の計算をここでする.
	return 0;
}

float napier(int n) {	// nは展開する数
	// ここにマクローリン展開級数を書く.
	// つまりここからべき乗が利用できる必要がある.
	return 0;
}

int main(void) {
	// ...
}
まずは上記のような構造になりますよね? まずは main() 関数.これが無ければ始まりません. そこから napier(int n)関数を呼び出しましょう.引数は.マクローリン展開の階数nです. napier関数の中でマクローリン展開をしますから,そこでべき乗を利用できる必要があります.

【重要】この段階でコンパイルが通るようにします.
    必ずコンパイルを通しながら少しずつ進めることが何より重要です.



この後はどうやって作りますか? いきなりネイピア数を求める級数部分を作りますか? もしも,級数部分を先に作ったとして,べき乗の関数ができていなければ,プログラムの正しさはどうやって検証しますか? 級数部分が正しいかどうかは,べき乗の部分が正しいことが確認できなければわかりませんね.

小さな部品から,ひとつひとつ部品ごとに作ろう

では必然的に,べき乗の部分から作って検証していくことになりますね.napier() の部分は当面放っておきましょう.
べき乗の関数とそれをチェックするプログラム
#include <stdio.h>

float bekijo(int n) {
	float x = 1.0;
	if(n<2) return x;
	for(int i=2; i<=n; i++) {
		x *= (n-i+2);
	}
    return x;
}

float napier(int n) {
	// まずはここは触らずに置いておく
	return 0;
}

int main(void) {	// べき乗関数のチェックをするためのメイン関数
	for(int i=0; i<7; i++) {
		printf("%d! = %f\n", i, bekijo(i));
	}
}

上記のチェックプログラムの実行結果
0! = 1.000000
1! = 1.000000
2! = 2.000000
3! = 6.000000
4! = 24.000000
5! = 120.000000
6! = 720.000000
いい感じですね!

ではいよいよ級数部分を作って行きましょう.
ネイピア数を求めるマクローリン展開級数部分とチェック用メイン関数

float napier(int n) {
    float e = 0;
    for(int i=0; i<=n; i++) {
        e += 1.0/bekijo(i); // 公式通り.べき乗がちゃんとできていればここが簡単になる.
    }
    return e;
}

int main(void) {
	for(int i=0; i<7; i++) {
		printf("%d! = %f, e = %f\n", i, bekijo(i), napier(i));
	}
}

上記のチェックプログラムの実行結果
0! = 1.000000, e = 1.000000
1! = 1.000000, e = 2.000000
2! = 2.000000, e = 2.500000
3! = 6.000000, e = 2.666667
4! = 24.000000, e = 2.708333
5! = 120.000000, e = 2.716667
6! = 720.000000, e = 2.718056
n=6にすると,小数点以下3桁まで合うことがわかります!

これで計算が合うことがわかったので,最後に入出力を問題に指定された形に合わせます.
問題で指定された入出力を持つプログラム
#include <stdio.h>

float bekijo(int n) {
	float x = 1.0;
	if(n<2) return x;
	for(int i=2; i<=n; i++) {
		x *= (n-i+2);
	}
    return x;
}

float napier(int n) {
    float e = 0;
    for(int i=0; i<=n; i++) {
        e += 1.0/bekijo(i);
    }
    return e;
}

int main(void) {	// 問題で要求された入出力を持つメイン関数
//	for(int i=0; i<7; i++) {
//		printf("%d! = %f, e = %f\n", i, bekijo(i), napier(i));
//	}	// これまでのチェック用のメインはコメントで残しておくことをオススメします.

    int n = 0;
    scanf("%d",&n);		// 階数を入力
    printf("%.6f\n", napier(n));	// 小数点以下6桁でネイピア数を出力
}

最終的なプログラムを実行してみた様子
$ ./napier
2
2.500000
$ ./napier
4
2.708333
$ ./napier
6
2.718056
$ ./napier
8
2.718279
$ ./napier
10
2.718282
$ 

上記で$はUnixのプロンプト(Win環境だと>ですね)で,(ドット)(スラッシュ)はプログラムを実行するときのおまじない. 最初の入力はキーボードから打ち込んだ「階数」で,その後がプログラムでの計算結果です.

これでようやく完成なので,必要に応じて,プログラムを Online Judge 等へコピペして終了です.






Quiz 5(第5回の提出課題ではありません)

以下の問それぞれに対応するプログラムを作成してみよう.


    1. 【コードの再利用】 以下のプログラムを見てください.関数を使っていないので,ほとんど同じコードがだらだらと続いています. 関数を用いることで同じコードは一度しか書かないようにし,かつ,表示される結果が同じになるようにしてください.
      まずは以下のプログラムを入力して,実行結果を確かめよう.
      // 関数を使っていないコード. 
      int main(void) 
      { 
          printf("%dねん %dくみ %dばん,", 1, 1, 1); 
          printf("なまえ:%s,", "めいじ たろう"); 
          printf("とくいかもく:%s", "さんすう"); 
          printf("\n"); 
      
          printf("%dねん %dくみ %dばん,", 1, 2, 2); 
          printf("なまえ:%s,", "めいだい じろう"); 
          printf("得意科目:%s", "こくご"); 
          printf("\n"); 
      
          printf("%d年 %d組 %d番,", 1, 3, 3); 
          printf("氏名:%s,", "しこん さぶろう"); 
          printf("かもく:%s", "りか"); 
          printf("\n"); 
      
          printf("%d年 %d組 %d番,", 1, 4, 4); 
          printf("氏名:%s,", "まえへ すすむ"); 
          printf("得意科目:%s", "しゃかい"); 
          printf("\n"); 
      }
      

      どうでしょうか? 同じことを言っているのにひらがな(例えば,ねん,くみ,ばんごう)と漢字(例えば年組番号) が混ざってちょっと見た目が変ではありませんか? 関数を使わずにベタ打ちしてしまうと,そういった 見栄えの統一などもし難くなります. できればそういうことは避けて,綺麗に統一された表示にしたいですよね?

      以下のコードに加筆,修正してみて下さい. 同じ内容を繰り返し書くことをしていないので,スッキリ,コンパクトになるはずです. このようにすると,コンパクトになっただけでなく,見やすくなるし, 同じコード部分を何度も使うことになるので,プログラムの間違いを減らすことにも役立つのです.
      // 関数を用いたコード
      void meibo_print(引数リスト)
      {
          // ここを編集
      }
      
      int main(void)
      {
          meibo_print(1,1,1,"めいじ たろう","さんすう”);
          meibo_print(以下を編集
          ....
          ....
      }
      
      【ヒント】文字列の引数は,char ...[ ] を使います
      【注意】書いたプログラムは,必ずコンパイルし,実行してみて,正しい結果が得られることを確認して下さい. これはすべての課題に共通します.コンパイル出来ないプログラムは採点されません.

      コード例 Q5-1


      出力例



    2. 【値を返す関数】 以下は,半径を入力すると円の面積を求めるプログラムです. これを修正して,半径を引数にとり,面積を返す関数 area_of_circle() を用いたプログラムにしてください.
      #include <stdio.h>
      #include <math.h>
      int main(void)
      {
          float r = 0.0;
          printf("radius = ");
          scanf("%f", &r);
          
          float s = M_PI * r * r;
          
          printf("area of the circle is %.2f\n", s);
      }
      

      コード例 Q5-2


      出力例



    3. 【実行時引数】 プログラムの実行時に2つの数字の引数をとり,その合計を表示するプログラムを作ってみよう.
      // 実行例(プログラム名がsum.exeの場合)
      > sum 1 2 (<-このようにキーボード入力)
      sum = 3  (<-このように画面に表示する)
      
      【ヒント】main関数の引数は,すべて文字列として渡されてきます. 数値として計算できるようにするためには,文字から数値に変換する必要があります. 以下のような関数を用いて数値に変換できます.
      int atoi(char str[]);        // 文字列を整数に変換
      double atof(char str[]);    // 文字列を浮動少数に変換
      long atol(char str[]);        // 文字列をlong型整数に変換
      
      これらの関数を利用するときには,stdlib.hをインクルードする必要がある.

      コード例 Q5-3


      出力例



    4. 【参照型引数】 2つの整数引数をとり,この2つの値を交換する関数 swap()を作りたい. どうすればよいでしょうか? まず,以下のプログラムを入力し,足らない部分を埋めて, 実行結果を確かめてみて下さい.うまく値が交換できるでしょうか?
      
      void swap(...)
      {
          ...
      }
      
      #if defined(TEST)
      #include <stdio.h>
      #include <stdlib.h>
      int main(void)
      {
          for(int i=0; i<10; i++){
              int x = rand()%100;
              int y = rand()%100;
              printf("swap実行前:x=%d, y=%d  ---->  ", x, y);
              swap(x, y);
              printf("実行後:x=%d, y=%d\n", x, y);
          }
      }
      #endif
      

      【ヒント】参照型引数を用いて,呼び出し側の変数の中身を書き換えている.
      【ヒント】スワップ(値の置き換え)をするために,値を一時的(temporary)に仮置きする変数tempを使っている.

      コード例 Q5-4

      自分でやってみて確認しよう!
      


    5. 【関数の入れ子】 1から3の3個の数の2乗和(1 2+22+32)を求めたい. 2乗を求める関数と,和を求める関数を分けるとすると,プログラムはどのように書けますか?
      【ヒント】関数から関数を呼び出すことができる.
      【ヒント】和を求める時,ここでは単に3回足し算すれば良いです.

      コード例 Q5-5


      出力例





Assignment 5(第5回の宿題)

課題の提出は Online Judge にて行います.

  • 提出期限があります.できるだけ早めにやりましょう.
  • Online Judge は普通の課題提出方法とは異なり,良い点が取れるまで何度でも挑戦できます.良い点が取れるまで頑張ってやろう.
  • わからないことは,早めに Teams で聞こう.問題は自分で解決すること.
  • それでもどうしてもわからないときは,毎週月曜日の2時限目にZoomによるオフィスアワーを用意しています. ZoomのリンクはOh-o! meijiで確認してください.
  • Online Judge のページはこちら