C++OpenSiv3D入門講座 Vol. 04 参照・クラスのポインタ

C++OpenSiv3D入門講座

オブジェクトから他のオブジェクトの情報を得るための参照・ポインタについて学び、OpenSiv3Dの実例ではEnemyからPlayerの情報を参照する。

bool型

C++には真偽を表すためだけの型、bool型が存在する。

#include <iostream>

int main(){

    bool b = true;

    if (b){
        std::cout << "条件文は真" << std::endl;
    }
    else{
        std::cout << "条件文は偽" << std::endl;
    }

    return 0;
}

以下のように、条件を満たすかどうかの真・偽を返す関数の返り値の型としてbool型を使うことが多い。

#include <iostream>

// 偶数だったらtrue,そうでなければfalseを返す関数
bool isEven(int num){
    return (num % 2) == 0;
}

int main(){

    if (isEven(10)){
        std::cout << "条件文は真" << std::endl;
    }
    else{
        std::cout << "条件文は偽" << std::endl;
    }

    return 0;
}   

スコープ

{} で囲まれた部分を抜けると、その{}内部で宣言された変数は破棄されるので、アクセスできなくなる。 変数や関数の「見える」範囲をスコープと言い、変数の寿命はスコープによって決まる。

#include <iostream>
void func(){
int a = 10;
std::cout << "aの値は " << a << std::endl;
}
int main() {
    func();
    //コメントを外すとコンパイルエラー
    //a = 100;  
    if (true) {
        int b = 20;
        std::cout << "bの値は " << b << std::endl;
    }
    //コメントを外すとコンパイルエラー
    //b = 100; 
    for (int i = 0; i < 3; i++) {
        std::cout << "iの値は " << i << std::endl;
    }
    //コメントを外すとコンパイルエラー
    //i = 100; 
    return 0;
}

{}だけでもスコープを作ることが出来る

#include <iostream>

int main(){
    {
        int b = 20;
        std::cout << "bの値は " << b << std::endl;
    }

    //コメントを外すとコンパイルエラー
    //b = 100; 

    return 0;
}

参照

C++ではポインタを扱いやすくした参照(リファレンス)というものが存在する。変数に別名を付けて参照することが可能となる機能である。

データ型& 名前
例:int& a

以下の例では、参照型のaliasでhogeを参照し、aliasからhogeの値を変えている。

#include <iostream>

int main(){

    int hoge = 2;

    //参照型のaliasで変数hogeを参照する(aliasを操作するとhogeの値が変わる)
    int& alias = hoge;

    std::cout << hoge << ", " << alias << std::endl;

    alias=4;
    std::cout << hoge << ", " << alias << std::endl;

    return 0;
}

ポインタ型と参照型

関数の中で引数で渡したものを変更するためにポインタを用いる事があると思う。 以下はその例である。

ポインタを用いたaとbを入れ替える関数

#include <iostream>
//aとbを入れ替える関数 
void swap(int *a, int *b){
    int tmp=*a;
    *a=*b;
    *b=tmp;
    return;
}

int main(){

    int a=3;
    int b=5;

    std::cout << a << "," << b << std::endl;

    swap(&a, &b);//aとbのポインタを渡さなければいけない

    std::cout << a << "," << b << std::endl;

    return 0;
}  


aとbを入れ替える関数のよくある間違い

#include <iostream>
//aとbを入れ替える関数 関数内でa, bを書き換えても元のa, bは書き換わらない
void swap(int a, int b){
    int tmp=a;
    a=b;
    b=tmp;
    return;
}

int main(){

    int a=3;
    int b=5;

    std::cout << a << "," << b << std::endl;

    swap(a, b);

    std::cout << a << "," << b << std::endl;

    return 0;
}


以下は参照を用いたaとbを入れ替える関数である。正しくaとbの値が入れ替わっており、かつポインタを用いた例より読みやすく書けていることがわかる。

#include <iostream>
//aとbを入れ替える関数
void swap(int& a, int& b) {
    int tmp = a;
    a = b;
    b = tmp;
    return;
}

int main() {

    int a = 3;
    int b = 5;

    std::cout << a << "," << b << std::endl;

    swap(a, b);

    std::cout << a << "," << b << std::endl;

    return 0;
}

ポインタを使っても同じことが出来るが、*&をつけるのに手間がかかり、ミスをしてバグを起こしやすいので、これからは参照を使おう。

値渡し・ポインタ渡し・参照渡し

void swap(int *a, int *b)のように、ポインタで引数を渡すことを「ポインタ渡し」という。
void swap(int& a, int& b)のように、参照で引数を渡すことを「参照渡し」という。
上記2つに対し、void swap(int a, int b)のように引数を渡すことを「値渡し」という。

値渡し

値渡しを行うと、コピーが行われる。例えば、int型が引数であれば、その数値がコピーされて関数の中で使われる。コピーされた引数を関数の中で変更しても元の変数は書き換えられない。

クラス等、サイズの大きな型は値渡しにするべきではない。コピーに時間がかかったり、オブジェクトの再構築で予期せぬ動作をする可能性がある。

ポインタ渡し・参照渡し

ポインタ渡しや参照渡しは関数の呼び出し元の実体を扱う。そのため、呼び出し元の変数を書き換えることができる。

また、オブジェクトのコピーが発生することを防ぐことができ、クラス等を渡す時に向いている。

const参照渡し

参照渡しを使うと、コピーが発生することを防ぐことができ、無駄がなくなる。ただし、参照渡しをすると呼び出し元の変数を書き換えることが出来てしまう。 呼び出し元の変数を書き換えてしまうのを防ぎたい時には、引数にconstを付けることで、関数内で値を変更できなくなる。 以下は、const参照渡しを使う関数の例である。本資料では以降クラスを引数に取る時、適宜参照渡し、const参照渡しを使っていく。

