第四回-01 : オブジェクトの配列

本ページでは「オブジェクトの配列とコンストラクタ」について学ぶ。


オブジェクトの配列 (1)

以下のプログラムを見てみよう。

#include <iostream>
using namespace std;

class samp{

    int a;

  public:
    void set_a(int n){ a = n; }
    int get_a(){ return a; }

};

int main(){

  samp ob[4];   // オブジェクトの配列の宣言

  samp ob2;   // オブジェクト1個のみの宣言

  for(int i=0 ; i<4 ; i++) ob[i].set_a(i);
  for(int i=0 ; i<4 ; i++) cout << ob[i].get_a();

  cout << "\n";

  return 0;
}


上記がコードであるが、samp クラスのオブジェクトの配列 ob[4] が問題なく定義、また利用できることがわかる。

なお、オブジェクトの配列ではなく、オブジェクト一個 (ob2) を宣言することもできるのも問題ないであろう。

ここでクラスのコンストラクタに関連し、資料より少し踏み込んだ解説をする。
今、上記の「samp ob[4];」および「samp ob2;」なる宣言により、下図のように計5つのオブジェクト「ob[0]、ob[1]、ob[2]、ob[3]、ob2」が作られる。



ここまでは問題なく理解できるであろう。

次に了解して欲しいことは、上の5つのオブジェクト 「ob[0]、ob[2]、ob[2]、ob[3]、ob2」全てに対してコンストラクタが呼ばれることである。

その際、注意しなければならないのは、samp クラスの定義にはコンストラクタは含まれないことである。

では何故コンストラクタを記述しないにも関わらずコンストラクタが呼び出されるのか。
実は、プログラマが明示的にコンストラクタを記述しなかった場合、コンパイラが自動的にコンストラクタを作成するのである。

自動的に作成されるコンストラクタは

samp::samp(){
}


のように、引数をとらず、何もしないコンストラクタである。(なお、引数をとらずに呼び出すことができるコンストラクタをデフォルトコンストラクタという)

このように、今までは触れなかったが、プログラマがコンストラクタを記述しないときも実は自動で (何もしない) コンストラクタが作成されていたことに注意して欲しい。


オブジェクトの配列 (2)

以下のプログラムを見てみよう。

#include <iostream>
using namespace std;

class samp{

    int a;

  public:
    samp(int n){ a = n; }   // () 引数つきコンストラクタ 

    void set_a(int n){ a = n; }
    int get_a(){ return a; }
};

int main(){

  samp ob[4]={-1,-2,-3,-4};   // () オブジェクトの配列の宣言

//  samp ob2;     // この行と
//  samp ob3[4];  // この行を有効にすると何が起こるだろうか?


  for(int i=0 ; i<4 ; i++) cout << ob[i].get_a() << ' ';

  cout << "\n";

  return 0;
}


この例は上の「オブジェクトの配列 (1)」の例とほとんど同じであるが、
() に示したように、「引数つきのコンストラクタ」を持つこととオブジェクトの宣言に初期値を設定していることが異なる。

容易に想像がつくように、初期値つきでオブジェクトの配列 ob[4] を宣言した場合、
4つのオブジェクト ob[0]、ob[1]、ob[2]、ob[3] それぞれに対し引数つきのコンストラクタが呼び出されて初期化される。

では再び資料から少し離れよう。
上記コード中、「この行とこの行を有効にすると何が起こるだろうか?」の二行を有効にしてみよう。 行の先頭の「//」を削除すれば有効になる。

この二行は1個のオブジェクト ob2 とオブジェクトの配列 ob3[4] を初期値なしで宣言しようとしており、文法的には何も問題がないように思われる。

しかし、この二行を有効にしてセーブしてからコンパイルしようとすると

'samp::samp()' に一致するものが見つからない(関数 main() )
'samp' 型の配列要素を初期化するデフォルトコンストラクタが見つからない(関数 main() )


