参照とコピーコンストラクタ

参照とは,型名に & を付けることで変数の別名をつけることができるものです. 関数の引数や戻り値において参照を用いることで,関数間で呼び出し元の変数を直接参照できるため, 値渡しのときのように変数のコピーは作成されません. 参照は,コピーによる初期化であるコピーコンストラクタと密接な関係があるため, 値渡しと参照渡しの違いを復習した上で,コピーコンストラクタの動作について説明します.

練習課題

MyStringクラスにコピーコンストラクタを追加し,下記のmain.cppを実行できるようにしなさい.

main.cpp

解説1:「値渡し(あたいわたし)」と「参照渡し(さんしょうわたし)」

参照とは,変数の別名のことでした.課題1での参照の説明を思い出しましょう.

プログラミング言語において,関数呼び出し時の変数の渡し方には2種類あります.

「値渡し」は,変数のコピーを渡す,ということです.呼び出し先の関数内でコピーをいくらいじっても,呼び出し元の変数は変化しません.コピーを伴うので,大きな変数(オブジェクト)の場合は性能低下が生じる恐れがあります.

「参照渡し」は,変数の参照を渡す,ということです.変数の参照を渡す場合は,呼び出し先の関数内で変数の参照をいじった場合,呼び出し元の変数が変化します.コピーを伴わないので,大きな変数(オブジェクト)を渡しても性能面で問題は少ないです.

C++言語においては,参照を渡すことができます.一方,参照渡しが無いC言語では,ポインタを値渡しして,呼び出し先の関数内でポインタの中身を操作することで,呼び出し元の変数を操作していました.

また,値渡しと参照渡しは関数の戻り値にも適用でき,「値を返す関数」だけでなく「参照を返す関数」も作成することができます.値を返す関数では戻り値のコピーが呼び出し元に返されますが,参照を返す関数では戻り値の参照を渡すため,呼び出し元で戻り値の変数を操作できます.戻り値として参照を返す際には,関数が終了した後も存在している変数の参照を返す必要があります.関数内の一時変数(ローカル変数)の参照を返すと,不定な動作をしますので注意しましょう.

ということで,「参照」はC++言語の重要な機能の一つです.

値渡しと参照渡しの補足説明

説明用スライド

解説2: コピーコンストラクタ

C++においては,あるオブジェクトを別のオブジェクトにコピーする状況として,”代入”と”コピーによる初期化”の2つがあります. (代入の方法を指定するには,次回に学習する”代入演算子のオーバーロード”を行います.ここでは触れません.)

コピーコンストラクタを定義すると,単純なコピーではない,「コピーによる初期化」の方法を指定することができます. コピーコンストラクタは,引数として同じクラスのオブジェクトの参照をとるコンストラクタで,下記の形式で記述されます.

classname(const classname& ref)
{
   // 定義部(メンバ変数のコピー方法等を記述する)
}

コピーコンストラクタの定義では,どのようにオブジェクト間のコピーを行うかを記述します. 多くの場合,コピーコンストラクタ内ではメンバ変数のコピーを行います.

一例として,MyStringクラスのcppファイル内においてコピーコンストラクタを記述する場合を示します.

MyString::MyString(const MyString& ref)
{
   // 定義部(メンバ変数のコピー方法等を記述する)
}

コピーコンストラクタの呼び出しは以下の場合に発生します.

  1. 新しいオブジェクトを宣言する際,同じクラスの既存のオブジェクトを元にして作成するとき
  2. MyString s1("t123456A"); // const char*を引数とするコンストラクタが呼ばれる
    MyString s2 = "t123456A"; // const char*を引数とするコンストラクタが呼ばれる
    MyString s3(s2); //ここはコピーコンストラクタが呼ばれる
    MyString s4 = s3; //ここもコピーコンストラクタが呼ばれる
  3. 関数にオブジェクトを渡すとき(関数呼び出し時に引数を値渡しする場合)
  4. // 引数が値渡し
    void func(MyString str1, MyString str2)
    {
       ...
    }

    int main()
    {
       MyString s1("t123456A");
       MyString s2;

       // func呼び出し時,str1とstr2のそれぞれでコピーコンストラクタが呼ばれる.
       // str1はs1をコピーすることで初期化され,str2はs2をコピーすることで初期化される.
       func(s1, s2);
       ...
    }
  5. 関数の戻り値として使用するオブジェクトを作るとき(関数が値を戻す場合)
  6. // 値を戻す関数
    MyString func(const char *chr)
    {
       MyString ret;
       ret.setString(chr);
       return ret;
    }

    int main()
    {
       MyString s1;

       // func呼び出し時,returnでコピーコンストラクタが呼ばれる.
       // retのコピーがs1に代入される.
       s1 = func("t123456A");
       ...
    }

逆に,以下の例ではコピーコンストラクタは呼ばれません.

  1. 初期化以外の代入操作
  2. MyString s1("t123456A");
    MyString s2;
    s2 = s1; // ここでコピーコンストラクタは呼ばれない.
  3. 関数にオブジェクトの参照を渡すとき(関数呼び出し時に引数を参照渡しする場合)
  4. // 引数が参照渡し
    void func(MyString& str1, MyString& str2)
    {
       ...
    }

    int main()
    {
       MyString s1("t123456A");
       MyString s2;

       // func呼び出し時,s1とs2の参照が渡されるためコピーコンストラクタは呼ばれない.
       func(s1, s2);
       ...
    }
  5. 関数の戻り値としてオブジェクトの参照を返すとき(関数が参照を戻す場合)
  6. MyString g_s;

    // 参照を戻す関数
    MyString& func(const char *chr)
    {
       g_s.setString(chr);
       return g_s;
    }

    int main()
    {
       MyString s1;
       // func呼び出し時,returnは参照を返しているのでコピーコンストラクタは呼ばれない.
       s1 = func("t123456A");
       ...
    }

コピーコンストラクタの補足説明

もし明示的にコピーコンストラクタを定義しない場合,コンパイラは自動的に単純なコピーによってオブジェクトの初期化を行います. よって,クラスによってはコピーコンストラクタを明示的に定義しない場合もありますが,クラスメンバにポインタを含む場合,単純なコピーでは問題が生じる場合があります.

説明用スライド

オプション課題

  1. コンストラクタのオーバーロード(多重定義)する理由を調査しなさい
  2. 関数のオーバーロードでは”あいまいさ”が問題となる.以下のオーバーロード関数があいまいである理由を説明しなさい