C++言語解説:1-6.関数の機能
2002-06-23

[概要] コマンドライン引数とプロトタイプ宣言。その他オブジェクト指向の準備として
    参照、関数オーバーロード、デフォルト引数を学習します。

[構成]・変数のスコープ
     * C++のローカル変数
   ・コマンドライン引数
     * argcとargv
     * 数値を渡すには
   ・プロトタイプ宣言
     * プロトタイプ宣言の意味
   ・再帰
     * 関数の再帰的使用
   ・参照
     * 参照仮引数
     * 参照を戻す
     * 独立参照
   ・関数オーバーロード
     * 同じ機能には同じ名前を
   ・デフォルト引数
     * 引数指定を省ける

変数のスコープ

 C++のローカル変数
 
  ・ローカル変数は、通常関数内でローカライズして使用することができます。そのた
   め関数の先頭部で定義するのが普通です。しかしC++では関数より小さな単位である
   コードブロックでもローカル変数を定義することができます。
  
  ・コードブロックで定義したローカル変数は、そのコードブロック内のでのみメモリ
   上に存在します。List1に具体例を示します。
 
    [List1.C++のローカル変数定義]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    int main()
    {
      int i_a = 10;

      cout << "main : i_a(1) =" << i_a << endl;
      
      for (int i_a = 0; i_a < 6; i_a++) {
        cout << "sub : i_a =" << i_a << endl; //forブロック内のi_a
      }
      
      cout << "main : i_a(2) =" << i_a << endl;

      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     main : i_a(1) =10
     sub : i_a =0
     sub : i_a =1
     sub : i_a =2
     sub : i_a =3
     sub : i_a =4
     sub : i_a =5
     main : i_a(2) =10
   └───────────────────────────────────┘
  
  ・関数内で整数i_aを定義していますが、for内のi_aは別物で、メモリ領域も完全に別
   となっていることがわかると思います。
  
  ・ブロック内のローカル変数は、グローバル変数に対する関数内ローカル変数の動き
   と同様に、同一変数名が有る場合はブロック内変数が優先して採用されます。
   
  ・C++では、使用する前に定義すれば良いのであり、有効範囲/スコープ(scope)はその
   定義位置で決まります。スコープから抜けると、変数の使用していたメモリ領域は
   解放
されます。これによりメモリの少ない組み込み系の環境等でも有効に変数を使
   用することができます。


コマンドライン引数

 argcとargv
  
  ・プログラム実行時にコマンドラインからプログラムへ情報を渡すことができます。
   これらはコマンドライン引数(command line argument)と呼ばれます。
  
  ・コマンドラインからの情報を、C++ではmain()関数の引数として、argcとargv(*1)
   よって受け取ります。
     int main(int argc, char* argv[]){ ・・・ }
   
  ・argcはコマンドライン引数の数が入っています。プログラム名自身も入っているので
   最低値は1になります。
  
  ・argvは引数文字列データへのポインタ配列です。OSが持っている引数データへのア
   ドレスが渡されます。例えばコマンドラインで
     prog arg1 arg2
   とした場合、argc = 3で
     argv[0] ← "prog"の先頭アドレス
     argv[1] ← "arg1"の先頭アドレス
     argv[2] ← "arg2"の先頭アドレス
     argv[3] ← 0 (*2)
   となっています。List2に例を示します。
   
    [List2.コマンドライン引数]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    int main(int argc, char* argv[])
    {
      if (argc < 2) {
        cout << "please input argument!!" << endl;
        return 1;
      }
      
      for (int i=0; i<=argc; i++){
        if (argv[i] != 0) {
          cout << i << ":" << argv[i] << endl;
        }
        else cout << i << ":0" << endl;
      }
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     C:\Tmp>prog argv1 argv2 argv3
     0:C:\Tmp\prog.exe
     1:argv1
     2:argv2
     3:argv3
     4:0
   └───────────────────────────────────┘


 数値を渡すには

  ・argvはchar型のポインタ配列です。従って、数値をコマンドライン引数に記述して
   も文字列として渡されます。

  
  ・つまり数値を扱うには、文字列から数値への変換が必要となります。C++の代表的な
   変換関数には
     atoi() : intへ変換
     atol() : long intへ変換
     atof() : doubleへ変換
   があります。詳しくはライブラリマニュアルを参照して下さい。これらの関数を使
   用するには <stdlib.h>ヘッダが必要です。
   
    [List3.コマンドライン引数の数値]
   ┌───────────────────────────────────┐
    #include <iostream>
    #include <cstdlib>  // <stdlib.h>
    #include <cstring>  // <string.h>
    using namespace std;
    
    int main(int argc, char* argv[])
    {
      int i_a1, i_a3;
      
      if (argc < 4) {
        cout << "please input argument!!" << endl;
        return 1;
      }
      
      i_a1 = atoi(argv[1]);
      i_a3 = atoi(argv[3]);
      
      if (!strcmp(argv[2],"+")) cout << i_a1 + i_a3 << endl;
      else if (!strcmp(argv[2],"-")) cout << i_a1 - i_a3 << endl;
      else cout << "operator?" << endl;
      
      return 0;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     C:\Tmp>prog 10 + 5
     15
     C:\Tmp>prog 10 - 5
     5
     C:\Tmp>prog 10 ? 5
     operator?
   └───────────────────────────────────┘


プロトタイプ宣言

 プロトタイプ宣言の意味
 
  ・関数のプロトタイプ宣言を「おまじない」で使ってきましたが、ここで意味につい
   て説明したいと思います。
  
  ・プロトタイプ宣言の目的は、コンパイラに対して以下の情報を提示することです。
     関数の戻り値の型
     関数の仮引数の型
     関数の仮引数の数
   これらの情報をコンパイラへ伝えることにより、コード内で使用された関数に対し
   戻り値型や仮引数の型や数に対するチェックが行われるようになります。
  
  ・従ってプロトタイプ宣言で使用する仮引数の名前は任意です。型のみで名前を付け
   ない記述方式も認められています。ただし、名前を付けた方が、型の不一致が発生
   した場合に、場所を名前で認識できる
ので便利です。
     void v_Funca(int, int);   //型のみ
     void v_Funca(int a, int b); //仮引数付き
  
  ・プロトタイプについては、K&Rの第1版に準拠した古いCコンパイラでは指定不要にな
   っているので、そのようなソースコードをANSIに準拠したC/C++コンパイラでコンパ
   イルすると、大量のwarningかエラーになります。そのときはプロトタイプ宣言を付
   ける
だけで解決することができます。


再帰

 関数の再帰的使用
 
  ・関数は自分自身を呼び出すことができます。自分自身を呼び出す関数は再帰的
   (recursive)
であると言われます。
  
  ・関数の再帰的呼び出しをイメージ的に説明すると「スタックの積み上げ」で実現さ
   れています(*3)

    [Fig1.再帰呼び出しのイメージ]
   ┌───────────────────────────────────┐
     ┌───────┐
     │int SubFunc() │
     ├───↓───┤
     │int SubFunc() │
     ├───↓───┤
     │int SubFunc() │
     ├───↓───┤
     │ int main() │
     └───────┘
   └───────────────────────────────────┘
  
  ・従って再帰呼び出しを使用する場合、関数呼び出しのオーバーヘッド(時間がかかる)
   とスタックのオーバーフロー
が起こり得ることに注意が必要です。
  
  ・List4に多項式の計算(Honorの方法)を行うためのサンプルを示します。ここでHonor
   の方法とは
     f(X) = An*Xn + An-1*Xn-1 + ・・・ + A0
   という多項式を計算する際
     fi = fi-1*X + An-i
     f0 = An
   で表せるという考え方です。List4では
     f(X) = 5*X4 + 4*X3 + 3*X2 + 2*X + 1
   を計算したいと思います。
  
    [List4.Honorの方法による多項式の計算]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    int i_Honor(int x, int i_n, int* ip_a, int i_N);
    
    int main()
    {
      int i_a[] = {1,2,3,4,5}; //0次〜4次の係数
      
      //多項式式表示ブロック
      cout << "f(x)=";
      for (int i=4; i>=0; i--){ //i=4は次数
        if (i) cout << i_a[i] << "x^" << i << " + ";
        else cout << i_a[i] << endl;
      }
      
      //x=0〜5の計算と表示
      for (int x=0; x<5; x++){
        cout << " f(" << x << ")=" << i_Honor(x, 4, i_a, 4) << endl;
      }
    }
    
    int i_Honor(int x, int i_n, int* ip_a, int i_N)
    {
      int i_ret;
      
      if (!i_n) { //再帰呼び出しの終了
        i_ret = *(ip_a+i_N);
      }
      else { //再帰呼び出し部
        i_ret = i_Honor(x, i_n-1, ip_a, i_N)*x + *(ip_a+(i_N-i_n));
      }
      cout << "[" << i_n << "]" << i_ret << ":";
      
      return i_ret;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     f(x)=5x^4 + 4x^3 + 3x^2 + 2x^1 + 1
     [0]5:[1]4:[2]3:[3]2:[4]1: f(0)=1
     [0]5:[1]9:[2]12:[3]14:[4]15: f(1)=15
     [0]5:[1]14:[2]31:[3]64:[4]129: f(2)=129
     [0]5:[1]19:[2]60:[3]182:[4]547: f(3)=547
     [0]5:[1]24:[2]99:[3]398:[4]1593: f(4)=1593
   └───────────────────────────────────┘


参照

 参照仮引数
 
  ・関数に値を参照渡しする際、C言語ではポインタを使用する必要がありました。しか
   しC++では参照(reference)により、参照渡しであることをコンパイラへ伝えること
   ができます。
  
  ・関数へ参照渡しであることを伝えるには、仮引数に参照を適用します。List5に例を
   示します。

    [List5.参照仮引数による参照渡し]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    void swap(int& i_a, int& i_b); //仮引数に参照を適用
    
    int main()
    {
      int i_a = 10;
      int i_b = 20;
      
      cout << "i_a=" << i_a << " i_b=" << i_b << endl;
      
      swap(i_a, i_b); //&は必要なし。自動的にアドレス渡しになる。
      
      cout << "i_a=" << i_a << " i_b=" << i_b << endl;

      return 0;
    }
    
    void swap(int& i_a, int& i_b)
    {
      int tmp;
      
      //i_a, i_bともポインタ的記述必要なし
      tmp = i_a;
      i_a = i_b;
      i_b = tmp;
      
      return;
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     i_a=10 i_b=20
     i_a=20 i_b=10 ←swapできている
   └───────────────────────────────────┘
  
  ・例でわかるように、参照を適用するには変数宣言時に&を付けます。&の付け方はポ
   インタの*と同じで
     int& i_a 又は int &i_a
   のどちらでも可です。また
      int& i_a, i_b;
   と宣言した場合ですが、ポインタと違ってこの記述は参照に適用できないので考え
   る必要はありません。理由は後述の独立参照で説明します。
   
  ・そして参照で宣言された変数は、自動的に実体を参照、つまり別名として振る舞う
   ため、ポインタ的な記述の必要はありません。
  
  ・参照仮引数は、関数の使用者に引数の参照渡しと値渡しを意識させたくないときに
   は有効な記述方法となります。
   
   
 参照を戻す
  
  ・関数の戻り値にも参照を適用できます。関数の戻り値型又は関数名の先頭に&を付け
   れば参照が戻り値であることを宣言できます。
  
  ・言葉だけでは意味がわかりにくいのでList6で例を見てみます。

    [List6.戻り値が参照の場合]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    int i_a, i_b; //グローバル変数
    
    int& ir_FuncAB(int n); //戻り値は参照
    
    int main()
    {
      i_a = 10; //初期値を入れておく
      i_b = 20;

      cout << "i_a=" << i_a << " i_b=" << i_b << endl;
      
      ir_FuncAB(0) = ir_FuncAB(1) + 10; //関数が左辺値になっている
     //↑i_aを指す  ↑i_bを指す
      
      cout << "i_a=" << i_a << " i_b=" << i_b << endl;
      
      return 0;
    }
    
    int& ir_FuncAB(int n)
    {
      if (!n) return i_a; //n==0ならばi_aへの参照
      else return i_b;  //n!=0ならばi_bへの参照
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     i_a=10 i_b=20
     i_a=30 i_b=20 ←i_aに加算されている
   └───────────────────────────────────┘
  
  ・ir_FuncAB(0)はi_aの参照、ir_FuncAB(1)はi_bの参照になります。ir_FuncAB()は関
   数ですが、int型変数への参照となるため、変数に対する代入式のように左辺へ置く
   ことができます。

  
  ・しかし参照の機能は、List5やList6のような使い方よりも、今後の教育で説明する
   演算子オーバーロードやコピーコンストラクタで多用されます。と言うより「その
   ために」用意された機能だと捉えて方がしっくりきます。なので今回は準備編と考
   えて頂ければ結構です。


 独立参照

  ・参照では独立した参照変数を宣言することもできます。独立参照(independence
   reference)
と呼ばれますが、この機能はほとんど使用する機会がありません。念の
   ため紹介だけしておきます。
  
  ・独立参照は以下のように使用します。
     int i_a = 10;
     int& ir_a = i_a; //ir_aはi_aの独立参照(別名)
   ir_aはi_aの別名として振る舞うことになります。
  
  ・独立参照は必ず参照先のオブジェクトが指定されなければなりません。従って
     int &ir_a, &ir_b;
   という記述はありえません。


関数オーバーロード

 同じ機能には同じ名前を
 
  ・オブジェクト指向の一要素である「オーバロード」。C++では仮引数の型が数が異な
   れば同一名の関数を複数定義できます
。これを関数オーバーロード(function
   overloading)
と呼びます。
  
  ・例えば、ある計算を行う関数を作りたいとします。(a+b)^2 を行うなど。従来であ
   れば、変数aとbの型に対し、異なる関数名を用意する必要がありました。例えば以
   下のように。
     int i_CalFunc(int a, int b);
     double d_CalFunc(double a, double b);

  ・しかし、C++では仮引数の型が異なれば関数名が同一でも違う関数として扱われる
   で、以下のような関数定義が可能です。
     int CalFunc(int a, int b);     // int型 (a+b)^2
     int CalFunc(int a);         // int型 (a+a)^2
     double CalFunc(double a, double b); // double型 (a+b)^2
  
  ・これにより、同じ処理を行う関数をデータ型や呼び出し方式に関係なく同じ関数名
   を適用することができます。使用者からは、同じ名前の関数が引数により異なる動
   きをするように見えます。
つまり関数オーバーロードは多態性(polymorphism)を実
   現する仕掛けの一つなのです。

    [List7.関数オーバーロードの例]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    int CalFunc(int i_a, int i_b);     //(i_a + i_b)^2
    int CalFunc(int i_a);          //(i_a + i_a)^2
    double CalFunc(double d_a, double d_b); //(d_a + d_b)^2
    
    int main()
    {
      int i_a = 1;
      int i_b = 2;
      double d_a = 1.4;
      double d_b = 2.7;
      
      cout << "int:" << i_a << " int:" << i_b << " =";
      cout << CalFunc(i_a, i_b) << endl;
      
      cout << "int:" << i_a << " =";
      cout << CalFunc(i_a) << endl;
      
      cout << "double:" << d_a << " double:" << d_b << " =";
      cout << CalFunc(d_a, d_b) << endl;

      return 0;
    }
    
    int CalFunc(int i_a, int i_b) //int (i_a + i_b)^2
    {
      cout << "[int int]:";
      return (i_a*i_a + i_b*i_b + 2*i_a*i_b);
    }
    
    int CalFunc(int i_a) //int (i_a + i_a)^2
    {
      cout << "[int]:";
      return (4*i_a*i_a);
    }
    
    double CalFunc(double d_a, double d_b) //double (d_a + d_b)^2
    {
      cout << "[double double]:";
      return (d_a*d_a + d_b*d_b + 2.0*d_a*d_b);
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     int:1 int:2 =[int int]:9
     int:1 =[int]:4
     double:1.4 double:2.7 =[double double]:16.81
   └───────────────────────────────────┘
  
  ・List7を見ればわかるように、引数の数や型により適切なCalFunc()関数が呼び出さ
   れています。
  
  ・注意点として関数オーバーロードで定数値を引数に置いた場合、暗黙の型変換によ
   り関数の指定が曖昧になる
ことがあります。例えば
     double Func(double d_a);
     float Func(float f_a);
   において
     Func(10.0);
   は暗黙の型変換により、小数点定数がdouble扱いとなります。しかし
     Func(10);
   は、整数定数をfloat/doubleのどちらに変換するかの規約が無いため、曖昧となり
   ます。これは基本的にはコンパイルエラーとなります。オーバーロード関数を定義
   する際には指定される引数の内容についてケアが必要です。
   
   
デフォルト引数
   
 引数指定を省ける
 
  ・C++では、仮引数に対応する引数が指定されていない場合デフォルト引数(default
    argument)
を与えることができます。例えば
      void Func (double d_a, int i_a = 0);
   と関数が宣言されている場合、int i_a = 0 がデフォルト引数です。
      Func(10.0, 5); //明示的にi_aに値を渡す
      Func(10.0);  //i_aはデフォルトの0
   のようになります。

    [List8.デフォルト引数の例]
   ┌───────────────────────────────────┐
    #include <iostream>
    using namespace std;
    
    int i_Func(int i_a, int i_b=10); //デフォルト引数指定
    
    int main()
    {
      cout << "20,20 : " << i_Func(20,20) << endl;
      cout << "20  : " << i_Func(20) << endl;
      
      return 0;
    }
    
    int i_Func(int i_a, int i_b)
    {
      return (i_a + i_b);
    }
   └───────────────────────────────────┘
   ┌───────────────────────────────────┐
    [出力結果]
     20,20 : 40
     20  : 30
   └───────────────────────────────────┘
  
  ・デフォルト引数を使用する場合、デフォルト値は一度しか指定できません。実質的
   にデフォルト値は関数のプロトタイプ宣言部で記述するのみ
となります。しかしオ
   ーバーロードした関数では、異なるデフォルト値をアサインできます。
     void Func (double d_a, int i_a = 0);
     void Func (double d_a, double d_b, int i_a = 10);
     
  ・またデフォルト値を持つ引数は、デフォルト値を持たない仮引数の右側に置かなけ
   ればなりません。
仮に
     void Func(int i_a=10, int i_b, int i_c = 20); //この記述はNG
   としたとき
     Func(10,20);
   は、省略変数が曖昧になってしまうからです。正しくは以下のようになります。
     void Func(int i_a, int i_b=10, int i_c=20);  //この記述はOK
  
  ・デフォルト引数を関数オーバーロードと同時に使用する場合は注意が必要です。
   略した変数によってオーバーロード関数が曖昧になる
ケースがあります。例えば
     void Func(int i_a);
     void Func(int i_a, int i_b=10);
   の場合、Func(10)はどちらを指しているかわかりません。オーバーロードのkeyとな
   る変数をデフォルト指定可にすると、ほぼ曖昧になる
と言えるでしょう。


(*1)引数のargcとargvは固定されているわけではありませんが、慣習としてこの名前が使
  用されています。他人が見るときを考えれば「郷には入れば郷に従え」で構わないと
  思っていますが。

(*2)ここにはNULLが入ると思っていたのですが、原著によると
    argv[argc] == 0
  となっていました。ポインタに対する0は、式中ではNULLに読み替えられるのですが
  argv[argc]への値はOSから渡されるものなので、コンパイラのように「読み替え」
  が行われるかどうか私にはわかりません。なので、ここでは原著の言う通り「0」と
  扱うことにしました。

(*3)あくまでイメージであり、実装はコンパイラに任されています。

[Revision Table]
 |Revision |Date    |Comments
 |----------|-----------|-----------------------------------------------------
 |1.00   |2002-06-23 |初版
 |1.01   |2002-06-30 |語句+リンク修正
[end]
Copyright(C) 2002 Altmo