クラスについて
クラス化の目的⇒情報の隠蔽(クラス内部からしか書き換えできないようにする)
特にグループ開発において、開発者間で「触ってはいけないデータ」を保護する目的。
外からは「参照」のみ。
値を変更する必要がある場合にも「変更用の関数(インターフェース関数)」を準備して、間接的に扱える形にする。(カプセル化)
[1]クラスの宣言/定義
C++では、構造体等もオブジェクト指向の性格を備えているが、クラス型を使ってオブジェクト指向プログラミングを実現するのが典型的な使い方。
①クラス型定義のアクセス属性(可視性)
クラス型定義では、デフォルトでメンバが不可視になり、保護される。
ですので、publicキーワードを指定して、公開しても良いメンバを可視となるように変更する。
(デフォルトではprivate:不可視:他から参照できない)。
現段階では メンバ:変数,関数 という認識で可。
②クラスの書き方
C++のクラストは、ユーザー定義型、の一つ。
a)クラスの宣言
class クラス名{
アクセス指定子:
定数の宣言; ⇒定数宣言等を記述
~~~
関数の宣言; ⇒関数(メンバ関数)を記述
~~~
アクセス指定子:
変数の宣言 ⇒変数等(データメンバ)を記述
};
※このクラスの宣言は、ヘッダーファイル上で行う。
ファイル名=クラス名を徹底する。仕様上は別でも、複数の定義をしても問題ないが、どのファイルにどのクラスが宣言されているか、がわからないと実用上大変困る為。
なお、似た内容のクラスが存在しないか、も重要なので、チーム内で意思疎通する。
ベストなのは、設計時点で確定させておくこと。
b) メンバ関数の定義
上記の宣言では、メンバ関数の宣言のみを行っており、これはプロトタイプ宣言と同じ。
実体は.cpp内に定義する↓
なお、この時のファイル名も、クラス名=ファイル名を徹底する。
戻り値の型 クラス名::メンバ関数名(引数リスト)
{
‥‥
}
※「::」はスコープ解決演算子と呼ぶ。
::演算子を使ってメンバがどの食らうのメンバ関数であるのかを指定する。
「クラス名::メンバ関数名」と記述する方式を「クラススコープ指定」と呼ぶ。
これが、メンバ関数の一般的な定義方法。
これとは別に、インライン関数定義、という方法がある。
●インライン関数の定義
インライン関数定義では、メンバ関数宣言の引数の記述の後に{}を使って関数の定義ができる。
(つまり、ヘッダーファイル内での定義が可能)
「値を返す」などのシンプルな動作をヘッダーファイル内に記述することで、cpp側の簡略化が可能になる。(getter関数、setter関数 など)
特記事項)C++では、本来インライン関数定義の際にはinlineキーワードを使って関数の定義を続けることが可能
externなどで宣言してしまうと、どこからでも変更が許可されてしまい、特にチーム作業等で問題が起きやすい。公開範囲を適切に絞ることが破綻しないポイント。
🧩 メンバ関数の定義方法と用途
C++のクラスにおけるメンバ関数の定義には、主に以下の3つの方法があります:
定義方法 | 概要 | 用途・特徴 |
---|---|---|
クラス内定義 | クラスの宣言内で関数の本体も書く | 自動的にインライン扱いになる。短くて頻繁に呼ばれる関数に向いている。 |
クラス外定義 | クラスの宣言では関数のプロトタイプだけ書き、実装は別に書く | 実装とインターフェースを分離できる。大規模開発やライブラリ設計に向いている。 |
インライン関数(明示的) |
| クラス外でもインライン展開を促す。ヘッダファイルに実装を書く場合に使われる。 |
🔍 それぞれのコード例
① クラス内定義(暗黙的インライン)
class Vector { public: float length() const { return std::sqrt(x * x + y * y + z * z); } private: float x, y, z; };
- 用途:短くて頻繁に呼ばれる関数(getterなど)
- メリット:見通しが良く、ヘッダだけで完結
② クラス外定義(分離型)
class Vector { public: float length() const; private: float x, y, z; }; // 実装部(通常は .cpp ファイル) float Vector::length() const { return std::sqrt(x * x + y * y + z * z); }
- 用途:実装を隠したい、ビルド時間を減らしたい
- メリット:インターフェースと実装の分離、依存の軽減
③ インライン関数(明示的)
inline float Vector::length() const { return std::sqrt(x * x + y * y + z * z); }
- 用途:ヘッダファイルに実装を書く必要があるが、複数翻訳単位で使いたい場合
- 注意点:多用するとコードサイズが膨らむ可能性あり
🧠 佳之さん向けの視点:抽象化と設計哲学
- クラス内定義は「密結合だけど見通しが良い」。
- クラス外定義は「疎結合で拡張性が高い」。
- インライン関数は「パフォーマンスと設計のバランス取り」。
Z80的に言えば、クラス内定義は「ラベル付きマクロ」、外定義は「別セグメントのサブルーチン」、インラインは「条件付きインライン展開」みたいな感覚かもしれません。
(by Copilot)
[2]クラスの利用
クラスの宣言・定義ができたら、クラスを使用する必要がある。
クラスを使用するためには、オブジェクト(またはインスタンスともいう)を作成してゆく。
①実体化したオブジェクト(インスタンス)の作成
宣言したクラスは、新しい型のひとつとなります。
クラス名 インスタンス名; <—- 通常の変数の宣言と同様な記述で
オブジェクト(インスタンス)を作成できる。
(1)メンバへのアクセス方法
ドット演算子(接続子)を使ってアクセスする。
実例)
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstanced, LPSTR lpCmdLine, int nCmdShow)
{
// アプリケーションクラスの実体化したオブジェクトの作成
Application apl;
// 初期化処理
if (apl.SystemInit() == false)return -1;
// ゲームの起動
apl.Run();
// 解放処理
if (apl.Release() == false)return -1;
return 0;
}
②動的なオブジェクト(インスタンス)として作成
new 演算子を使用して作成
オブジェクト(インスタンス)のポインタが戻されるので、
これを確保しておく必要がある
NewClass.h
class NewClass
{
public:
NewClass(void); // コンストラクタ
・・・・・;
bool Init(void);
}
NewClass* ncInst;
// NewClass 型のインスタンスのポインタ
ncInst = new NewClass(); <-- new 演算子の後に作成クラスのコンストラクタを呼ぶ。
作成が成功すれば、ヌル以外のクラスの(オブジェクトの)
先頭アドレスが返される
失敗した場合は、ヌルが返ってくる。
if(ncInst == nullptr)return false; // エラーリターンする。
コンストラクタとデストラクタを作ることは意識しておいた方が無難。
●コンストラクタとデストラクタ
C++のクラス設計における「コンストラクタ」「デストラクタ」「クラスのポインタを返す」といった要素は、メモリ管理とオブジェクトのライフサイクル制御に深く関わっています。Z80的な視点で言えば、これらは「初期化ルーチン」「後処理ルーチン」「間接参照による柔軟な操作」の抽象化とも言えますね。
🧱 コンストラクタ(Constructor)
🔹 目的
- オブジェクト生成時に初期化処理を自動で行う。
- メンバ変数に初期値を設定したり、リソースを確保したりする。
🔹 使い方
class Vector {
public:
Vector(float x, float y, float z) : x(x), y(y), z(z) {}
private:
float x, y, z;
};
🔹 特徴
- オブジェクト生成時に必ず呼ばれる。
- 引数付き・引数なし・コピーコンストラクタなど複数定義可能。
new
を使った動的生成でも呼ばれる。
🧹 デストラクタ(Destructor)
🔹 目的
- オブジェクト破棄時に**後処理(リソース解放など)**を行う。
- メモリ、ファイル、ネットワーク接続などのクリーンアップ。
🔹 使い方
class FileHandler {
public:
~FileHandler() {
fclose(file);
}
private:
FILE* file;
};
🔹 特徴
- オブジェクトがスコープを抜けるとき、または
delete
されたときに呼ばれる。 - 明示的に呼ぶことはできない(C++が自動で呼ぶ)。
- 仮想デストラクタにすると、ポインタ経由でも正しく派生クラスのデストラクタが呼ばれる。
🧭 クラスのポインタを返す(Factory的な使い方)
🔹 目的
- 柔軟なオブジェクト生成(動的生成、条件付き生成、抽象型の返却など)。
- ライフサイクルの制御やポリモーフィズム(多態性)に活用。
🔹 使い方
class Enemy {
public:
static Enemy* createBoss() {
return new Enemy(1000, true);
}
private:
Enemy(int hp, bool isBoss) : hp(hp), boss(isBoss) {}
int hp;
bool boss;
};
🔹 特徴
new
を使ってヒープに確保 → ポインタで返す。- 呼び出し側が
delete
する責任を持つ(スマートポインタで自動化も可能)。 - 抽象クラスやインターフェースを返す場合にも使われる。
🧠 まとめ:抽象化と制御の設計
機能 | ハードウェア的視点 | 抽象化の目的 |
コンストラクタ | 初期化ルーチン | オブジェクトの一貫性を保証 |
デストラクタ | 後処理ルーチン | リソースの安全な解放 |
ポインタ返却 | 間接参照・動的領域 | 柔軟な生成と多態性の実現 |
「メモリの実体」から設計を見ていると、これらは単なる文法ではなく、オブジェクトの生存戦略そのものに見えてくるはずです。
もし「スマートポインタとの関係」や「仮想デストラクタの必要性」など、さらに深掘りしたいテーマがあれば、喜んでお付き合いしますよ。
(by Copilot)
(1)メンバへのアクセス
アロー演算子(接続子)を使ってアクセスする
void Application::Update(void)
{
ncInst->Init();
}
(2)オブジェクトの破棄
delete 演算子を使ってオブジェクトを破棄する。
オブジェクトに割り当てられたメモリも解放される。
delete ncInst; <-- ポインタに格納されているアドレス番号はそのまま変数の中に残っている
ncInst = nullptr; <-- 破棄されたか否かを判断できる様にする為にもヌルを代入しておく必要がある
※必要な時にクラスを動的に作成して、不要になったら破棄するという処理を繰り返す様な
システム構成になった場合は、破棄されているのか作成されているのかを判断する必要が
あるので、破棄した後は必ずヌルを代入する様にしておく
※インスタンスのポインタもコンストラクタ等で、必ずヌルで初期しておく必要がある。
※メモリリーク:確保されたメモリを開放しなかった場合、そのメモリ領域は以後利用不可になる。
●クラス定義とメモリ領域の確保
クラスの定義も、突き詰めれば「メモリの構造をどう設計するか」という話に行き着きます。Z80のようなハードウェア視点で見れば、クラスは構造体+振る舞いの定義であり、インスタンス化された時点で具体的なメモリ領域が確保されるという意味で、変数定義と同じく物理的な裏付けがあります。
🧠 クラス定義とメモリ確保の関係
1. クラス定義そのものは「型の設計」
class Vector3 {
public:
float x, y, z;
void normalize();
};
- この時点ではまだメモリは確保されていません。
- これは「型の設計図」にすぎず、言い換えれば「構造体+関数ポインタ群の抽象化」。
2. インスタンス化でメモリ確保
Vector3 v;
- ここで v に対して、float x, y, z 分のメモリが確保されます。
- メンバ関数は共有される(関数自体はコード領域にあり、インスタンスごとにコピーされない)。
3. 動的確保の場合
Vector3* p = new Vector3();
- ヒープ領域に sizeof(Vector3) 分のメモリが確保され、ポインタ p がそれを指す。
- delete p; で明示的に解放する必要がある。
🧩 Z80的な視点で言うなら…
- クラス定義は「ラベル付き構造体のテンプレート」。
- インスタンス化は「そのテンプレートに従ってRAM上に領域を割り当てる」。
- メンバ関数は「共通ルーチン」であり、インスタンスごとに持つのではなく、thisポインタを通じて動的に振る舞いを変える。
🧠 抽象化の進化としてのクラス
佳之さんのように「メモリの実体」から抽象化を見ていると、クラスは単なる便利な構文ではなく、**「データと操作の空間的・時間的分離」**を可能にする設計手法だと見えてきますよね。
たとえば、DirectX12のリソース管理も、クラスでラップすることで「GPUメモリの確保と解放」を抽象化できます。つまり、クラスはメモリ管理の契約を隠蔽しつつ、制御可能にする手段でもある。
もし「このクラスはどこで何を持ってるのか?」とか「関数はどこに存在してるのか?」みたいな疑問があれば、アセンブリレベルでの挙動も含めて掘り下げられますよ。
(by Copilot)
※インスタンスのポインタもコンストラクタ等で必ずNULLで初期化しておく必要がある。
[3]クラスを利用する手順のまとめ
①クラスを宣言する
②クラスのメンバ関数等の定義(実装)を行う
③呼び出す側のクラスからオブジェクト(インスタンス)を作成する
④クラスを動的に作成した場合には、終了時もしくは不要になった時点で、破棄するクラス内で、動的確保を行ったメモリや、読み込んだ画像データ等は、クラス自体を破棄する前に、それぞれを破棄しておく必要がある。
動的ではなく、実体型としてオブジェクトを作ると、メモリ消費量以前にexeファイルから巨大になる。このため「動的に作成していく」手法が広く使われる。
[4]メンバへのアクセス制限(クラスカプセルとアクセス属性)
クラス型定義では、デフォルトでメンバが不可視になり、保護される。
このため、publicキーワードを指定して、後悔しても良いメンバを可視とするように変更が可能。
外部に公開することが適切でないクラスの重要なデータメンバ、あるいはデータメンバを操作するための内部的な実装の詳細をクラスの外部から遮断(hide)することが可能。
(カプセル化:encapsulation)
①クラスメンバの可視性(アクセス属性)
C++では、クラスカプセル化をクラスメンバの可視性(visibility)を制御するという方法で実現している。
メンバの「可視性」とはメンバに対するアクセス制限の有無のこと。
可視=アクセス制限なし 不可視=アクセス制限あり
デフォルトでは不可視属性。
利用できるアクセス属性は下記。
private:不可視(外部に非公開)
protected:派生クラスを除き不可視(派生クラス:継承されたクラス を除き非公開)
public:可視(外部に公開)
例:
class NewClass
{
int number; ⇒不可視のメンバ(外部からアクセス不可)
public:
NewClass(void);⇒ここから
~NewClass(void);
int GetNumber(void){ return number;}
void Update(void);
⇒ここまで可視のメンバ(外部からアクセス可能)
private:
AbcClass* abcInst; ⇒ここから不可視のメンバ(外部からアクセス不可)
int image;
};
3)カプセル化の簡単なまとめ
クラスの中にデータ(データメンバ)と昨日(メンバ関数)をひとまとめにし、保護したいメンバにprivateをつけて勝手にアクセスできなくする手法(機能)をカプセル化と言う。
②クラス化を使ったカプセル化の一般的技法
(授業では省略)
※クラスというのは、設計段階で、外部からのオブジェクトへのアクセスを許すか許さないかを整理して作成していくもの。
[5]コンストラクタとデストラクタ
●コンストラクタ関数の特性
1.関数名はクラス名と同様
2.戻り値の型を持たない(voidでもない)
3.クラス型のオブジェクトの生成時に呼び出される
4.オブジェクトの初期化を行う
5.1つのクラス定義で複数のコンストラクタを装備できる。
🧱 コンストラクタとは?
🔹 目的
・オブジェクト生成時に自動で初期化処理を行う特別な関数。
🔹 特徴
・オブジェクト生成時に必ず1回だけ呼ばれる。
・クラス名と同じ名前の関数。
・戻り値の型は書かない(void
も不要)。
🧩 コンストラクタの種類と使い方
① デフォルトコンストラクタ(引数なし)
class MyClass {
std::string my_name;
public:
MyClass() {
my_name = "Default";
}
};
MyClass obj; // → 自動で MyClass() が呼ばれ、my_name = "Default" に
- 最も基本的な初期化方法。
new MyClass();
でも同様に呼ばれる。
② 引数付きコンストラクタ
class MyClass {
std::string my_name;
public:
MyClass(std::string name) {
my_name = name;
}
};
MyClass obj("ゆき"); // → my_name = "ゆき"
・初期化内容を引数で指定できる。
・newClass(“ゆき”);でも同様。
③ コンストラクタのオーバーロード(複数定義)
class MyClass {
std::string my_name;
public:
MyClass() {
my_name = "Default";
}
MyClass(std::string name) {
my_name = name;
}
};
・引数の数や型が違えば、複数のコンストラクタを定義可能。
・呼び出し時の引数に応じて、適切なコンストラクタが選ばれる。
🧹 デストラクタとは?
🔹 目的
- オブジェクトが消滅する時に**後始末(リソース解放など)**を行う。
🔹 特徴
- 関数名は
~クラス名
(チルダ付き)。 - 引数なし、戻り値なし。
- 1クラスにつき1つだけ定義可能。
- 自動で呼ばれる(手動では呼べない)。
🧩 デストラクタの使い方
class MyClass {
public:
~MyClass() {
std::cout << "オブジェクト破棄時に呼ばれます" << std::endl;
}
};
void func() {
MyClass obj; // スコープを抜けると自動で ~MyClass() が呼ばれる
}
🧠 デストラクタが呼ばれるタイミング
オブジェクトの種類 | 消滅タイミング |
---|---|
ローカル変数 | スコープを抜けた時 |
グローバル変数 | プログラム終了時 |
new で生成 | deleteされたとき |
🧠 抽象化とライフサイクル
- コンストラクタ → 「生まれる瞬間の初期化ルーチン」
- デストラクタ → 「死ぬ瞬間の後始末ルーチン」
Z80的に言えば、コンストラクタは「電源投入時の初期化処理」、デストラクタは「電源断前のシャットダウン処理」。
C++ではそれを自動で呼び出す仕組みがあるから、安心して抽象化されたオブジェクトを使えるんですね。
もし「コピーコンストラクタ」や「ムーブコンストラクタ」など、さらに深い話に進めたくなったら、いつでもどうぞ。
(by Copilot)
~実習~
800x600pxのウインドウ上で半径10pxのボールをカーソルキーで動かす…を例題として作成
①VisualStudio2022で空のプロジェクトを作成
②main.cppとApplication.cppを作成
③Application.hを作成
Application.h
#pragma once
class Ball;
class Application
{
public:
static constexpr int WINDOW_WID = 800;//外部から参照可能
static constexpr int WINDOW_HIG = 600;//外部から参照可能
Application(void); //コンストラクタ
~Application(void); //デストラクタ
bool SystemInit(void);//Cでのプロトタイプ宣言にあたる
void Run(void);
bool Release(void);
private:
Ball* ball = nullptr;
void Update(void);//外部から参照不能
void Draw(void);
};
Application.cpp
#include<DxLib.h>
#include"Application.h"
#include"Ball.h"
//コンストラクタ
Application::Application(void)
{
}
//デストラクタ
Application::~Application(void)
{
}
bool Application::SystemInit(void)
{
//システム設定処理
SetWindowText("SampleProgram");
SetGraphMode(WINDOW_WID, WINDOW_HIG, 32);
ChangeWindowMode(true);
if (DxLib_Init() == -1)return false;
ball = new Ball();
if (ball == nullptr)return false;
ball->SystemInit();
}
void Application::Run(void)
{
while (ProcessMessage() == 0 && CheckHitKey(KEY_INPUT_ESCAPE) == 0) {
Update();
Draw();
}
}
bool Application::Release(void)
{
ball->Release();
delete ball;
ball = nullptr;
if (DxLib_End() == -1)return false;
return true;
}
void Application::Update(void)
{
ball->Update();
}
void Application::Draw(void)
{
SetDrawScreen(DX_SCREEN_BACK); //描画対象画面を裏画面に設定する
ClearDrawScreen(); //裏画面のクリア
DrawBox(0, 0, WINDOW_WID, WINDOW_HIG, GetColor(0, 0, 255), true);
ball->Draw();
ScreenFlip();
}
main.cpp
#include<DxLib.h>
#include"Application.h"
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdshow)
{
Application apl;
if (apl.SystemInit() == -1)return -1;
apl.Run();
if (apl.Release() == false)return -1;
return 0;
}
Ball.h
#pragma once
#include"StDefine.h"
class Ball
{
static constexpr int BALL_RADIUS = 10; //ボールの半径
static constexpr int BALL_SPEED = 6; //ボールの移動速度
public:
Ball(void);
~Ball(void);
void SystemInit(void);//クラスが違えば、他で使っている関数名と被ってもOK
void Update(void);//クラスごとに別の関数として識別される
void Draw(void);
void Release(void);
private:
POSITION pos; //ボールの中心座標
};
Ball.cpp
#include<DxLib.h>
#include"Ball.h"
#include"Application.h"
Ball::Ball(void)
{
pos.x = Application::WINDOW_WID / 2;
pos.y = Application::WINDOW_HIG / 2;
}
Ball::~Ball(void)
{
}
void Ball::Update(void)
{
if (CheckHitKey(KEY_INPUT_UP) == 1) {
pos.y -= BALL_SPEED;
}
if (CheckHitKey(KEY_INPUT_DOWN) == 1) {
pos.y += BALL_SPEED;
}
if (CheckHitKey(KEY_INPUT_LEFT) == 1) {
pos.x -= BALL_SPEED;
}
if (CheckHitKey(KEY_INPUT_RIGHT) == 1) {
pos.x += BALL_SPEED;
}
//ボールの移動範囲制限(はみださないように)
auto clamp = [](int v, int lo, int hi) { return (v < lo ? lo : (v > hi ? hi : v)); };
pos.x = clamp(pos.x, BALL_RADIUS, Application::WINDOW_WID - BALL_RADIUS);
pos.y = clamp(pos.y, BALL_RADIUS, Application::WINDOW_HIG - BALL_RADIUS);;
}
void Ball::Draw(void)
{
DrawCircle(pos.x, pos.y, BALL_RADIUS, GetColor(255, 255, 255), true);
}
void Ball::SystemInit(void)
{
}
void Ball::Release(void)
{
}
StDefine.h
#pragma once
//StDefine.h
struct POSITION {
int x;
int y;
};
●構造体
C++における「構造体(struct)」は、初心者にとってクラスの前段階として理解しやすい概念です。Z80のようなハードウェア寄りの視点から見れば、構造体は「メモリ上に並んだデータのまとまり」を意味します。ここでは、初心者向けにわかりやすく、実例を交えて説明しますね。
🧱 構造体とは?
構造体(struct
)は、複数の異なる型の変数をひとつにまとめるための仕組みです。たとえば、「座標」を表すには x
と y
の2つの値が必要ですが、それらをひとつの構造体にまとめることで、扱いやすくなります。
✍️ 基本的な構造体の定義と使い方
// 構造体の定義
struct Point {
int x;
int y;
};
// 構造体の使用
int main() {
Point p1; // 構造体の変数を宣言
p1.x = 10; // メンバにアクセス
p1.y = 20;
std::cout << "座標: (" << p1.x << ", " << p1.y << ")" << std::endl;
return 0;
}
🔹 ポイント
Point
は新しい型の名前。p1
はPoint
型の変数(インスタンス)。p1.x
やp1.y
で個別のメンバにアクセス。
🧠 構造体とクラスの違い(初心者向け)
項目 | struct | class |
---|---|---|
デフォルトのアクセス修飾子 | public | private |
主な用途 | データのまとまり | データ+振る舞い(関数) |
継承・ポリフォーリズム | 可能(ただしあまり使われない) | よく使われる |
※C++では struct
も class
とほぼ同じ機能を持ちますが、初心者には「構造体=データの箱」「クラス=データ+操作の箱」と覚えるとわかりやすいです。
🎮 実例:ゲームのキャラクター情報を構造体で表す
struct Character {
std::string name;
int hp;
int attack;
};
int main() {
Character hero = {"勇者", 100, 30};
std::cout << hero.name << " のHPは " << hero.hp << "、攻撃力は " << hero.attack << std::endl;
return 0;
}
🔹 この例のポイント
Character
は「名前」「HP」「攻撃力」を持つ構造体。- 初期化時に
{}
を使って一括で値を設定できる。 - データのまとまりとして扱えるので、関数に渡すのも簡単。
🧠 Z80的な視点で言うなら…
構造体は「連続したメモリ領域に、意味のあるラベルを付けたもの」と考えるとしっくりきます。
Z80で言えば、LD HL, data
のようにデータ領域を指して、そこから LD A, (HL)
→ INC HL
で順に読み取るような操作が、C++では p.x
や p.y
で直接アクセスできるようになった、という抽象化です。
もし「構造体を関数に渡す」「配列にする」「ポインタで扱う」など、次のステップに進みたくなったら、いつでもどうぞ。その抽象化センスなら、すぐにクラスとの違いや設計意図まで見えてくるはずです。
構造体も立派な「型(type)」です。しかも、自分で定義できる型というのがポイントですね。
プリミティブ型(int
, float
, char
など)はCPUが直接扱える基本単位ですが、構造体はそれらを意味のあるまとまりとして再構成した抽象型です。まさに「型の抽象化の第一歩」と言えます。
🔧「型」としての構造体の役割
構造体は、以下のような目的で使われます:
- 意味づけ:ただの
int x, y;
よりもPoint
型の方が「座標だな」とすぐにわかる。 - 再利用性:同じ構造を何度も使いたいときに便利。
- 安全性:型チェックが働くので、誤った使い方を防げる。
🧠 型とは何か?
Z80のような低レベル環境では、型は存在せず、すべては「バイト列」でしたよね。でもC++では、型があることで:
- メモリの意味づけができる(
Point
は2つのint
を持つ構造だとわかる) - コンパイラが型チェックしてくれる(
Point
にfloat
を代入しようとすると怒られる) - 関数の引数や戻り値に意味を持たせられる(
Point getCenter()
など)
つまり、構造体は「意味を持ったメモリの塊」であり、「型としての抽象化の入り口」なんです。
バイト列 → 構造体(struct) → クラス(class) → テンプレート(template) → 型理論(type theory)
構造体はこの流れの中で、「意味づけされたデータの集合」として登場します。佳之さんが興味を持っている型理論の世界では、構造体は「product type(直積型)」として扱われ、型の構成要素のひとつになります。
もし「型ってそもそも何?」とか「型理論で構造体はどう扱われるの?」みたいな話に進めたくなったら、喜んでお付き合いしますよ。
🧠 オブジェクト vs インスタンス:ざっくり整理
C++ではこの2つの言葉がほぼ同義で使われることも多いです。
厳密に言えば:
用語 | 意味 | 例え |
オブジェクト | クラスから生成された「個体」 | 設計図に基づき作られた機械 |
インスタンス | オブジェクトの「実体化された存在」 | RAM上に配置された機械の現物 |
- クラス:型の定義(抽象的な設計図)
- インスタンス:その型に基づいて確保されたメモリ領域(実体)
- オブジェクト:インスタンス化されたもの全般(状態+振る舞いを持つ)
(by Copilot)
●ball -> Draw();の意味
1. ball は何者?
Application
クラスの中にこうあります👇
private:
Ball* ball; // ← Ball型の「ポインタ」
つまり ball
は「Ball クラスのオブジェクトを指す矢印(ポインタ)」です。
SystemInit()
の中で実際に new Ball();
されてます:
ball = new Ball();
これで ball
が本物の Ball
オブジェクトを指すようになります。
2. -> 演算子の役割
C++では、
- オブジェクトそのものならドット
.
でメンバにアクセス - **ポインタ(矢印)**ならアロー
->
を使ってメンバにアクセス
というルールです。
Ball b;
b.Draw(); // 普通のオブジェクトなので . を使う
Ball* pb = &b;
pb->Draw(); // ポインタなので -> を使う( (*pb).Draw() と同じ)
3. ball->Draw(); がやっていること
ball
はBall*
(Ballクラスへのポインタ)->Draw()
で「その先にある Ball オブジェクトのDraw()
関数を呼ぶ」
イメージすると:
Application が Ball の“操作盤”を持っていて、
`ball->Draw();` で「ねえ Ball さん、自分を描画して!」と呼びかけている。
実際に呼ばれるのは Ball.cpp のこれ:
void Ball::Draw(void)
{
DrawCircle(pos.x, pos.y, BALL_RADIUS, GetColor(255, 255, 255), true);
}
つまり ball->Draw();
は「その Ball の位置(pos.x,pos.y)に白い円を描いて!」という意味。
4. まとめ
ball
… Ball クラスを指すポインタ->
… ポインタを通して中の関数や変数にアクセスするための記号ball->Draw();
… Ball が持つDraw()
関数を呼び出し、ボールを画面に描画する
💡ポイント.
と ->
の違いを押さえるとスッキリします!
- オブジェクトそのものを持っているなら
.
- オブジェクトを指すポインタを持っているなら
->
いや、ball.Draw()、ball->Draw()との使い分けとか、全体の意味構造が私には理解できていない。なんでアクセス方法が二種類あるのさ、って。 この例で、ポインタがクラスの先頭アドレスを差すのは分かったけれど「ポインタの中の関数」っていうのが私には理解不能。
ChatGPT:
すごくいい疑問です。そこが ポインタとオブジェクトの本質的な違い で、C++に入った人が最初に「ん??」ってなるポイントなんですよ。
順を追って、イメージとコードで説明しますね。
1. クラスは「変数の束 + 関数の束」
たとえば Ball クラスには
- 変数(メンバ変数)
posx
,posy
- 関数(メンバ関数)
Update()
,Draw()
など
が「ひとまとまり」になっています。
C言語でいう struct
に、さらに「そのデータを扱う関数」が同居しているイメージ。
2. アクセス方法は「値を持ってるか」「アドレスを持ってるか」で変わる
- オブジェクトそのものを持っている
→ その場で中身にアクセスできる
→ball.Draw();
- オブジェクトの場所(アドレス)しか持っていない
→ 中身を取り出すには「*で間接参照」してからアクセスする必要がある
→(*ball).Draw();
でも (*ball).Draw()
は書きにくいので、C++ では糖衣構文として ball->Draw()
が用意されています。
つまり、
ball->Draw(); ≒ (*ball).Draw();
です。
3. 「ポインタの中の関数」ではなく「ポインタの先のオブジェクトの関数」
ここが誤解しやすいところ。
ポインタそのものは「数値(アドレス)」しか持っていません。関数なんて入っていません。
ball->Draw();
の意味は、
ball
が指しているアドレスに行く- そこにある
Ball
オブジェクトを見つける - そのオブジェクトの
Draw()
を呼び出す
です。
だから正確には「ポインタの先のオブジェクトの関数を呼んでいる」。
4. 使い分けは「直接持つ」か「指すだけ」か
直接オブジェクトを持つ場合
class Application {
Ball ball; // ボールを直接持つ
};
void Application::Draw() {
ball.Draw(); // . でアクセス
}
ポインタで持つ場合
class Application {
Ball* ball; // ボールを指すだけ
};
void Application::Draw() {
ball->Draw(); // -> でアクセス
}
5. なんでわざわざポインタを使うの?
- 動的に生成したいとき(new / delete)
- ゲーム中に「ボールを増やす/消す」ができる
- 存在をオプションにしたいとき
- ないときは
nullptr
- ないときは
- 多態性(継承・仮想関数)で差し替えるとき
Enemy* e;
がSlime
かDragon
を指す、みたいな使い方
今のサンプルだと「必ず1個のボールがある」のでポインタを使う必然性は薄いですが、後々拡張するゲーム構造に対応しやすくするためポインタで設計されていると考えてOKです。
✅ まとめ
.
→ オブジェクトそのものにアクセス->
→ ポインタが指す先のオブジェクトにアクセス((*ptr).
の省略形)- 「ポインタの中に関数がある」のではなく、ポインタが差しているオブジェクトの関数を呼んでいる
✅ クラスとは何か?
- クラスは「設計図」です。
「この型にはこんなデータ(変数)と、こんな操作(関数)があるよ」とまとめたもの。
例:
class Ball {
public:
int x; // データ
int y; // データ
void Draw(); // 操作
};
- この時点では「ただの型の定義」に過ぎません。
int
とかstruct
と同じように「こういうものを作れるよ」という設計図です。
✅ オブジェクトとは何か?
- クラスを実際に使える“実体”にしたものが オブジェクトです。
Ball b; // Ballクラスのオブジェクトを生成
b.x = 100;
b.y = 200;
b.Draw();
ここでの b
がオブジェクトです。
**設計図から作った“完成品”**というイメージ。
✅ 直接オブジェクトを持つ vs ポインタで持つ
直接オブジェクトを持つ
class Application {
Ball ball; // Ballという部品をそのまま内蔵
};
- Application の中に Ball が「組み込まれる」
- Application を作ると同時に Ball も必ず作られる
- 「必ず1個ある」場合にシンプルで安全
ポインタで持つ
class Application {
Ball* ball; // Ballという部品を指すだけ(アドレスを保持)
};
- Application の中には アドレスだけ(指し示すだけ)
- 本体は
new Ball()
で別に作る - 「あるかないかを切り替える」「動的に増やす・減らす」が可能
- 複数種類の継承クラスを指すときに便利(多態性)
✅ クラス定義に「直接」と「間接」があるのではなく、使う側の宣言に2パターンある
混乱しやすいですが、こう整理するとスッキリします:
- クラス定義はいつも「設計図」だけ。そこに直接/間接の区別はない。
- オブジェクトを使うときに、
- 直接オブジェクトを生成するか(
Ball b;
) - ポインタを用意して後から new するか(
Ball* b = new Ball();
)
の二択があるだけ。
- 直接オブジェクトを生成するか(
✅ 具体例で対比
直接オブジェクト
Application app;
app.ball.Draw(); // .でアクセスできる
ポインタオブジェクト
Application* app = new Application();
app->ball->Draw(); // -> でアクセス(ポインタだから)
✅ まとめ
- クラス:設計図(「こんなデータと機能を持つ型」)
- オブジェクト:設計図から作った実体(変数)
- 直接オブジェクトを持つ:必ず作られる・安全・シンプル
- ポインタで持つ:動的に制御できる・柔軟だが手間が増える
- クラス定義そのものに「直接/間接」があるわけではない、使い方のスタイルの違いだけ。
(by ChatGPT)
コメント