【C++】第一回 C++を使ったゲーム開発 〜C++基礎知識(型、ポインタ、メモリ管理)編〜

スポンサーリンク
PC創作
マイケル
マイケル
みなさんこんばんは!
マイケルです!
エレキベア
エレキベア
クマ〜〜〜
マイケル
マイケル
今日から新しく C++を使ったゲーム開発 企画を始めるよ!
エレキベア
エレキベア
C++クマか〜〜
何か難しそうなイメージがあるクマ〜〜
マイケル
マイケル
今回は手をつけてみたいけど難しそうという人達に向けて、
C++を使うきっかけとなるよう書いていこうと思うよ!
対象読者
  • 普段JavaやC#といった高水準言語を使用している人
  • ゲームエンジンを使った開発を行ったことがあり、より深い知識を得たい人
マイケル
マイケル
というわけで、プログラミング言語が触ったことがあり、
オブジェクト指向もある程度は理解している
という前提で書いていきます!
今回もそれらの言語と比較した場合のC++の特徴や注意点 という観点での説明となるのでご容赦ください。
エレキベア
エレキベア
オブジェクト指向ならカンペキクマ〜〜〜
マイケル

マイケル
また、今後の予定としては下記を計画しています!

 第一回 C++の基礎知識
 第二回 SDLを使った2Dゲーム開発
 第三回 OpenGLを使った3Dゲーム開発

今回は第一回ということで C++の基礎知識編 です!
エレキベア
エレキベア
やったるクマ・・・!!
スポンサーリンク

参考書籍

マイケル
マイケル
今回参考にした書籍は下記になります!

ゲームプログラマになる前に覚えておきたい技術

ゲームプログラミングC++

エレキベア
エレキベア
C++の本じゃないクマね
マイケル
マイケル
どちらもゲームプログラミングに関する本だけど、
C++の特徴の部分はざっと書いてあるので、触ったことがない人にもおすすめです!
マイケル
マイケル
何よりゲームプログラミングに関して幅広く触れられているので、
ゲームエンジンでの開発しかやったことがなければ確実に力がつく本だと思います・・・!
エレキベア
エレキベア
読み終わったら貸してくれクマ・・・
スポンサーリンク

使用するIDE

マイケル
マイケル
まずC++を動作させる環境についてですが、使用するIDEは主に
VisualStudio、VisualStudioCode、CLionの3択になると思います。
Mac版のVisualStudioはC++はサポートしていないようなのでご注意ください!
IDE Windows Mac
VisualStudio ×
VisualStudioCode
CLion(有償)

↑各IDEでのC++サポート(2021年8月現在)

マイケル
マイケル
CLionは有償というのもあるので、VSCodeに「C/C++」「CodeRunnner」等の拡張機能を入れて使うのが一番手っ取り早そうですね。
エレキベア
エレキベア
VSCodeは便利クマ〜〜〜

スポンサーリンク

C++の基礎知識

マイケル
マイケル
それではC++を触っていきましょう!

マイケル
マイケル
まずは使用できる型についていくつか見ていきます!
エレキベア
エレキベア
型なんて余裕クマ〜〜
数値
マイケル
マイケル
数値型について、int、floatなどが使えるのはC#やJavaと同じですが、
C/C++では一部の型にunsignedを指定することができます。
// 整数型
// *unsignedを付けると正の範囲だけとなる
// *ビット演算、添え字、不変な数値等に使用する
int num1;            // -2,147,483,648 ~ 2,147,483,647
unsigned int num2;   // 0 ~ 4,294,967,295