なるエラーが出て、コンパイルに失敗する。 1行目は ob2 に対するエラーで、2行目は ob3[4] に対するエラーであるが、
いずれも「引数なしで呼ばれるコンストラクタ (デフォルトコンストラクタ) が存在しない」点が問題になっているのである。

つまり、「オブジェクトの配列 (1)」ではデフォルトコンストラクタは自動で生成されたが、今回は生成されない。
これは今回は引数つきのコンストラクタ samp(int n) を自分で定義したためである。
つまり、「デフォルトコンストラクタが自動で生成されるのは、プログラマがコンストラクタを一つも記述しなかった時のみ」であることに注意しよう。

では、「samp ob[4]={-1,-2,-3,-4};」、「samp ob2;」、「samp ob3[4];」の宣言全てを許すにはどうすれば良いか?
答えは、以下のようにコンストラクタを多重定義 (オーバーロード) することである。

class samp{
    …
  public:
    samp(){  }   // 引数なしコンストラクタ (デフォルトコンストラクタ)。何か処理を書いても良い。
    samp(int n){ a = n; }   // 引数つきコンストラクタ 
    …
};


この記述により、 がそれぞれ呼ばれるようになる。

このように、一つの関数名 (samp) で複数の機能を持たせる (samp() と samp(int n)) ことを多重定義 (オーバーロード) と呼ぶ

あるいは、以下のように引数つきのコンストラクタに「デフォルト引数」を持たせても良い。

class samp{
    …
  public:
    samp(int n=0){ a = n; }   // 引数つきコンストラクタ。引数なしコンストラクタは定義しない
    …
};


この場合は、 が呼ばれるようになる。

# (以下余談)
#
# C++ や多くのオブジェクト指向言語にはオーバーロード (多重定義 ; overload) とオーバーライド (再定義 ;override) の二つの用語があり、混同しやすい。
# オーバーロード (overload) とは「積荷 (load) を積みすぎる」という意味であるが、
# 上の例で言うと、一つの関数名 (samp) に対して、複数の意味 (samp() と samp(int n)) を持たせている点が「積み荷の積みすぎ」を想起させる。
# 一方オーバーライド (override) とは「踏みにじる、無視する」などの意味で、これは関数を上書きすることを意味する。(いずれ学ぶ)
#


オブジェクトのポインタ

以下のプログラムを見てみよう。

#include <iostream>
using namespace std;

class samp{

    int a, b;

  public:
    samp(int n, int m){ a = n; b = m; }   // 引数つきコンストラクタ

    int get_a(){ return a; }
    int get_b(){ return b; }
};

int main(){

  samp ob[4]={ samp(1,2), samp(3,4),  samp(5,6),  samp(7,8)}; // () 
  samp *p;

  p = ob;  // () 配列の開始アドレスを取得する。p = &ob[0] と書いても同じ。

  for(int i=0 ; i<4 ; i++){
    cout << p->get_a() << ' ';
    cout << p->get_b() << "\n";
    p++;                         // () p=p+1 や p+=1 と同じ
  }

  cout << "\n";

  return 0;
}


ポイントは () の行。

1行目は、「このような初期化の仕方もある」ということで頭に入れておけば良い。

2行目「p=ob;」は「配列の先頭アドレスをポインタ p に代入」 という意味である。理解できない人は第三回演習-01を復習すること。
配列の名前 (ob) だけを記述することで配列の先頭アドレスを取り出せることに注意。 「p=&ob[0];」と書いても同じ効果がある。

この時のメモリの模式図は下図 (a) のようになる。



その後、for 文内部で 「p++」 という記述があるが、これはポインタを一つ先へ進めるという意味で、
上図 (b) のような効果を与える。

for 文の記述は以下のようにも書ける。アロー演算子「->」とドット演算子「.」との使い分けに注意しよう。

  …
  for(int i=0 ; i<4 ; i++){
    cout << p[i].get_a() << ' ';
    cout << p[i].get_b() << "\n";
  }
  …




←第三回演習第四回-02 : new/delete によるメモリの動的管理→

第四回トップページへ

クラスから入る C++ へ戻る