#include <iostream>

class Color {
public:
    int r, g, b;
    Color(int _r, int _g, int _b) :
        r{ _r },
        g{ _g },
        b{ _b }
    {
    }
};

void showColor(const Color& color) {
    std::cout << "r:" << color.r << " g:" << color.g << " b:" << color.b << std::endl;
    // コメントを外すと値を書き換えることになるのでコンパイルエラー
    // color.r = 0;
}

int main() {

    Color color{ 150, 100, 50 };

    showColor(color);

    return 0;
}

オブジェクトへのポインタ

オブジェクトを指すポインタからオブジェクトの要素にアクセスしたい時がある。下の例では、オブジェクトを指すポインタ、ptrから、objのxにアクセスしたい。(*ptr).xと書けばobjのxにアクセスできるが、少々書きづらいので、ptr->xと書くことができるようになっている。

(*ptr).xptr->x は同じ意味である。-> をアロー演算子という。

#include <iostream>

class Vector2{
public:
    int x, y;

    Vector2(int _x, int _y)
    {
        x = _x;
        y = _y;
    }
};

int main(){

    Vector2 obj{ 12, 34 };
    Vector2 *ptr = &obj;

    //普通に表示
    std::cout << obj.x << " " << obj.y << std::endl;

    std::cout << (*ptr).x << " " << (*ptr).y << std::endl;

    std::cout << ptr->x << " " << ptr->y << std::endl;


    //↓このようには書けない。 *(ptr.x)と解釈されるから
    //std::cout << *ptr.x << " " << *ptr.y << std::endl;


    return 0;
}

ちなみに、*ptr.xと書くと*(ptr.x)と解釈され、ptrのx(存在しない)が指すものを表す。

まとめ

  • bool型は真偽(true, false)を表す型である。
  • 変数や関数の見える範囲({}で囲まれた範囲)をスコープと言い、変数の寿命はスコープによって決まる
  • C++ではポインタを扱いやすくした参照(リファレンス)というものが存在する
  • クラス等を関数の引数にする時は参照渡しをしよう
  • クラス等を参照渡しする際、値を変更してほしくない時はconst参照渡しをしよう
  • オブジェクトを指すポインタからオブジェクトの要素にアクセスするときはアロー演算子->を利用できる

演習問題(コンソール)

  1. 参照でint型を受け取り2倍にする関数を作れ。

演習問題(OpenSiv3D)

今回は、これまでコードを書いてきたプロジェクトとは別に、プロジェクトを作ることを推奨します。

  1. 以下の様なPlayerクラスとEnemyクラスを用意した。PlayerクラスとEnemyクラスのインスタンスを作り、動作を確認せよ。
    #pragma once
    #include <Siv3D.hpp>
    
    class Player {
    public:
        const double Speed;
        Vec2 pos;
        Player();
        void update();
        void draw();
    };
    
    #include "Player.h"
    
    Player::Player() :
        pos{ 320.0, 240.0 },
        Speed{ 50.0 }
    {
    }
    
    void Player::update() {
        //このフレームで進む距離の計算
        const double delta = Scene::DeltaTime() * Speed;
    
        //上下左右キーで移動
        if (KeyLeft.pressed()) {
            pos.x -= delta;
        }
        if (KeyRight.pressed()) {
            pos.x += delta;
        }
        if (KeyUp.pressed()) {
            pos.y -= delta;
        }
        if (KeyDown.pressed()) {
            pos.y += delta;
        }
    }
    
    void Player::draw() {
        Circle{ pos, 30.0 }.draw(Color{ 0, 0, 255 });
    }
    
    #pragma once
    #include <Siv3D.hpp>
    #include "Player.h"
    
    class Enemy {
    public:
        Vec2 pos;
        Vec2 velocity;
        Enemy(const Vec2& _pos);
        void update();
        void draw();
    };
    
    # include "Enemy.h"
    
    Enemy::Enemy(const Vec2& _pos) :
        pos{ _pos },
        velocity{ 0.0, 0.0 }
    {
    }
    
    void Enemy::update() {
        pos += Scene::DeltaTime() * velocity;
    }
    
    void Enemy::draw() {
        Circle{ pos, 30.0 }.draw(Color{ 255, 0, 0 });
    }
    
  2. EnemyがPlayerの方向に移動するようにしたい。Enemyクラスが「Playerクラスへのポインタ」をメンバに持つようにして、PlayerクラスとEnemyクラスのインスタンスを生成した後にEnemyのインスタンスににPlayerのインスタンスのポインタを渡し、そのポインタからPlayerクラスのx,yにアクセスすることでEnemyがPlayerの位置を取得し、その方向に移動できるようにせよ。
    (今回は、敵の追尾は大まかでも良い。例:PlayerのxがEnemyのxより大きければEnemyは右に、そうでなければ左に動く…など)

ヒント

  • Enemyクラス内でPlayerクラスを扱う為に、Enemy.hからPlayer.hをincludeしている。

  • EnemyクラスにPlayerへのポインタを保存するメンバ変数を追加し、コンストラクタで初期化できるようにする。
#pragma once
#include <Siv3D.hpp>
#include "Player.h"
class Enemy {
public:
    Vec2 pos;
    Vec2 velocity;
    Player* pPlayer; //追加
    Enemy(const Vec2& _pos, Player* ptr); //コンストラクタでpPlayerを初期化できるようにする
    void update();
    void draw();
};
Player player; //Playerをインスタンス化
Enemy enemy{ Vec2{ 480, 0 }, &player }; //Enemyをインスタンス化、playerのポインタを渡す

C++OpenSiv3D入門講座