// 浮動小数点型
float num3;  // 3.4E +/- 38 (7 桁)
double num4; // 1.7E +/- 308 (15 桁)
↑int型にunsigned指定が可能
マイケル
マイケル
unsignedを指定すると、正の値の範囲のみとなります。
しかし、0以下になると最大値に巻き戻る現象が起きてしまうので、unsignedを付与するのはビット演算や不変な値の場合のみとした方がよいです。
エレキベア
エレキベア
正の値だけの表現にすることで表現できる範囲も広がっているクマね
マイケル
マイケル
またintの他、char型にもunsignedを指定することができます。
char型は文字を格納しますが、実質は1バイトの整数型です。
こちらもunsignedを付与するのはビット演算や不変な数値として扱いたい場合のみとした方がよいです。
// 文字を表す整数型
// *文字として扱う場合はchar
// *1バイト幅であるのが強み
// *ビット演算や数値として扱う場合はunsigned
char c1;          // -128 ~ 127
unsigned char c2; // 0 ~ 255
↑char型にもunsigned指定が可能
文字列
マイケル
マイケル
次に文字列の扱いについてですが、C言語ではstringが無く、char型の配列として表現することしかできませんでしたが、C++では標準ライブラリとしてstringが追加されました。
文字列の長さが決まっているか固定の場合はchar配列を使ってもよいですが、可変な場合はstringを使用した方がよいと思います。
// 文字列
// *stringはC++標準ライブラリ
// *可変な場合はstringを使った方が楽
char str1[] = "テスト";
std::string str2 = "テスト";
エレキベア
エレキベア
stringは元々無かったのクマね
真偽値
マイケル
マイケル
真偽値に関してはC#やJavaとも変わりなくtrue、falseのみですね。
// フラグ
bool flag; // true or false
マイケル
マイケル
以上、よく使用する型について解説しましたが、その他の型については
下記の公式ドキュメントを参照してください!

データ型の範囲|Microsoft Docs

エレキベア
エレキベア
大きな違いはunsignedクマね

参照とポインタ

マイケル
マイケル
次に参照とポインタについて解説します!
よくポインタは難しいと聞くかもしれませんが、参照型やメモリ構造を理解していればそれほど難しい話ではありません。
エレキベア
エレキベア
ポインタ怖いクマ・・・
参照
マイケル
マイケル
参照は既に存在する変数を参照する変数のことです。
型の後ろに&を付けて代入することでその変数の参照を示すようになります。
    // aの参照を代入
    int a = 10;
    int& b = a;
    std::cout << a << std::endl; // 10
    std::cout << b << std::endl; // 10

    // aを変えるとbも変わる
    a = 20;
    std::cout << a << std::endl; // 20
    std::cout << b << std::endl; // 20
↑参照の挙動
マイケル
マイケル
上記の例では、aとbどちらも同じメモリアドレスを参照しているため、aの値が変わるとbの値も変わるといった挙動になります。
ScreenShot 2021 08 12 14 05 16
エレキベア
エレキベア
これはC#やJavaでも同じクマね
ポインタ
マイケル
マイケル
そしてポインタもメモリアドレスを参照する点では同じですが、ポインタ変数自体はアドレスを示しています
そのため、そのまま出力すると「0x7ffee93d07cc」といったアドレスが出力されます。
マイケル
マイケル
ポインタ変数は型の後ろに*を付与して宣言し、変数の前に&を付けてアドレスを返して代入します。
そしてアドレスでなく値にアクセスする際には変数の前に*を付与します。
    // 変数aのアドレスをポインタに設定
    // &を付けるとアドレスを返す
    int a = 10;
    int* p = &a;
    std::cout << p << std::endl;  // アドレスが出力される(0x7ffee93d07ccなど)
    std::cout << *p << std::endl; // 10

    // *をつけると値にアクセスできる
    *p = 20;
    std::cout << *p << std::endl; // 20

    // ポインタの初期化はnullptr(何も指し示さない状態)
    p = nullptr;
↑ポインタの挙動
エレキベア
エレキベア
*や&が少しややこしいクマ・・・
マイケル
マイケル
最初は混乱するかもしれないけど、そういうルールだと思って覚えておこう!
マイケル
マイケル
更に参照と異なる点として、ポインタは参照するアドレスを移動できるという点があります。
ポインタ変数をインクリメント(+1)することで参照先を移動することができ、メモリアドレスを連続で確保する配列のループとしてよく使われます。
    // 配列を作成
    int a[5];
    a[0] = 10;
    a[1] = 20;
    a[2] = 30;

    // 配列[0]のアドレスをポインタに設定
    int* p = &a[0];
    std::cout << *p << std::endl; // 10

    // インクリメントで次のアドレスを参照できる
    // 配列の場合、連続したアドレスを確保しているため
    // [0] -> [1] -> [2] ... となる
    ++p;
    std::cout << *p << std::endl; // 20
    ++p;
    std::cout << *p << std::endl; // 30
