ファイル分割編
開発が進むとメインのソースファイルが肥大化し、見通しが悪くなります。そこでファイルを分割します。
分割しやすいのはクラスで、定義場所をヘッダーファイルに移動できます。
※一般的にはソースファイルとヘッダーファイルに分割しますが、コンパイル順序を良く理解する必要があります。かなりややこしいため、部ではヘッダーファイルのみに分割しています。
#include "Motor.hpp"
Motor motor1{ 2, 3, 4 };
Motor motor2{ 5, 6, 7 };
void setup()
{
motor1.begin();
motor2.begin();
}
void loop()
{
motor1.move(-128);
motor2.move(128);
}
#pragma once
class Motor
{
int pinA;
int pinB;
int pinP;
public:
Motor(int pinA, int pinB, int pinP)
: pinA(pinA)
, pinB(pinB)
, pinP(pinP)
{
}
void begin()
{
pinMode(pinA, OUTPUT);
pinMode(pinB, OUTPUT);
}
void move(int power)
{
digitalWrite(pinA, (power >= 0) ? HIGH : LOW);
digitalWrite(pinB, (power <= 0) ? HIGH : LOW);
analogWrite(pinP, abs(power));
}
};
🌟 インクルードガード
#pragma once
はインクルードガードと呼ばれるプリプロセッサ命令です。
インクルード文はファイルの内容をコピーするだけなので、複数回インクルードすると多重定義でエラーが発生します。
インクルードガードを使うと、一度インクルードされたファイルは再度展開されません。
マクロを使って実装されることもあります。C 言語では主流ですが、C++ でも使います。#pragma once
が標準化されていないためです(事実上の標準というやつです)。
🌟 ファームウエアのビルド順序
ソースファイルは次のように実行ファイルへと変換されます。この工程はビルドとも呼ばれます。
graph LR
source(Main.cpp) --プリプロセス--> pre
pre(Main.i) --コンパイル--> object
object[[Main.o]] --リンク--> exe
lib[[Library.o]] --リンク--> exe
exe[(実行ファイル)]
拡張子
.cpp
: ソースファイル.hpp
: ヘッダーファイル.i
: プリプロセス後のファイル (ほぼソースファイル).o
: オブジェクトファイル.exe
: 実行ファイル (Windows)
ヘッダーファイルがある場合
ヘッダーファイルをインクルードしている場合、次のようになります。プリプロセスはただの結合なのでわかりやすいと思います。
graph LR
source(Main.cpp) --プリプロセス--> pre
header[Motor.hpp] --プリプロセス--> pre
pre(Main.i) --コンパイル--> object
object[[Main.o]] --リンク--> exe
lib[[Library.o]] --リンク--> exe
exe[(実行ファイル)]
複数のソースファイルがある場合
この場合どのようにコンパイルされるでしょうか?次のように結合されてからコンパイルされそうですよね。
graph LR
source1(Main.cpp) --プリプロセス--> pre
source2(Motor.cpp) --プリプロセス--> pre
pre(Main.i) --コンパイル--> object
object[[Main.o]] --リンク--> exe
lib[[Library.o]] --リンク--> exe
exe[(実行ファイル)]
実はソースファイルは別々にコンパイルされ、最後に結合されます。
graph LR
source1(Main.cpp) --プリプロセス--> pre1
source2(Motor.cpp) --プリプロセス--> pre2
pre1(Main.i) --コンパイル--> object1
pre2(Motor.i) --コンパイル--> object2
object1[[Main.o]] --リンク--> exe
object2[[Motor.o]] --リンク--> exe
lib[[Library.o]] --リンク--> exe
exe[(実行ファイル)]
このような順序にすることで、変更していないソースファイルを再度コンパイルする必要がなくなり、ビルド時間の短縮につながります。
しかしこれに起因した複雑怪奇なエラーが発生することもあります 🤢🤢
🌟 ヘッダーファイルに関数を定義してはいけない理由
実はヘッダーファイルには関数を定義してはいけません。
一旦 Motor クラスは忘れて、関数をヘッダーファイルに定義し、インクルードしたとします。しっかりインクルードガードもしてます。
実はエラーが隠れていますが、この場合エラーになりません。
複数のソースファイルからインクルードすると、リンクエラーが発生します。
1>ソース.obj : error LNK2005: "double __cdecl divide(double,double)" (?divide@@YANNN@Z) は既に Main.obj で定義されています。
1>C:\Users\xxxxxxxxxx\Main.exe : fatal error LNK1169: 1 つ以上の複数回定義されているシンボルが見つかりました。
1>プロジェクト "Main.vcxproj" のビルドが終了しました -- 失敗。
エラー原因
ソースファイルは別々にコンパイルされるため、インクルードガードを突破して関数が複数定義され、リンクエラーとなります。
graph LR
source1(Main.cpp) --プリプロセス--> pre1
source2(F.cpp) --プリプロセス--> pre2
pre1(Main.i) --コンパイル--> object1
pre2(F.i) --コンパイル--> object2
object1[[Main.o]] --リンク--> exe
object2[[F.o]] --リンク--> exe
lib[[Library.o]] --リンク--> exe
exe[(実行ファイル)]
プリプロセス後のファイルは次のようになります。
コンパイル後のオブジェクトファイルのイメージです。実際はバイナリデータです。
リンクの際、同じ名前の関数が複数あるためリンクエラーが発生します。
対応策 / インライン化
インライン関数として定義することで、ヘッダーファイルに関数を定義してもリンクエラーが発生しません。
インライン関数は複数のソースファイルで登場しても関数の実体は一つとできるためです。
対応策 / ソースファイルに定義
ヘッダーファイルに関数の宣言だけ書いておき、ソースファイルに関数の定義を書く方法もあります。C 言語の場合はこの方法を使います。
この方法が一般的ですが、関数の仕様変更時にヘッダーファイルとソースファイルの両方を修正する必要があったり、ファイルが増えるためインライン化の方が楽です。
🌟 クラスは?
クラスはヘッダーファイルに定義してもリンクエラーが発生しません。実はクラスのメンバ関数は暗黙の内にインライン化されています。
🌟 まとめ
- クラスはヘッダーファイルに分割する
- インクルードガードを使う
- ヘッダーファイルに関数を定義する際はインライン化する
- クラスのメンバ関数はインライン化されるため何もしなくて OK