Lecture 6 関数(2)

関数の解説動画

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




引数の値渡しと参照渡し

関数に引数を渡す方には大きく分けて2種類あります.一つは値渡し(call by value)と呼ばれる方法です.値渡しでは関数側の仮引数に引数の値がコピーされてから渡されます. すなわち,呼び出し側の元の変数には影響を与えません.
一方,参照渡し(call by reference)は,値をコピーせず,呼び出し側の元のデータを文字通り参照*します.関数の中で示している変数は,名前こそ,仮引数の名前となりますが,その実体は元のデータ(変数)そのものになります.従って,関数の中で値を変更すると,元のデータの値も更新されます.

表5.2 値渡しと参照渡し ... 違いがわかりますか?

clear1.cpp 値渡し clear2.cpp 参照渡し
#include <stdio.h>

void clear(int x)
{
    x = 0;
}

int main(void)
{
    int a = 3;
    clear(a);
    printf("a=%d\n",a);
}

#include <stdio.h>

void clear(int& x)
{
    x = 0;
}

int main(void)
{
    int a = 3;
    clear(a);
    printf("a=%d\n",a);
}
//このコードはC++コンパイラでのみコンパイル可能
* 参照渡しの場合,実際には値ではなく,アドレス(変数のメモリ上での位置を示す値)がコピーされます.

main関数の引数

既に述べた通り,main()も関数です.実は,main()も引数を取ることができます.main()に引数をとると,プログラムの実行時に,OS(Unix, Linux, Windows, MacOSなど)から値をもらってくることができ,便利なプログラムを作成することが可能になります.
#include <stdio.h>
int main(int argc, char* argv[])    // 常にこのような書き方をします
{
    for(int i=0; i<argc; i++)
        printf("%d番目の引数 = %s\n", i, argv[i]);    // i番目の文字列
}
この例は,main()の引数として何が渡されてきたのかを調べるプログラムです.main()の引数は2つあり, 1番目の引数 argc は,プログラムを起動した時の引数の数,2番目の引数は,その時の引数として与えられた文字列を示し文字列*配列*として渡されます.

実行例

例えば,作成したプログラムの名前が test.exe であった場合,
> test Have a nice day!
と入力した時, argc は 5 となる.
また,
    argv[0] ... test (これはプログラム自体の名前)
    argv[1] ... Have
    argv[2] ... a
    argv[3] ... nice
    argv[4] ... day!
である.
ヒント:char* argv[]はちょっとむずかしいですが,文字列の配列を示しています.
覚えよう「文字列」 文字列は,一つ一つの文字が列をなすように並べたものです.
char s[] = "abcde";        // 文字列の宣言と初期化
char s[10];                // 10個の文字が入る大きさの文字列変数をつくる
この文字列を画面に表示するには,以下のようにします.
printf("%s\n", s);
文字列s[ ]のかぎかっこを取りさったものを引数として与えます.この[ ]を取るという事には大きな意味があるのですが,今はただ,そうするのだ,と覚えてください.
覚えよう「配列」 配列は,文字列と同じように,一つ一つの変数が列をなすように並べたものです.
int num[] = { 1, 2, 3 };        // 配列の宣言と初期化
float x[10];                // 10個の浮動小数点の値が入る大きさの配列をつくる


再帰関数

C言語では関数の中で「自身の関数」を呼び出すことができます. 例えば,以下は階乗の計算です:
#include <stdio.h>

int kaijo(int x)
{
    int n = 0;
    if(x<0)
        n = 0;   // 例外のエラー処理
    else if(x==0)
        n = 1;   // 0! = 1である.
    else
        n = x * kaijo(x-1); // ここが再帰呼び出し
    return n;
}

int main(void)
{
    for(int i=0; i<10; i++)
        printf("%d! = %d\n", i, kaijo(i));
}

階乗の計算は,例えば,

5! = 5*4*3*2*1

である.これは,

5! = 5 * 4!

に等しいですね.これを一般項で考えると,

x! = x * (x-1)!

となります. これを再帰関数呼び出しで自身を呼び出すことで実現していけば良いです.
最後に,再帰呼び出しではない定数部(この場合は x=0 で リターンが 1)に至って初めて値が定まり, 次々にリターンして(戻って)きます.
再帰呼び出しは,ループを使わずに繰り返し計算のような処理が可能であることがメリットの一つで, 近年のマルチコアプロセッサでの高速化が期待できる点があげられますが, 一般には簡素に書ける以上のメリットはあまりないと言われています. 関数を呼び出すのに若干のオーバーヘッド(処理速度とメモリ)がありますが, これも最近のコンパイラでは,最適化処理によってループに自動変換されるので. さほど気にする必要はないでしょう.

