C++で型がクラスの変数の初期化について

投稿者: | ↻ : 2019年1月28日

Arduinoのライブラリを作成するために、既存のコードを解析していて、C++言語の仕様でうろ覚えな部分があり調べ直しました。
動作検証は、ArduinoとMacBookで行いました。
Arduinoで使用できるのは、avr-g++コンパイラの対応範囲のみなので、どこまで対応しているかの検証も兼ねています。

検証環境

  • Arduino UNO Ver.3
  • avr-g++ (GCC) 4.9.2

C++で型がクラスの変数を初期化する記述について

次のコードが、C++言語の仕様としてどう処理されるのかに疑問を持ちました。
Arduinoのスケッチ例やライブラリで、型がクラスの変数の初期化は、この記述が使われています。


#include <iostream>

class Cat
{
public:
  Cat() : _age(1) {}
  Cat(int age) : _age(age) {}
  int age() { return _age; }
private:
  const int _age;
};

Cat tama = Cat();  // この2行が疑問部分
Cat mike = Cat(3); //

C++言語で、型がクラスの変数の初期化は次の記述でも可能です。


...
              // 最適なコンストラクタが選ばれ、インスタンスが生成される
Cat tama;     // この場合は、引数を持たないコンストラクタ = Cat()
Cat mike(3);  // この場合は、整数を引数にもつコンストラクタ = Cat(int age)

初めてC++言語を学習した時は、この記述でした。
一つ目などは、Javaの経験があると初期化を行っていないのでnullになると思われますが、C++言語ではインスタンスが生成されます。

疑問点


...
Cat mike = Cat(3);

この場合に「Cat mikeと、= Cat(3)で2回インスタンスが生成されるのか?」と疑問が湧きました。結果としては1回だけのインスタンス生成となります。

以降で、検証コードと、調べたC++言語の仕様をまとめます。

コードによる検証と確認

C++言語の仕様を確認する前にコードで検証してみました。
(検証を優先したコードなので力技です)

Arduinoのスケッチ

検証コードを、Arduino Uno 3で実際に動かして確認します。

1回となる例


int newCount = 0;
int delCount = 0;

class Sample
{
public:
  Sample() { newCount++; }
  ~Sample() { delCount++; }
};

void setup() {
  Serial.begin(9600);
}

void loop() {
  {
    Sample obj = Sample();  //宣言と初期化(これは代入ではない)
  }
  delay(1000);
  Serial.print("new="); Serial.print(newCount);
  Serial.print(",del="); Serial.println(delCount);
}

結果(シリアルモニタ)

2回となる例


int newCount = 0;
int delCount = 0;

class Sample
{
public:
  Sample() { newCount++; }
  ~Sample() { delCount++; }
};

void setup() {
  Serial.begin(9600);
}

void loop() {
  {
    Sample obj;       // 宣言と初期化
    obj = Sample();   // 代入(新しいインスタンスが生成された後にコピーされる)
  }
  delay(1000);
  Serial.print("new="); Serial.print(newCount);
  Serial.print(",del="); Serial.println(delCount);
}

結果(シリアルモニタ)

C++言語仕様の確認とまとめ

仕様が複雑なので、理解できたところまでをまとめ、今後も調査を続けられるように出来るだけ用語は正しく記します。

宣言(Declaration)

変数は利用する前に宣言が必要です。
宣言は、指定子(specifier)と宣言子(declarator)に分けられます。

  • 指定子で、型を指定します。
  • 宣言子で、なんという名の変数かを宣言します。

int // 指定子
age // 宣言子(カンマ区切りで複数の宣言ができます)
;

初期化(initialization)

宣言子には初期値を指定するために、初期化子(Initializer)を含められます。
初期化には複数の種類があります。以降は、クラス型に関連する部分だけを抜き出しました。


class T
{
  ...
};

T y1;             // (1-1)
T y2(1);          // (1-1)

T *x1 = new T     // (2-1)
T *x2 = new T(1)  // (2-2)

T z1 = x;         // (3-1)
T z2 = T(1);      // (3-2)
  • (1-1) この文法で、初期化子を省略すると、オブジェクトはデフォルト初期化されます。
    Javaなどと違い、nullにはなりません。

  • (1-2) この文法で、初期化子を指定すると、オブジェクトは直接初期化されます。
  • (2-1) new初期化子(式リストか、値リスト)を省略すると、オブジェクトはデフォルト初期化されます。
  • (2-2) new初期化子を指定すると、オブジェクトは直接初期化されます。
  • (3-1) この文法で、初期化子を指定すると、オブジェクトはコピー初期化されます。
    コピーと付いていますが代入とは異なり、コピーコンストラクタやコピー代入演算子は利用されません。

  • (3-2) この文法で、初期化子を指定すると、オブジェクトは直接初期化されます。

(3-2)の文法が、調べたかった書き方です。

まとめ

言語仕様上にて「目的の型がクラス型で、初期化子がコピー初期化、初期化子の型が指定子の型と同じか、その派生クラスならば、直接初期化と同じ方法で初期化される」と、動作が不定や未定義ではないことが確認できました。

初期化関連の仕様が複雑すぎるです_(:3 \z)_

参考資料