↑参照するアドレスの変更
ScreenShot 2021 08 12 14 19 10
エレキベア
エレキベア
より柔軟な操作ができる参照みたいなイメージクマね
引数としての利用
マイケル
マイケル
参照とポインタは関数の引数としても利用することができます。
例えば次のスワップ関数を見ていきましょう!
// スワップ関数(NG)
// 値渡しのため元の変数が変わらない
void SwapNG(int a, int b)
{
    int temp = a;
    a = b;
    b = temp;
}
↑値渡しのNG例
マイケル
マイケル
上記の例ではa,bを値渡ししているため呼び出し元の変数は変わりません。
この引数を参照型やポインタ型にすることで、呼び出し元の変数にも反映させることができます。
// スワップ関数
// (参照で渡した例)
void Swap1(int& a, int& b)
{
    int temp = a;
    a = b;
    b = temp;
}

// スワップ関数
// (ポインタで渡した例)
void Swap2(int* a, int* b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {

    int a = 10;
    int b = 20;
    // 参照で渡した場合
    Swap1(a, b);
    // ポインタで渡した場合
    // &が付くため値が変わることが予測しやすい
    Swap2(&a, &b);

    return 0;
}
↑参照渡し、ポインタ渡しの例
マイケル
マイケル
これはどちらも同じ挙動となりますが、ポインタ変数で渡すと呼び出し元にも「&」を付けないといけないため、「変数が変わること」を予測しやすくなります。
エレキベア
エレキベア
呼び出し側からも変わるのが分かった方が間違いが起こりにくそうクマね
マイケル
マイケル
また、オブジェクトを値渡ししてしまうとその度にコピーが発生してしまうため効率的にはよくないです。
そのため、オブジェクトは参照かポインタで渡すのが基本となりますが、不変な場合にはconstを付けることで値が変更されることを防ぐことができます!
// メッセージ出力
// *オブジェクトはコピーが発生しない参照orポインタ渡しの方が効率がよい
// *書き換えを行わない場合はconstを付けることで不変であることを保証できる
void PrintMessage(const std::string& message)
{
    std::cout << message << std::endl;
}

int main() {
    PrintMessage("こんにゃく"); // 呼び出し側も変わらないと思っている
    return 0;
}
↑引数にconstを付けることで値の変更を防ぐかつ効率よくする
マイケル
マイケル
以上のことから、
・可変なオブジェクトを渡す場合はポインタ渡し
・不変なオブジェクトを渡す場合にはconst参照渡し
というルールを設定することで可読性がよく効率もよい記述ができます!
エレキベア
エレキベア
これで参照とポインタはマスタークマ〜〜〜

メモリ管理

メモリの動的確保
マイケル
マイケル
そして次はメモリ管理についてです!
下記はint型の値と配列を宣言する処理ですが、通常はスタックメモリと呼ばれる領域に確保されます。
    // メモリの静的確保
    // *コンパイル時にスタック領域にメモリが確保される
    // *スコープを抜けると破棄される
    int num1 = 10;
    int num1_array[] = {10, 20, 30};
↑メモリの静的確保
マイケル
マイケル
このような確保を静的確保といいますが、
・スタックメモリは利用できる量が少ない
・スコープを抜けるとメモリが破棄されるため存続期間が限られる
といったデメリットがあります。
マイケル
マイケル
そこで下記のようにnewを使用して宣言することで、ヒープメモリという別領域に動的確保することができます。
    // メモリの動的確保(new/delete)
    // *プログラム実行中にヒープ領域にメモリが確保される
    // *確保したメモリのアドレスをポインタとして受け取る
    // *自身でメモリ破棄のタイミングを指定できる
    int* num2 = new int(10);
    int* num2_array = new int[3];
    num2_array[0] = 10;
    num2_array[1] = 20;
    num2_array[2] = 30;
    // 自身でメモリを破棄する
    delete num2;
    delete[] num2_array; // []を付けること!
↑メモリの動的確保
マイケル
マイケル
動的確保したらポインタが返され、好きなタイミングでメモリを開放(delete)することができます。
逆に言えばメモリ管理の責任を負うということで、メモリを確保したら開放処理も書かなければなりません。
エレキベア
エレキベア
開放を忘れてしまったら恐ろしいクマ・・・
マイケル
マイケル
また、配列のメモリ開放時はdelete[]を使用する必要がある
ことには気をつけましょう!
補足:メモリアロケータ
マイケル
マイケル
メモリ管理について補足ですが、確保と開放を繰り返しているとメモリの断片化が発生することがあります。
マイケル
マイケル
断片化とは下記のように、メモリの空領域が散りばることで空容量はあるのに確保できないという状態になることです。
ScreenShot 2021 08 11 22 34 01
↑メモリの断片化
エレキベア
エレキベア
メモリがぐちゃぐちゃになってるクマね・・・
マイケル
マイケル
そこでメモリを管理するクラス(メモリアロケータ)を自作して対処する方法があります。
マイケル
マイケル
実装方法については割愛しますが、調べたら分かりやすい解説記事がいくつかあったので、下記にいくつか紹介しておきます!
興味がある方は勉強してみてください!


[自作メモリアロケータの例]
・Stack Allocator
 先頭ブロックから順にメモリを取得するが、開放は全て開放かある状態に戻すしかできない。
 高速かつ無駄なメモリを使用しないが、自由に開放できない欠点もある。
 参考:プログラミング備忘録 スタックアロケータ

・Pool Allocator
 8、16、32、64、128byteといったそれぞれのプール領域を作成し、確保するサイズに応じて割り当てる方法。
 無駄が少なく実装も比較的シンプル。
 参考:C++の簡単なメモリアロケータ実装 – Qiita

・Linked List Allocator
 開放時に空ブロックをリンクリストで繋げる方法。
 参考:Linked List Allocator – Qiita

エレキベア
エレキベア
いろんなやり方があって面白いクマね〜〜

クラス定義

マイケル
マイケル
次はクラス定義について!
オブジェクト指向の言語を触ったことがあれば基本は同じです。
基本的な書き方
マイケル
マイケル
C++では、ヘッダファイルに変数や関数の宣言を書いてcppファイルに処理を書く、というように分けるのが一般的です。
下記はいくつかの変数と関数を定義したシンプルなクラスの例です。
#pragma once

// エレキベアクラス
class ElekiBear {
public:
    ElekiBear(int hp, char* name = "エレキベア", bool is_dead = false); // デフォルト値も指定可
    ~ElekiBear();
    const char* GetVoice();
    void Say();
private:
    int m_hp;
    char* m_name;
    bool m_is_dead;
};
#include <iostream>
#include "ElekiBear.h"

// コンストラクタ
ElekiBear::ElekiBear(int hp, char* name, bool is_dead) :
m_hp(hp),
m_name(nullptr),
m_is_dead(is_dead)
{
    m_name = new char[strlen(name) + 1];
    strcpy(m_name, name);
}

// デストラクタ
ElekiBear::~ElekiBear()
{
    delete[] m_name; // 動的なオブジェクトは破棄する
}

// 鳴き声を返す
const char* ElekiBear::GetVoice()
{
    return "クマ〜〜〜";
}

// 喋らせる
void ElekiBear::Say()
{
    std::cout << m_name << ":" << GetVoice() << std::endl;
}
#include <iostream>
#include "ElekiBear.h"

int main() {
    // エレキベアインスタンス生成
    ElekiBear* bear = new ElekiBear(100);
    bear->Say(); // エレキベア:クマ〜〜〜
    // 削除も忘れずに!
    delete bear;

    return 0;
}
マイケル
マイケル
書き方を覚えれば基本はJavaやC#と同じように書けるかと思います!
エレキベア
エレキベア
余裕クマ〜〜〜
継承
マイケル
マイケル
継承も基本的にJavaやC#と同じような実装になります。
例えば今回ElekiBearクラスの他にGoroyanクラスを作成しようとした場合、各オブジェクトの共通的な処理を管理するクラスが欲しくなると思います。
マイケル
マイケル
そこで下記のように親クラスとしてMonsterクラスを定義することで、ElekiBearクラスとGoroyanクラスには固有の処理のみ持たせることができます。
ScreenShot 2021 08 12 23 46 03
↑親クラスを定義
エレキベア
エレキベア
(モンスター扱いされたクマ・・・)
ゴロヤン
ゴロヤン
ゴロ・・・
マイケル
マイケル
C++ではこれを下記のように実装します!
まずMonsterクラスには共通処理を全て定義して、固有の処理を定義してほしいGetVoice関数にはvirtualを指定するようにします。
#pragma once

// モンスター基底クラス
class Monster {
public:
    Monster(int hp, char* name = "NONAME", bool is_dead = false);
    virtual ~Monster();
    virtual const char* GetVoice();
    void Say();
private:
    int m_hp;
    char* m_name;
    bool m_is_dead;
};
#include <iostream>
#include "Monster.h"

// コンストラクタ
Monster::Monster(int hp, char* name, bool is_dead) :
m_hp(hp),
m_name(nullptr),
m_is_dead(is_dead)
{
    m_name = new char[strlen(name) + 1];
    strcpy(m_name, name);
}

// デストラクタ
Monster::~Monster()
{
    delete[] m_name; // 動的なオブジェクトは破棄する
}

const char *Monster::GetVoice()
{
    return "";
}

// 喋らせる
void Monster::Say()
{
    std::cout << m_name << ":" << GetVoice() << std::endl;
}
マイケル
マイケル
この時、子クラスを破棄した時にデストラクタが確実に呼ばれるよう、デストラクタにもvirtual指定するようにしてください。
マイケル
マイケル
そしてElekiBearクラスとGoroyanクラスは下記の通り!
コンストラクタ、デストラクタの他、親クラスの上書きしたい処理を定義(オーバーライド)しましょう!
#pragma once
#include "Monster.h"

// エレキベアクラス
// (Monsterクラスを継承)
class ElekiBear : public Monster {
public:
    ElekiBear(int hp, char* name = "エレキベア", bool is_dead = false);
    ~ElekiBear();
    const char* GetVoice() override;
};
#include "ElekiBear.h"

// コンストラクタ
// 親クラスのコンストラクタを指定
ElekiBear::ElekiBear(int hp, char* name, bool is_dead) :
Monster(hp, name, is_dead)
{
}

// デストラクタ
// 親クラスのデストラクタも呼ばれる
ElekiBear::~ElekiBear()
{
}

// GetVoiceメソッドをオーバーライド
// 鳴き声を返す
const char* ElekiBear::GetVoice()
{
    return "クマ〜〜〜";
}
#pragma once
#include "Monster.h"

// ゴロヤンクラス
// (Monsterクラスを継承)
class Goroyan : public Monster {
public:
    Goroyan(int hp, char* name = "ゴロヤン", bool is_dead = false);
    ~Goroyan();
    const char* GetVoice() override;
};
#include "Goroyan.h"

// コンストラクタ
// 親クラスのコンストラクタを指定
Goroyan::Goroyan(int hp, char* name, bool is_dead) :
Monster(hp, name, is_dead)
{
}

// デストラクタ
// 親クラスのデストラクタも呼ばれる
Goroyan::~Goroyan()
{
}

// GetVoiceメソッドをオーバーライド
// 鳴き声を返す
const char* Goroyan::GetVoice()
{
    return "ゴロ〜〜〜";
}
マイケル
マイケル
このように書くことで、子オブジェクトそれぞれの鳴き声で喋るようになったはずです!
#include <iostream>
#include "ElekiBear.h"
#include "Goroyan.h"

int main() {

    ElekiBear* bear = new ElekiBear(100);
    bear->Say(); // エレキベア:クマ〜〜〜
    delete bear;

    Goroyan* goroyan = new Goroyan(300);
    goroyan->Say(); // ゴロヤン:ゴロ〜〜〜
    delete goroyan;

    return 0;
}
↑それぞれ固有の処理になっている
エレキベア
エレキベア
モンスター扱いは納得できないクマ
インクルードガード
マイケル
マイケル
気になっていた方もいるかもしれませんが、ヘッダークラスの頭には「#pragma once」という指定をしていました。
エレキベア
エレキベア
この呪文は何なのクマ?
マイケル
マイケル
これはインクルードガードといって、インクルードされた時に何回も展開されるのを防ぐための指定のことです!
これを付けないと重複でインクルードされてエラーになってしまいます。
エレキベア
エレキベア
そんな罠があったクマね・・・
マイケル
マイケル
インクルードガードには、今回紹介した「#pragma once」指定の他にも「#ifndef」指定をする方法もあります。
#pragma once

// エレキベアクラス
class ElekiBear {
public:
    ElekiBear(int hp, char* name = "エレキベア", bool is_dead = false); // デフォルト値も指定可
    ~ElekiBear();
    const char* GetVoice();
    void Say();
private:
    int m_hp;
    char* m_name;
    bool m_is_dead;
};
↑pragma onceでの指定
#ifndef CPPSTANDARD_ELEKIBEAR_H
#define CPPSTANDARD_ELEKIBEAR_H

class ElekiBear {
public:
    ElekiBear(int hp, char* name = "エレキベア", bool is_dead = false); // デフォルト値も指定可
    ~ElekiBear();
    const char* GetVoice();
    void Say();
private:
    int m_hp;
    char* m_name;
    bool m_is_dead;
};

#endif //CPPSTANDARD_ELEKIBEAR_H
↑ifndefでの指定
マイケル
マイケル
「#pragma once」の方が新しく行数も少ないため、こちらを使用で問題ないかと思います!
エレキベア
エレキベア
ヘッダファイルには忘れず付けるクマ
演算子のオーバーロード
マイケル
マイケル
またC++特有の使用として、演算子のオーバーロードがあるので紹介しておきます!
下記のように「operator【演算子】」で関数を定義することで、演算子を使った固有の処理を指定することができます!
#pragma once

// 2次元ベクトルクラス
class Vector2 {
public:
    float x;
    float y;

    Vector2();
    explicit Vector2(float inX, float inY);

    void operator*=(float scalar);
    void operator+=(const Vector2& a);
    void operator-=(const Vector2& a);
};
#include "Vector2.h"

Vector2::Vector2()
:x(0.0f)
,y(0.0f)
{}

Vector2::Vector2(float inX, float inY)
:x(inX)
,y(inY)
{}

// Scalar *=
void Vector2::operator*=(float scalar)
{
    x *= scalar;
    y *= scalar;
}

// Vector +=
void Vector2::operator+=(const Vector2& a)
{
    x += a.x;
    y += a.y;
}

// Vector -=
void Vector2::operator-=(const Vector2& a)
{
    x -= a.x;
    y -= a.y;
}
マイケル
マイケル
これはベクトルクラスの例になりますが、このように記述することで直接演算子で計算できるようになります!
#include <iostream>
#include "Vector2.h"

int main() {

    // ベクトル定義
    Vector2 vec1(2.0f, 3.0f);
    Vector2 vec2(3.0f, 2.0f);
    std::cout << vec1.x << ", " << vec1.y << std::endl; // 2, 3
    // ベクトルを加算
    vec1 += vec2;
    std::cout << vec1.x << ", " << vec1.y << std::endl; // 5, 5

    return 0;
}
↑直接計算できるようになる
エレキベア
エレキベア
これは便利クマ〜〜〜

補足:STL

マイケル
マイケル
最後に補足になりますが、これまで度々出てきたstd::coutやstd::stringといったものは、STL(Standard Template Library)というC++ライブラリの一部分になります。
下記リファレンスに他の機能も載っているので目を通して見てください!

C++ 標準ライブラリ

マイケル
マイケル
std::vectorは動的配列クラスとして便利で次回も使うと思うので、一度使い方を調べて見るとよいと思います。
(JavaでいうArrayListみたいなイメージですが挙動は違う部分があるので注意!)
エレキベア
エレキベア
いろいろ用意されてて楽しいクマね
スポンサーリンク

おわりに

マイケル
マイケル
というわけで今回はC++の基礎知識についてでした!
どうだったかな?
エレキベア
エレキベア
C#やJavaと比べるとやっぱりメモリ周りの考慮が必要になるクマね
マイケル
マイケル
低レベルな処理まで行うのは大変だけど、
その分無駄な処理を省いて効率化するのが醍醐味になりそうだね。
マイケル
マイケル
それでは次はC++を使って簡単な2Dゲームを作っていくよ!
お楽しみに〜〜!!
エレキベア
エレキベア
クマ〜〜〜〜

【C++】第一回 C++を使ったゲーム開発 〜C++基礎知識(型、ポインタ、メモリ管理)編〜 〜完〜

 ※続きはこちら!

コメント