C++
参考
講義
VSCode でC++を使う
拡張機能でC/C++を入れる
Raylibを導入する
今回はv5.5
GItLabからサンプルファイルをもらう
C:\Users\watas\260304_c_fundamentals\vscode-template-main.zip
基礎部分
main()関数
コード実行を開始するエントリーポイントとしての役割を持つ。
code:c++
int main() // Header
{
// Body
Statement1(); // ステートメントと呼ぶ
StartMusic(); // PartyLibraryのStartMusicを呼べる
}
用意したライブラリを、#includeで読み込むイメージ
⭐️なにがintなのか
OSに対して、プログラムのステータスコードを報告するためにreturn 0;等として返している。C++はOSとやり取りする言語なので、このintの返り値0, -1, 1 を返すことが作法として存在する。
ハローワールドする
まず、エントリーポイントであるmain()の記述
code:cpp
int main()
{
printf("Hello, World! \n");
printf("Osaka, Japan \n");
printf("Video Games \n");
}
ビルド
https://scrapbox.io/files/69a7e4e0f1c53c173222d454.png
実行できるようになる
code:terminal
PS C:\Users\watas\260304_c_fundamentals\Project> .\hello
Hello, World!
std::のような記載について
code:cpp
int main() {
std::string myName = "勇者";
std::cout << myName << "が現れた!" << std::endl;
return 0;
}
単純なstring型でもstd::というようにライブラリの呼び出しがある。C++は結構こういうものが多い(歴史あり)
最低限知っとけリスト
std::cout: シーアウト いわゆるprint。コンソールに出す。
std::endl: エンドル (end line)。改行しつつ出力をリフレッシュ。
std::vector: リスト、配列。
code:cpp
int main() {
std::vector<int> testVector;
testVector.push_back(10);
std::cout << testVector0 << "をpush" << std::endl; return 0;
}
std::unordered_map: ハッシュマップ(連想配列)。
code:cpp
int main() {
// アイテム名(string)」と「価格(int)」のペア
std::unordered_map<std::string, int> shopMenu;
// 値の取り出し
std::string target = "やくそう";
// キーが存在するかチェックしてから表示するのが安全
if (shopMenu.count(target)) {
std::cout << target << "の価格は " << shopMenutarget << " ゴールドです。" << std::endl; }
// 要素を削除する場合
shopMenu.erase("せいすい");
return 0;
}
std::unique_ptr: スマートポインタ(後述)。
std::shared_ptr: 複数場所から同じメモリを共有したいときに使うスマートポインタ。
ビルドについて
流れ
Preprocessing
#includeなどを読み込み、準備を整える
Compilation
人間の可読出来る.cpp(高級言語)を、.obj(機械語)に翻訳
Linking
ばらばらに作られた機械語データやライブラリをつなぎ合わせて、最終的な.exeファイルを完成させる
include関連
<>での読み込み
C++の提供する公式ライブラリ。ビルトインされたもの
""での読み込み
外部ライブラリ。それこそRaylibなど。
.hファイル
ヘッダファイル。ライブラリについての情報を持つ
変数
floatとdouble
floatは6-7桁程度の小数点を保持出来て、精度が少し緩めの代わりにメモリの節約もできる。 doubleは15-16桁の小数点を保持できる代わりにメモリを要する。なので、特に精度が必要ない場合はfloatを使うとよい。
デバッグ
ブレークポイント
赤い丸でつけられるあれ。DebuggingでRunさせると、その行の手前で止まる。
マウスを重ねると、現在どんな値が入っているか確認できる
https://scrapbox.io/files/69b7a9b3b9e150750ae4b8d8.png
↑の場合は、float root_beerというものにマウスを合わせているが、1.401...という値が入っている
-45乗なので、とても小さな値(まだ、1.99という値で初期化されていない)。メモリ位置を参照しているという意味合いにもなる。
F11 などで、Step intoで次の工程に行くと、1.9900001とかの値が入っている
多少不正確な理由は、浮動小数点の値には常にある程度の不正確性があるため。
最初から入っているe-45...みたいなものがガベージデータと呼ばれる(定義漏れ)。
これを防ぐのが初期化で、対応がいくつかある
=による代入
中カッコを使用した宣言
code:cpp
double cheese_burger{5.99};
double cheese_burger{}; // 0に初期化
bool isSet{}; // デフォルトはfalseなので、false
Watch対象に加える
https://scrapbox.io/files/69b7afd7b9e150750ae4c9a4.png
デバッグモード中にブレークポイントを設定して止めて、Add to Watchで追いかける対象のデータ内容をチェックできる
ループ処理
こんな感じでウインドウを出す処理の途中で無限ループを挟むと、ウィンドウが確認できる。ただ、代わりに砂時計マークがウィンドウのマウスカーソル上では存在して、よくあるプログラムの応答がありません、というような待ち状態になり強制終了するかどうか選択する形になる
https://scrapbox.io/files/69b7b0dcb9e150750ae4cbfa.png
⭐️C++とは関連が少ないが、ゲーム関連
スプライトが1枚の画像である理由
複数画像でなく、単一画像にまとめることでメモリを節約している
メモリの話
プログラムが動くとき、メモリ(RAM)上には主に2つのデータ置き場が用意される。それがスタックとヒープ。
スタック
関数の中のローカル変数などを一時保存するための領域。スコープ{ }を抜けると自動で消去。
code:cpp
void Attack() {
// これらの変数はすべてスタックに積まれる(作られる)
int damage = 50;
Player tempPlayer;
} // ← 到達した瞬間、damage も tempPlayer も自動で消滅
LIFO(最後に入れたものを先に出す)で管理されているので、メモリ確保や解放が瞬時に終わる。
ただし狭い(数MB~)ので、巨大な3Dモデルデータなどを作ろうとするとプログラムが強制終了する。
これが有名な スタックオーバーフロー
ヒープ
スタックで管理しきれないような巨大なデータや、関数が終わっても消えずに残っていて欲しいデータを置く場所。
code:cpp
void SpawnBoss() {
// new を使うと、ヒープに巨大なBossデータが作られる
// 今回はそのポインタ(住所情報)だけを bossPtr に渡している
Boss* bossPtr = new Boss();
bossPtr->Roar();
} // ← 関数が終わると、スタックにあった鍵(bossPtr)は消滅する。
// しかし、ヒープにある Bossの実体 は delete されていないので残り続ける
パソコンメモリの許す限り広大なスペースを使えるが、スタックと比べるとアクセスや準備に時間がかかる。また、プログラマが削除処理を命令するまで残り続ける。ヒープを借りる命令がnewで、返却する命令がdeleteである。
code:正しく管理.cpp
void SpawnBossCorrectly() {
Boss* bossPtr = new Boss(); // ヒープ取得
bossPtr->Roar();
delete bossPtr; // 役目を終えたら返却。
bossPtr = nullptr; // 安全のためポインタを使えない状態としておく
}
ちなみにC#, PHPはnew Player()とするとヒープが作られるが、使わなくなったらガベージコレクタが裏で自動的にdeleteで解放してくれる(この瞬間に多少かくつくことがある)。C++はこれを自身で管理するため、メモリ領域をフルに活かすことができるが、謝るとメモリリークでクラッシュするというケースも起こり得る。
deleteを忘れてバグが生まれるため、現代のC++ではスマートポインタを使う事が多い(後述)。
クラス周り
クラスファイルの分割
C++では、1つのクラスをヘッダファイル.hとソースファイル.cppに分割して管理するのが基本。.hには宣言だけ(変数名、関数名)を書いて、.cppには実装を書く。
code:Player.h
#pragma once // ← 同じファイルが何度も読み込まれるのを防ぐ定義 class Player {
private:
std::string name;
int hp;
public:
Player(std::string playerName); // コンストラクタ(名前だけ)
void Update(); // Update関数(名前だけ)
};
code:Player.cpp
// 「Playerクラスの所属である( Player:: )」と明記して中身を書く
Player::Player(std::string playerName) : name(playerName), hp(100) {
// 初期化の処理
}
void Player::Update() {
std::cout << name << " は更新された!" << std::endl;
}
なぜ分けるか
C++は実行速度が早い代わりに、コンパイルが遅い。巨大なエンジンだとコンパイルが大変。1つのファイルにすべて書くと、そのファイルを読み込んでいるすべてのファイルが連鎖的にコンパイルされる。ただし.hといように分割しておけば、他ファイルの連鎖的なコンパイルを防止できて開発効率が上がる。
関数について
code:cpp
class Bullet {
private:
float speed;
bool active;
public:
bool IsActive() const { return active; }
関数の後ろのconstは、クラス内の変数を書き換えないという宣言。ここで言うと、speedやactiveは、この関数内では参照するだけだという宣言。
クラスを見る
code:cpp
class Player : public GameObject {
public:
Player() { /* コンストラクタ */ }
};
class Player : public GameObject
GameObjectクラスから継承したということを示す。publicをつけるのが一般的(つけないと親のメソッドが外から呼べない)。
public:
ここからの記載はすべてpublicとして処理する、というブロック形式で書く。
⭐️PHPとかはpublic void Test()とか1つずつ定義していたが、そうではない
クラスを拡張した例と、仮想関数
code:cpp
class GameObject {
public:
// 【重要】親クラスのデストラクタには必ず virtual をつける(メモリリーク防止)
virtual ~GameObject() {
std::cout << "GameObject が破棄されました。" << std::endl;
}
// 仮想関数(子クラスで具体的な処理を上書きする)
virtual void Update() = 0;
virtual void Draw() = 0;
};
class Player : public GameObject {
public:
void Update() override {
...
}
virtual ~GameObject()
~から始まる関数は、デストラクタと呼ばれる
インスタンスが破棄(メモリから消える)ときに呼ばれる関数。C++では親クラスに子クラスを入れている場合、親クラスのデストラクタにvirtualを付与していないと、親クラスの部分だけが破棄されてPlayer特有のデータがメモリに残り続けてしまう仕様が存在する。これがメモリリーク。そのため、親になる可能性があるクラスのデストラクタには必ずvirtualをつけておくこと。
関数周辺の記述
キーワード回り
virtual
仮想関数。継承先のクラスで上書きされる前提であるということを示す。UEにおいてはBeginPlay()など。
付与すると、親クラスのポインタに子クラスのインスタンスが入っている状態でも、中に入っている子クラスの関数が呼ばれる。付与しなければ、親クラスのポインタに子クラスのインスタンスが入っている状態でも、親クラスの関数がそのまま呼ばれる。
override
「親クラスのvirtual関数を上書きする」ということを明示するための指定子。virtual 付与時に、親クラスにも同じ名前 / 引数を持つ関数があるかどうかをコンパイラにチェックさせ、エラーを出してくれるようになる。付与しなくとも正しい名前 / 引数を渡していれば正常に上書きされる挙動となるが、複雑な関数になって書き間違えた場合エラーを出さずに素通りしてしまうという挙動を防ぐ。
= 0
純粋仮想関数。インターフェースなどのメソッド定義と同じ役割を持つ。親クラス側では関数の処理の中身を書く必要が無くなる。同時にこの関数を1つでも持つクラスは「抽象クラス」として扱われ、newなどでインスタンス化できなくなる。また、継承した子クラスはこの関数を必ず実装しなければコンパイルエラーが発生する。
ちなみにC++には、abstract等のキーワードは標準規格としては存在せず、= 0で表す
挙動例
override
なし
code:Player.h
class Player : public IDamageable
{
public:
void Attack(IDamageable& Target);
void TakeDamag(int Damage); // override なしで Typo したとき
Player クラス 内部に純粋仮想関数が存在する(Idamageableを持っている)ので、オブジェクトを作ることが出来ないというエラーが発生する
1>C:\SampleC\SampleC\main.cpp(77,12): error C2259: 'Player': cannot instantiate abstract class
あり
code:Player.h
void TakeDamag(int Damage) override; // override 付きで Typo したとき
基底クラスのメソッドTakeDamagがオーバーライドしなかったというエラーになる。
1>C:\SampleC\SampleC\Player.h(28,7): error C3668: 'Player::TakeDamag': method with override specifier 'override' did not override any base class methods
なしとありで、どちらもエラーが出るのだがoverrideがあるほうがエラー箇所が明示的となる。
virtual
code:.h
# 親
class Monster
public:
void NormalAttack();
virtual void MagicAttack();
# 子
class Slime : public Monster
public:
void NormalAttack();
void MagicAttack();
code:cpp
# 親
void Monster::NormalAttack()
std::cout << "モンスター の通常攻撃!\n";
void Monster::MagicAttack()
std::cout << "モンスター は魔法を発動!\n";
# 子
void Slime::NormalAttack()
std::cout << "スライム は 体当たり した!\n";
void Slime::MagicAttack()
std::cout << "スライム は溶解液 を飛ばした!\n";
このようにして、main()で実行したとする。
code:main.cpp
// スライムを生成し親クラスの参照として受け取るケース(Slime を Moster として扱う)
Slime MySlime;
Monster& TargetMonster = MySlime;
TargetMonster.NormalAttack();
TargetMonster.MagicAttack();
モンスター の通常攻撃!
スライム は溶解液 を飛ばした!
結果として、TargetMonster.NormalAttack();は、Slimeで書いたにも関わらず親クラスの同名の関数を上書きできていない。Monster&のため、子クラスであろうと親を呼ぶという挙動となる。overrideは付与しても付与しなくとも良いのだが、virtualは親クラスの関数を上書きしたい場合必須。
⭐UE 5.5 などでは、親クラスの関数を呼ぶときSuper::BeginPlay();とするが、Superは UE が独自に用意した設計のため、Pure C++では使えない。子クラスから親クラスの関数を呼びたいときは、名前を直接指定する形。
code:cpp
void Slime::MagicAttack()
{
Monster::MagicAttack();
std::cout << "スライム は溶解液 を飛ばした!\n";
UE5 における挙動例
code:cpp
protected:
virtual void OnActivated(); // UFUNCTION()は BP から呼ばないので、不要
private:
UFUNCTION()
void OnItemBeginOverlap(
UPrimitiveComponent* OverlappedComponent, ...
);
# cpp
void AItemBase::OnItemBeginOverlap(UPrimitiveComponent* OverlappedComponent, ...)
{
//AItemBase::OnActivated(); クラスを明示すると、子クラスの処理が呼ばれない
OnActivated();
}
code:Weapon.cpp
protected:
virtual void OnActivated() override;
# cpp
void AWeapon::OnActivated()
{
Super::OnActivated();
UE_LOGFMT(LogTemp, Warning, "AWeapon::OnActivated()");
}
という具合に書くとよい。
仮想クラスとインターフェース
C++ における特徴
C#, PHP ではインターフェースであることをキーワードで示すことが出来るが、C++ではそういったことはしない。純粋仮想関数= 0をクラスに持たせることで、そのクラスは仮想クラスとして扱われるようになる。そういったことやクラス名をIDamageableなど明示的な名前に命名することでインターフェースであることを示すようにする。
インターフェースは中身のないクラスとなるため.hだけで事足りるケースがほとんど(.cppを作らない)。
継承の際に使われるpublicというキーワードは、public 継承と言われる機能での継承。private 継承や protected 継承という概念も存在し、親の機能をもらいつつ外部から一切見えなくすることができる。基本的にpublic継承でよい。
挙動例
code:IDamageable.h
class IDamageable
{
public:
virtual ~IDamageable() = default;
virtual void TakeDamage(int damage) = 0;
};
今回の場合、TakeDamage(int damage) = 0という記載から、コンパイラが抽象クラスであると判断する。
デストラクタ
defaultはvirtual ~IDamageable() {}とほぼ同じ。中身のない関数であることをキーワードで明示している。
(上記でも書いているが)virtualとすることでインターフェースを継承したクラスをdeleteしたとき、親と子どちらのデストラクタも呼ぶようにするという表示。
オブジェクトのコピーとコピーコンストラクタ
code:phpの例.php
class SimpleClass { public $var = 'Simple'; }
$s1 = new SimpleClass();
$s2 = $s1;
$s2->var = "s2!";
echo $s2->var . PHP_EOL; // s2!
echo $s1->var . PHP_EOL; // s2!
たとえばPHPやC#では、Player p2 = p1としたとき、同じ実体を指す(メモリ上の同じ場所を指す参照渡しになる)。そのためp2を書き換えるとp1も書き換わることになるが、C++ではそうはならず、中身が全く別の同じクローンを生成することになる(PHPで例えるとPlayer p2 = clone p1の挙動)。このクローンを作るときに自動で呼ばれるのがコピーコンストラクタという関数。ただし、クラス内部にポインタを所持している場合、この関数に頼り切りだと同じメモリ番地をコピーすることになりプログラムがクラッシュする。
code:だめな例.cpp
class BadPlayer {
public:
int* hp; // HPの値を保存するメモリのポインタ
// コンストラクタ
BadPlayer() {
// ヒープ(プログラムの実行中に動的にメモリを確保・解放するRAM領域)領域に新しくメモリを確保
hp = new int(100);
}
// デストラクタ
~BadPlayer() {
delete hp; // 役目を終えたらメモリを解放
std::cout << "メモリを解放しました!" << std::endl;
}
};
int main() {
BadPlayer p1;
// コピーが発生(C++が自動で浅いコピーをする)
// p1.hp と p2.hp は「全く同じ住所」を指している状態になる
BadPlayer p2 = p1;
return 0;
} // ← main関数が終わる時、p1 と p2 のデストラクタが呼ばれる
# free(): double free detected in tcache 2
# Aborted (core dumped)
main関数が終わったとき、~BadPlayer()が呼ばれてdelete hp;が走る(役目を終えたメモリの解放)。ただしp1とp2は同じhpポインタを持っており、p1で解放したのち、p2でも同じ位置を解放させようとする。この場合二重解放(Double Free)と呼ばれるバグになり、プログラムがクラッシュする。
code:考慮した例.cpp
class GoodPlayer {
public:
int* hp;
GoodPlayer() {
hp = new int(100);
}
// コピーコンストラクタを自身で定義する
// クラス名(const クラス名& コピー元)
GoodPlayer(const GoodPlayer& other) {
// 同じ住所を使い回さず、新しくメモリ(new)を確保する
hp = new int(*other.hp);
std::cout << "Deep Copy 実行" << std::endl;
}
~GoodPlayer() {
delete hp;
}
};
int main() {
GoodPlayer p1;
GoodPlayer p2 = p1; // 自分で書いたコピーコンストラクタが呼ばれる
return 0;
}
自身でコピーコンストラクタを正しく定義して、純粋なコピーだけでなく、値を取ってそれを新しいメモリに割り当てるという処理を書いてやろう。
⭐️モダンC++では、そもそもインスタンスをコピーさせない設計にする or スマートポインタ(後述)に頼るのが主流。
code:cpp
class Player {
public:
Player() {}
Player(const Player&) = delete; // コピーコンストラクタをdeleteして使用禁止にする
};
int main() {
Player p1;
Player p2 = p1; //ここでエラーになる
return 0;
}
// Main.cpp:10:12: error: call to deleted constructor of 'Player'
// 10 | Player p2 = p1;
// | ^ ~~
// Main.cpp:5:5: note: 'Player' has been explicitly marked deleted here
// 5 | Player(const Player&) = delete; // コピーコンストラクタをdeleteして使用禁止にする
// | ^
ポインタ・参照・const参照
C++ではポインタや参照渡しなどの要素を、書き手が管理するのが基本
⭐️何もつけない形(値渡し)で渡すのがNGな理由
C++はデフォルトでクローンを作る。関数にPlayer pを渡すと、関数が呼ばれた瞬間にコピーが作られて関数に渡されるため、元のデータは変わらない。
code:cpp
// 値渡し: Player @
void TakeDamageValue(Player p, int damage) {
p.hp -= damage; // ここで減っているのはcloneのHPなので、myPlayerは変化しない
std::cout << "関数の中のHP: " << p.hp << "\n";
}
// main()
TakeDamageValue(myPlayer, 20); // myPlayerのcloneが作られて関数に渡される
ポインタ渡しPlayer* p, ->
code:cpp
class Player {
public:
int hp = 100;
};
// * p (ポインタ渡し)
void TakeDamagePointer(Player* p, int damage) {
// ポインタが空かどうかのチェック
if (p != nullptr) {
p->hp -= damage; // 中身へのアクセスはアロー演算子
std::cout << "ポインタ渡し: HPが " << p->hp << " になった!\n";
}
}
int main() {
Player myPlayer;
TakeDamagePointer(&myPlayer, 20);// 変数の前に & で変数のアドレスを取得できる
return 0;
}
ポインタ自体が空かどうかをnullptrでチェックできるので、検知処理中に対象が範囲外だとか、そういったときも空チェックできる
対象を切り替えたいときは別の住所に書き換えて違う対象を選び直せる
⭐️ちなみに、TakeDamagePointer(&myPlayer, 20)のdamage部分である20も値渡しとして扱われている。ただしデータサイズが小さく、かつ関数の中でもとの値を書き換える必要がないため値渡しを使っても構わない。myPlayerとかは書き換えている + クラス内部の処理がUpdateやらTakeDamageやらたくさんあったときコピーにある程度の工数がかかり、かつ毎秒60fpsほどで動かした場合、当然処理落ちの原因になり得るためやっちゃ駄目。
参照渡しPlayer& p, .
code:cpp
// & p (参照渡し)
void TakeDamageReference(Player& p, int damage) {
// 絶対に中身が存在することが保証されているので、nullptr のチェックはしない
p.hp -= damage; // ドット . で値を取り出す
std::cout << "参照渡し: HPが " << p.hp << " になった!\n";
}
int main() {
Player myPlayer;
TakeDamageReference(myPlayer, 20); // 住所(&)を取る必要はなく、そのまま渡す(裏で自動的に参照される)
return 0;
}
もとの変数に別の名前をつけて、弄るというようなイメージ。
メリット
必ずそこにデータがあるということを前提としたコードを書ける。
const参照渡し const Player& p
code:cpp
// const をつけることで読み取り専用の参照になる
void PrintPlayerHP(const Player& p) {
std::cout << "現在のHP: " << p.hp << "\n";
// p.hp = 999; // ← こんな感じで書き換えられなくなる(エラーで落ちる)
}
プリント専用の関数などで誤って数値が弄られてしまうケースを防ぐことができる。
モダンC++において
基本は参照渡し& か const参照const &を使う。安全で早い。
対象が存在しないケースがあるとき、途中で対象を変更したいなど、明確にポインタを使う理由があるときはポインタ*を使えばよい。
メンバ初期化子リスト
クラス変数を初期化するときのC++でのベストプラクティス。高速化とC++のコンパイルを正しく通す役割がある。
code:ブロック内での代入例.cpp
// パターンA:C#やPHP風の書き方(ブロック内で代入)
class BadExample {
std::string name;
public:
BadExample() {
name = "勇者";
}
};
code:初期化小リスト.cpp
// パターンB:C++の正しい書き方(メンバ初期化子リスト)
class GoodExample {
std::string name;
public:
// コンストラクタの()の後ろに : を書き、変数名(初期値) を並べる
GoodExample() : name("勇者") {
// ブロックの中は空っぽでOK
}
};
なぜパターンBが良いか
C++のオブジェクト生成には厳格な順番があるため、コンストラクタが実行される前に必ず全てのメンバ変数が何らかの形で生成(初期化)される。
A
nameという空の文字列が生成される
{ }でname = "勇者"という値が上書きされる
B
最初からname = "勇者"というデータが作られる。
この程度なら誤差だが、画像データや長い文字列になると空生成→上書きという手間が塵積で負荷の要因となる恐れがある。
さらに、メンバ初期化小リストでないと、コンパイルが通らないケースがある
code:cpp
class Player {
const int maxHp; // 変更不可能な最大HP
int& currentScore; // 外部のスコア変数を参照する(&)
public:
// ↓ コロン(:)を使って生まれた瞬間に値を入れるようにしなければ通らない
Player(int hp, int& score) : maxHp(hp), currentScore(score) {
}
};
const変数(定数)であるconst、 参照変数である&どちらもルールがある。定数は後から上書きすることはできず、参照変数は生まれた瞬間から中身が決まっていなければならない。そのため{}で代入を試みるとエラーとなる。
code:エラー例.cpp
class Player {
public:
const int maxHp; //
Player() {
maxHp = 10; // ここで空のmaxHPのconstを書き換える形になるので
}
};
int main() {
Player p;
std::cout << "Max HP: " << p.maxHp << std::endl; // エラーになる
return 0;
}
リストとスマートポインタ
code:コードの例.cpp
int main() {
InitWindow(400, 300, "C++ & Raylib - Smart Pointers & Polymorphism");
SetTargetFPS(60);
// std::unique_ptr を使うことで、自動メモリ管理(擬似ガベージコレクション)を実現
// 親クラス(GameObject)の型を使うことで、PlayerもEnemyも同じリストに入れられる
std::vector<std::unique_ptr<GameObject>> gameObjects;
// リストにオブジェクトを追加(std::make_unique で生成)
gameObjects.push_back(std::make_unique<Player>());
gameObjects.push_back(std::make_unique<Enemy>());
std::vector
C#でいうList<T>, PHPでいう配列の定義。サイズ可変のリストを定義している。
std::unique_ptr
スマートポインタ
C#やPHPは不要となったメモリを自動で掃除してくれる(ガベージコレクション)。ただしC++は古いものだと、new Player()などで作ったものは、使い終わったときに自身でdeleteと定義しておかないとパソコンのメモリを食いつぶしてしまう。そこで使われるのがスマートポインタ。手動でdeleteし続けるのはバグの原因となるため、こちらで指定させたポインタが役目を終えたとき、中のメモリも自動でdeleteするという処理が含まれた機能。このおかげである程度処理を自動化できる。
std::make_unique<Player>()
new Player()のような、スマートポインタを用いたインスタンス生成
スマートポインタstd::unique_ptrについて、もっと詳しく
ポインタ(raw pointer)の最大の問題は、ヒープを解放し忘れること。ならば、スタックからポインタが消滅する瞬間にヒープもdeleteしてくれるような物を作ればいいのでは、という思想から生まれた。
code:cpp
class Player {
public:
Player() { std::cout << "Playerがヒープに生成されました\n"; }
~Player() { std::cout << "Playerがヒープから削除されました!\n"; }
};
class MySmartPointer {
private:
Player* rawPointer; // 本物の鍵(生ポインタ)を隠し持つ
public:
// コンストラクタ:ヒープで作られたデータ(new Player)を受け取って保管する
MySmartPointer(Player* p) {
rawPointer = p;
}
// デストラクタ
~MySmartPointer() {
// この箱がスタックから消滅する瞬間に、中身の生ポインタも道連れにして delete する
delete rawPointer;
}
// 中身にアクセスするための -> 演算子を使えるようにする
Player* operator->() {
return rawPointer;
}
};
void TestFunction() {
// ヒープにPlayerを作り、その鍵をスタック上の「賢い鍵箱」に入れる
MySmartPointer smartPtr(new Player());
// 普通のポインタと同じように使える smartPtr->Update();
} // ← 関数が終わると、スタック上の smartPtr が自動消滅する
// その瞬間、MySmartPointerのデストラクタが呼ばれ、自動で delete rawPointer が実行される
int main() {
// ...
}
「スタックは{ }を抜けると自動でデストラクタが呼ばれて消滅する」というルールがあるので、それを活かしてヒープ領域も掃除させるという設計にした考え方。
今回のコードで言うと、
code:cpp
// 1: std::unique_ptr(スマートポインタ) でリストを作る
std::vector<std::unique_ptr<GameObject>> gameObjects;
// 2: リストにPlayerとEnemyを追加
gameObjects.push_back(std::make_unique<Player>());
gameObjects.push_back(std::make_unique<Enemy>());
// 3: ゲーム処理...
// 4: メイン関数の終了 ※ } に到達
gameObjectsリストはスタックに存在する。リストの中はスマートポインタが入る。
Player, Enemyの実体はヒープにある。
main関数が終わったとき、スタック上のgameObjectリストが消える。
同時に、内部のスマートポインタのデストラクタが起動。自分が管理していたヒープ上のPlayer, Enemyに対してdeleteを実行し、メモリが漏れることなくプログラムが終了する。
実際のコードを読んでみる
code:cpp
class Player {
private:
Vector2 position;
public:
Player() : position({ 200.0f, 250.0f }) {}
// 参照渡し(&)
// 弾のリストの「実体」を関数に渡して、Playerが直接リストに弾を追加できるようにする
void Update(std::vector<std::unique_ptr<Bullet>>& bulletList) {
// 移動処理
if (IsKeyDown(KEY_RIGHT)) position.x += 4.0f;
if (IsKeyDown(KEY_LEFT)) position.x -= 4.0f;
// スペースキーで発射
if (IsKeyPressed(KEY_SPACE)) {
// std::make_uniqueでスマートポインタを作り、渡されたリストに追加!
bulletList.push_back(std::make_unique<Bullet>(position));
}
}
void Draw() const {
DrawRectangleV({position.x - 15, position.y - 15}, {30, 30}, BLUE);
}
};
std::vector<std::unique_ptr<Bullet>>& bulletList
std::vectorのリスト宣言
std::unique_ptr<Bullet>で、Bullet型のスマートポインタ宣言
& 参照渡しで。
bulletList.push_back(std::make_unique<Bullet>(position));
bulletList.push_back(): リスト(vector)の末尾に要素を追加する関数
std::make_unique<Bullet>(position): ヒープにBulletの実体を作り、スマートポインタで包み込む。引数としてpositionを受け取っているので、この位置からインスタンスを生成するという意味になる
main関数の例
code:cpp
int main() {
InitWindow(400, 300, "C++ & Raylib - Shooting Test");
SetTargetFPS(60);
Player player;
std::vector<std::unique_ptr<Bullet>> bullets;
ここでは、playerもbulletsもスタックに作られている(newで宣言していないので)。つまり、main()が終わるときに自動で消滅する。
code:cpp
while (!WindowShouldClose()) {
// --- Update (更新処理) ---
player.Update(bullets);// プレイヤーに弾リストの「参照」を渡す
// 弾の更新 (範囲for文)
for (auto& b : bullets) {
b->Update();
}
bulletsをbとしてforで回す。それぞれ個別にb->Update()を回す。
auto: 型推論。varとかと同じ。
code:cpp
for (std::unique_ptr<Bullet>& b : bullets) { b->Update(); }
↑と一緒。こっちのほうが丁寧なのでは?という見方もできるが、コンパイラからするとstd::vector<std::unique_ptr<Bullet>> bullets という定義の時点で把握している。なのでautoと書いてしまって構わない。
// なのでautoで別にいい。
code:cpp
// スマートポインタ: 不要になった弾をリストから消す処理
for (int i = 0; i < bullets.size(); ) {
if (!bulletsi->IsActive()) { // erase を呼んでリストから外す。
// その瞬間、スマートポインタの寿命が尽き、自動でデストラクタが呼ばれて delete される!
bullets.erase(bullets.begin() + i);
} else {
i++;
}
}
まず、通常のforと違ってi++の記載がない。
elseにi++を書いている理由は、要素がeraseされたあとにインクリメントするとバグになるから。C++のリストは、eraseで消すとそれより後ろにあった要素が自動的に手前にズレてくるので、弾のチェックが飛ぶ。
弾丸個別処理としては、画面外に弾丸があった場合はeraseでリストから除外。このときデストラクタが走り、管理していたヒープ上の実体もdeleteし、メモリを解放する形になる
code:cpp
// --- Draw (描画処理) ---
BeginDrawing();
ClearBackground(RAYWHITE);
player.Draw();
for (const auto& b : bullets) {
b->Draw();
}
EndDrawing();
}
CloseWindow();
return 0; // OSに正常終了報告
}
forでbulletそれぞれの描画処理。constをつけることで、Drawで行われることは弾の座標などを見るだけで、書き込んだりしないということを明示しているような書き方
マクロ
例えば、Sphereを描く処理
code:デフォルト.cpp
UWorld* World = GetWorld();
if (World)
{
FVector Location = GetActorLocation();
DrawDebugSphere(World, Location, 25.f, 12, FColor::Red, false, 30.f);
}
というように、まずワールド情報をポインタとして取得できるかどうか決めてから設計している。ただこれを、マクロというような形でいつでも呼び出せるような形に変形することができる
code:マクロ設定.cpp
#define DRAW_SPHERE(Location) if (GetWorld()) DrawDebugSphere(GetWorld(), Location, 25.f, 12, FColor::Red, true); // ...
FVector Location = GetActorLocation();
DRAW_SPHERE(Location)
THIRTYを30として認識できるように定義
DRAW_SPHERE()という関数を、GetWorldの結果がtrueのとき、指定の関数を呼び出せるようにした形。
マクロのセミコロン
code:cpp
# Slash.h
#define DRAW_SPHERE(Location) ... ; #define DRAW_LINE(StartLocation, EndLocation) ... ; # 呼び出し側.cpp
void AItem::BeginPlay()
{
DRAW_SPHERE(Location)
DRAW_LINE(Location, Location + Forward * 100.f)
// ...
こんな感じで呼べるが、SPHERE, LINEで改行するとセミコロンがないので、IDEが自動でインデントで調整しようとする。なので、マクロであってもセミコロンをつけて管理しておくのが無難。
C++の変数初期化
code:Item.h
class SLASH_API AItem : public AActor
{
public:
protected:
private:
float Amplitude = .25f; // 振幅
↑こんな感じで定義時に書く方法
code:Item.cpp
AItem::AItem() : Ampliture(0.25f)
{
PrimaryActorTick.bCanEverTick = true; // 毎フレーム行う処理がないなら、falseにすればTickが動かなくなる。
}
このように渡す方法
code:Item.cpp
AItem::AItem()
{
PrimaryActorTick.bCanEverTick = true; // 毎フレーム行う処理がないなら、falseにすればTickが動かなくなる。
Ampplitude = 0.25f;
}
コンストラクタで渡す方法がある。
テンプレート関数
型を抽象化した関数。テンプレートクラスというのもある。
code:Item.h
protected:
// ...
template<typename T>
T Avg(T First, T Second);
private:
// ...
};
template<typename T>
inline T AItem::Avg(T First, T Second)
{
return (First + Second) / 2;
}
inline
この関数を呼び出す箇所には、関数の処理の中身を直接展開してくれということを示す。通常の関数は以下の手順。
処理の一時停止
引数の受け渡し
別のメモリ場所(関数先)にジャンプ
結果を受け取り、元の場所に戻る
今回のように簡易的なメソッドだと、中身の計算より↑の処理のほうが手間になるケースがある。なのでinlineを付与すると、関数内のメソッドがそのまま展開されてコストが軽くなることが見込める。
T
引数として型を受け取り、その型でreturnするという形
code:Item.cpp
void AItem::BeginPlay()
{
int32 AvgInt = Avg<int32>(1, 3);
float AvgFloat = Avg<float>(3.45f, 7.86f);
FVector AvgVector = Avg<FVector>(GetActorLocation(), FVector::ZeroVector);
}
typenameであるTにint32やfloatなどの型を格納する形。
前方宣言(Forward Declaration)
例えば以下の形のように、UCapsuleComponent* Capsule;を宣言したい。ただし、これには指定のincludeファイルを読み込む必要があるので下記のようにした。
code:Bird.h
#include "Components/CapsuleComponent.h" private:
UPROPERTY(VisibleAnywhere)
UCapsuleComponent* Capsule;
code:Bird.cpp
ABird::ABird()
{
Capsule = CreateDefaultSubobject <UCapsuleComponent>(TEXT("Capsule"));
この場合、Bird.hでincludeしているが、「Bird.hをincludeしたいが、CapsuleComponentが不要なクラス」があったらどうか。Bird.hをincludeしたとき、不要なCapsuleComponent.hの要素まで取り組むことになる。これが更に重なると、雪だるま式に積まれていってしまう。
そこで、.cpp側でincludeするようにする。そうすれば.cppを読み込まない限りはこの現象は発生しない。だが.h側ではUCapsuleComponent* Capsule;とするためにincludeしなければコンパイルエラーが発生する。それを防ぐのが前方宣言。
code:Bird.h
class UCapsuleComponent; // 前方宣言
class SLASH_API ABird : public APawn
{
// ...
private:
UCapsuleComponent* Capsule;
// class UCapsuleComponent* Capsule; ともできる
code:Bird.cpp
#include "Components/CapsuleComponent.h" ABird::ABird()
{
Capsule = CreateDefaultSubobject <UCapsuleComponent>(TEXT("Capsule"));
// ...
前方宣言が出来ないケース
code:cpp
class Player : public IDamageable
というように親クラス / 仮想クラスなどから継承させるときは、前方宣言で済ませることはできない。クラス警鐘を行う場合は親の詳細な情報が必要となるため、#includeで中身を全て読み込ませる必要がある。
シグネチャ
C++というよりも、プログラム全般で使われる共通概念。
関数を識別するためのプロフィールのこと。
関数名、引数の数、データ型、引数の順番などを総じてシグネチャと呼ぶ。
forと参照
code:cpp
TArray<FOverlapResult> OverlapResults;
...
for (const FOverlapResult& Hit : OverlapResults)
正しくパフォーマンス出すためにやること
&を付けずに書くと、配列の中身を取り出すたびにデータのコピー処理が発生する。そのため参照として渡してコピー処理を省く。
書き換えない処理(ROだけで行う処理)なら、const を付属させて書き換えないという安全性を保証させる。
アクセス修飾子
Enum
todo vsのデバッグを使う。