プログラムの品質を高めるために

飛行機や車はコンピュータにより制御されています. プログラムの品質が悪くて,飛行中や走行中に制御が止まってしまったら人命に関わります. また経済活動(銀行のオンライン処理や,クレジットカードの決済,ネットショッピングなど) もコンピュータ(=ハード&ソフトウェア)に支えられています. 現代社会はコンピュータのプログラムで保たれていると言っても過言ではありません.
すなわち,プログラムの品質を高めることは,何にも代えがたいほどの重要性を持っています.
そのために重要なのは,

  • できるだけ関数化する.
  • 関数化することによって,データや変数を局所に留める.
  • コード量を減らし,同じコードを何度も再利用することでより信頼性を高める.

ということです.

さらに,作ったコードは,必ずテスト(動作試験,品質試験)することが重要です. 関数化することでコードテストがやりやすくなります.

例えば,以下のように,割り算をするプログラムを作ったとします.
// 割り算をする関数
#include <stdio.h>

float warizan(float bunshi, float bunbo)
{
    float ret = bunshi / bunbo;	// そのまま割り算をする.それで大丈夫?
    return ret;
}

// 以下は,上の関数が正しいか調べるためのテストコード
#ifdef TEST
int main(void)
{
    for(float bunshi=-1; bunshi<= 1; bunshi+=1){  // 色々なパターンを作って試す
        for(float bunbo=-1; bunbo<= 1; bunbo+=1){

            float kotae = warizan(bunshi, bunbo); // 分母分子に色々な値を入れてみる.
                                                  // 同時にこれが関数の使用例にもなる.

            printf("%g/%g = %g\n", bunshi, bunbo, kotae);
        }
    }
}
#endif
これをテストモードでコンパイルします.
bcc32c -DTEST warizan.cpp
するとどうなるでしょうか?
以下は UNIXのg++でコンパイルした場合の結果です.
-1/-1 = 1
-1/0 = -inf
-1/1 = -1
0/-1 = -0
0/0 = nan
0/1 = 0
1/-1 = -1
1/0 = inf
1/1 = 1
これを見ても分かる通り,2行目では0割(0による割り算)が行われてしまい, 計算結果が -inf(マイナス無限大)になってしまっています. これが Embarcaderoのコンパイラの場合は,
warizan.exeは動作を停止しました
となり,プログラムが異常停止してしまいます. まさに,飛行機が墜落して多くの人命が危険にさらされることになるわけです(!) 関数化してテストコードを書くことで,このようなプログラムの問題点を早期に発見できる可能性が高まります. これは是非実践するように心がけて下さい.

ライブラリ化

作成した関数は,独立したファイルにおいておき,後から利用することができます. これによって,過去に作成したコードを何度も再利用することができるため, 生産性を上げることができるだけでなく,何度も再使用することによって, バグを早期になくすことが可能となります.
ライブラリ化には,
  • 関数本体の入ったファイル(ここでは warizan.cpp)
  • 関数定義の書かれたヘッダファイル(ここでは warizan.h)
  • もちろん,これらの関数を利用するmain(アプリケーションプログラム)
が必要です.
例えば,上記の割り算のプログラムをライブラリとして用いる場合,次のヘッダファイルを作ります.
// warizan.h ... 割り算関数の定義が書かれたヘッダファイル
float warizan(float bunshi, float bunbo);


作った割り算関数を利用するアプリケーションソフトを以下の様に作ることができます.
// main.cpp ... 割り算ライブラリを利用するアプリ
#include <stdio.h>
#include <warizan.h> // ... 作成した割り算のヘッダファイル

int main(void)
{
    float bunshi = 10;
    float bunbo  = 100;
    float kotae = warizan(bunshi, bunbo);	// フィアル内のどこにも定義がないのに使える!
    printf("%g/%g = %g\n", bunshi, bunbo, kotae);
}

これらのプログラムをコンパイルするには以下の様にします.
bcc32 main.cpp warizan.cpp



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

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


    1. 【実行時引数】 プログラムの実行時に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


      出力例



    2. 【参照型引数】 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

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


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

      コード例 Q5-5


      出力例





Assignment 6(第6回の宿題)

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

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