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)_