はじめに
私は現在,セキュリティ・キャンプ ネクストの応募課題として,C++のクラスについて調査し,解説記事を執筆しています.
本記事では,せっかく執筆し公開するのであれば,見てくださる人の役に立つものにしたいと考え,C++を触れたことのない,大学の授業で基本的な構文(if,forなど)は理解しているけど設計思想は知らないといった人を対象に内容を調整しました.
本記事のゴールは「C++に触れたことのない,大学の授業で基本的な構文は理解した人がオブジェクト指向の基礎を理解し,安全にC++のクラスを書けるようになる」です.
本記事ではクラス及びオブジェクト指向の必要性から解説をはじめ,C++におけるクラスの使い方や注意点を実際のコードを踏まえて解説したいと思います.
本記事ではクラスの安全な書き方までしかお伝えしません.
クラスを定義した先にある実践的に扱っていく機能(カプセル化,継承,多態性等)は取り上げないので注意ください.
オブジェクト指向とは
まず,オブジェクト指向について説明します.C++のクラスという記法に関する記事なのにオブジェクト指向から学ぶ理由としては,クラスというものはオブジェクト指向プログラミングを実現するための概念であり,オブジェクト指向プログラミングを実現するためにC++ではクラスに関する記法が存在するからです.
それでは,オブジェクト指向について説明していきます.
オブジェクト指向を一言で説明すると「読みやすく,変更がしやすいコードを書くためのプログラミングの考え方」です.
ここで具体例を挙げながら説明します.C++で書いていますが,この段階では説明とこれまで習ってきたことから何となくイメージをつかんでいただければいいです.
それでは,まず複数の「武器」の攻撃力データを管理し,攻撃処理を行うプログラムを作るとします.
この時何も意識せずに書くと次のようになります.
ここでは,データsword_powerと処理void attack(std::string name, int power) がばらばらに管理されているため,毎回,攻撃力と武器の名前を引数に渡しています.
また,sword_powerはグローバル変数(プログラム全体で読み出し・書き換えが可能な変数)なのでどこからでも書き換えられます.
#include <iostream>
#include <string>
// 剣と斧の攻撃力を定義
int sword_power = 10;
int axe_power = 20;
std::string playerA = "Tom";
std::string playerB = "Ken";
// 攻撃した武器の名前とダメージを表示する関数
void attack(std::string name, int power) {
std::cout << name << "で攻撃! " << power << "のダメージ!\n";
}
int main() {
std::cout << playerA << "のターン " << "\n";
attack("剣", sword_power); // 剣での攻撃表示
std::cout << playerB << "のターン " << "\n";
attack("斧", axe_power); // 斧での攻撃表示
std::cout << playerA << "のターン " << "\n";
attack("剣", sword_power); // 剣での攻撃表示
std::cout << playerB << "のターン " << "\n";
attack("斧", axe_power); // 斧での攻撃表示
// 意図しないデータの書き換えができてしまう
sword_power = 9999;
std::cout << playerA << "のターン " << "\n";
attack("剣", sword_power);
return 0;
}
これをオブジェクト指向を使って書き直すと次のようになります.
コメントアウトの「1」を探してください.ここのclass Weaponの中ではデータname, powerと処理attackがまとめて管理されています.
たったこれだけの変更ですが,sword.attack()のように「誰が(剣が)・何をする(攻撃する)」という直感的な語順になり,コードが非常に読みやすくなりました.もちろん,毎回引数に攻撃力を渡す必要もありません.
また,データと処理をひとつの箱にまとめ,private(アクセス修飾子で説明) としたことで,意図しない書き換えから大切なデータを守ることができます(コメントアウトの「2」を参照).
#include <iostream>
#include <string>
// 1.武器の振る舞いを管理するクラス
// データ(name,power)と処理(use)をまとめて管理
class Weapon {
private:
std::string name;
int power;
public:
// コンストラクタ(初期化子リストを使用したよりC++らしい書き方)
Weapon(std::string n, int p) : name(n), power(p) {}
// 武器を使う(攻撃する)振る舞い
// 値をgetさせるのではなく,武器自身にメッセージを出力させる
void use() const {
std::cout << name << "で攻撃! " << power << "のダメージ!\n";
}
};
// プレイヤーを管理するクラス
class Player {
private:
std::string name;
Weapon weapon; // プレイヤーが内部に武器を持つ(コンポジション)
public:
// コンストラクタでプレイヤーの名前と持たせる武器を設定する
Player(std::string n, Weapon w) : name(n), weapon(w) {}
// プレイヤーが攻撃する振る舞い
void attack() const {
std::cout << name << "のターン \n";
weapon.use(); // 自分が持っている武器に対して「使え」と指示を出す
}
};
int main() {
// 1. 武器を作成する
Weapon sword("剣", 10);
Weapon axe("斧", 20);
// 2. プレイヤーを作成し,それぞれに武器を持たせる
Player playerA("Tom", sword);
Player playerB("Ken", axe);
// 3. 各プレイヤーが攻撃のメソッドを実行する
playerA.attack();
playerB.attack();
playerA.attack();
playerB.attack();
// 2.外部から直接書き換えようとするとコンパイルエラーが発生
// wapon.power = 9999;
return 0;
}
このように,データと処理をまとめて管理することで,コードを読みやすく,安全に変更できるようにする考え方がオブジェクト指向です.
オブジェクト指向にはクラスとインスタンスという2つの重要なキーワードがあります.
-
クラス:
「どんなデータ(変数)を持っているか」「どんな処理(関数・メソッド)ができるか」を定義したものです.
(例:「武器」というクラスは,攻撃力・リーチというデータを持ち,ダメージを与えるという処理(当たったかの判定処理,敵のダメージを減らす処理)ができる) -
インスタンス:
クラスからメモリ上に作られた具体的な実体のことです.
(例:武器クラスに剣なら攻撃力10,リーチ1.5mという具体的なデータを与え,剣ならではのダメージを与える処理を可能にした状態)
オブジェクト指向には,読みやすく変更しやすいコードを実現させるために大きく分けて3つの機能が決められています.
具体的には,外部からデータをいじらせないためのカプセル化,既存のクラスから動きを引き継いで新しいクラスを作る継承,同じ関数名でもオブジェクト間で異なる振る舞いを示す多態性です.
ここで3つの機能はオブジェクト指向において非常に重要な概念(クラスは基礎of基礎)ですが説明すると長くなり,本題であるC++のクラスの説明からそれてしまうので,別の記事で改めて説明させていただきます.
オブジェクト指向は,これらの機能によって,プログラム変更の影響範囲を限定することで管理をしやすくし,プログラムの再利用を高めることでコードを書く効率を高める設計思想です.
C++におけるオブジェクト指向入門
ここまでオブジェクト指向の説明をしてきました.では,実際にC++のクラスの使い方を説明していきます.
まず,クラスを使ったプログラミングでは,クラスの型宣言,処理の記述,インスタンスの作成といった手順で書いていきます.これはどの言語でもおおよそ同様です.
順番に見ていきましょう.
クラス宣言
まず,クラスの書き方として,クラスの型(設計図)を書きます.
class Sample
{
public:
void set(int num);
int get();
private:
int member_num;
};
Sampleの前のclassは型宣言です.これからクラスを定義しようという宣言です.
クラスの構成要素はメンバと呼ばれ,関数については「メンバ関数」,変数については「メンバ変数」と呼ばれます.
ここのメンバ関数やメンバ変数の宣言方法は,通常のC言語と同じです 型宣言 名前(関数なら引数);.
また,このメンバ関数のことを「メソッド」と呼ぶこともあります.
アクセス修飾子
public と private について説明します.これらはアクセス修飾子といいます.
public はすべての範囲(クラスの外側)から呼び出し・読み出しが可能であることを示します.一方,private はそのクラスの内部からしかアクセスができない(隠されている)ことを示します.つまり,Sample クラスの member_num を直接操作できるのは,同じクラス内に定義された set や get といった関数だけになります.
この外部からのデータアクセスを制限する仕様は先に軽く触れた,カプセル化の思想に基づいています.オブジェクト内の処理からしかデータにアクセスできないようにすることで,データの保護を助けます.
また,外部アクセスを制限することで,オブジェクトを使う人は複雑な処理を意識せず使え,作る人はオブジェクト内の変更がオブジェクト外に影響しないメリットがあります.
メンバ変数・メンバ関数の定義方法
ここまで,クラスの宣言の方法を見てきました.型は宣言しましたが,このままでは具体的な処理の中身がないので使えません.そこで,メンバ関数の具体的な定義方法について説明します.
void Sample::set(int num)
{
member_num = num;
}
int Sample::get()
{
return member_num;
}
上記のコードでは,クラスの外側で関数の具体的な処理を書いています.関数名の前にある Sample:: という記述は,「これは Sample クラスに属する関数ですよ」とコンパイラに教えるための記号(スコープ解決演算子)です.
C++ではこのように,「関数の名前や引数(設計図)」と「具体的な処理(中身)」を分けて書くのが一般的です(もちろん,クラスの中に直接処理を書くこともできます).
これにより,コードが長くなっても設計図を見れば「何ができるクラスなのか」が一目でわかるようになります.
インスタンスの作成
これで,クラスが使えるようになりました.最後にインスタンスを作成し,コード内で呼び出すところを説明します.
#include <iostream>
#include "sample.h" // Sampleクラスの宣言が書かれたファイルを読み込む
using namespace std;
int main()
{
Sample obj1; // Sampleをインスタンス化
Sample obj2; // Sampleをインスタンス化
int num1 = 1;
int num2 = 2;
obj1.set( num1 ); // obj1のメンバ変数をセット
cout << obj1.get() << endl; // obj1のメンバ変数の値を出力
obj2.set( num2 ); // obj2のメンバ変数をセット
cout << obj2.get() << endl; // obj2のメンバ変数の値を出力
cout << obj1.get() << ", " << obj2.get() << endl; // obj1, obj2のメンバ変数の値を出力
return 0;
}
本筋とは関係ないですがcout << obj1.get() << endl;と見慣れない書き方があります.これは,C++における出力文です.PythonやC言語で言うPrint文に当たります.
ここでは Sample obj1; のように記述して,設計図であるクラスから実際にメモリ上にデータとして使える「インスタンス(実体)」を作り出しています.
重要なのは,obj1 と obj2 は同じ設計図から作られていますが,全く別のデータを持っているという所です.そのため,obj1 に 1 をセットしても,obj2 のデータには影響せず,使いまわすことができます.
また,メンバ変数にアクセスしたり関数を呼び出したりする際は,obj1.set() のように .(ドット演算子)を用いて繋ぎます.
クラスを用いることで,member_num という大事な変数を直接書き換えられる危険を防ぎつつ,set や get といった決められた手順(関数)を通してのみ安全にデータを操作できるようになっています.
コンストラクタ(初期化処理の追加)
先ほどのSampleクラスでは,インスタンスを作成したあとにsetメソッドを使ってデータをセットしていました.
しかし,もしプログラマがsetを呼び忘れてgetを使ってしまった場合,変数にはゴミ値が入っているので,予期せぬバグが起こります.(Pythonから入った人には馴染みがないかもしれませんがC言語やC++では,ゴミ値が入った状態でもコンパイルが通り,プログラムが動いてしまいます.)
そこで,インスタンスを作成するのと同時に,必ず初期値をセットさせるようにできる仕組みが用意されています.これがコンストラクタです.
class Sample
{
public:
// コンストラクタの宣言(戻り値の型を持たず,クラス名と同じ名前にする)
Sample(int num);
int get();
private:
int member_num;
};
// コンストラクタの定義
Sample::Sample(int num)
{
// privateのmember_numに初期値を設定
member_num = num;
}
int Sample::get()
{
return member_num;
}
#include <iostream>
#include "sample_const.h"
using namespace std;
int main()
{
// インスタンス作成時に( )を使って初期値を渡すよう強制される
Sample obj1(1);
Sample obj2(2);
// setを呼び出さなくても,すでにデータが初期化されている
cout << "obj1の値: " << obj1.get() << endl;
cout << "obj2の値: " << obj2.get() << endl;
return 0;
}
このようにコンストラクタを使うことで,インスタンス作成時に初期値を渡すよう強制できます.
これによって,「データが正しくセットされていないオブジェクト」が作られるのをコンパイル時点で防ぐことができ,より安全なプログラムになります.
より安全なクラス設計をするためのヒント
ここまで,オブジェクト指向の基礎を学び,安全なクラス設計を学んできました.
最後に,少しだけ背伸びをしてより安全なクラス設計をするための2つのヒントをお伝えします.
constを使おう
C++には,変数の書き換えを禁止するconstという便利なキーワードがあります.これをクラスのメンバ関数(メソッド)に対しても使うことができます.
たとえば,attackメソッドやgetメソッドでは,データを読み出すだけで,武器の名前や攻撃力といったデータ自体は書き換えは起こらないです.そういった「データを変更しない関数」には,関数の後ろにconstをつけるのがC++の鉄則です.
class Weapon {
private:
std::string name;
int power;
public:
Weapon(std::string n, int p) {
name = n;
power = p;
}
// データを書き換えない(読み取り専用の)関数には const をつける
void attack() const {
std::cout << name << "で攻撃! " << power << "のダメージ!\n";
// power = 0;
// ↑ このように,誤ってデータを書き換えようとしても,コンパイルエラーで守られる.
}
};
constをつけることで,「この関数を呼んでもデータは絶対に書き換わらない」ことをコンパイラが保証します.
もしあなたが誤って関数内でデータを書き換えるコードを書いても,実行前のコンパイル時にエラーを吐かせられるため,予期せぬバグを防ぐことができます.
getやsetの乱用は良くない
クラスを学びたての頃に一番陥りやすい罠が,「すべてのprivate変数に対して,とりあえずgetとsetを作ってしまうこと」です.
例えば,武器を強化するプログラムを書きたいとします.このとき,以下のように定義したsetとgetを使うことで実装ができます.
// あまり良くない例
class Weapon {
private:
int power;
public:
int get_power() const { return power; }
void set_power(int p) { power = p; }
};
sword.set_power(sword.get_power() + 5);
これでも想定する処理を実現することは可能です.ただし,オブジェクト指向のお作法ではアウトです.なぜなら,この書き方でprivateに設定した値に直接アクセスすることと本質的には変わりません.
privateは外部からのアクセスを制限し,データを保護するためにあります.したがって,どんな処理であってもgetやsetで実現してしまうことは危険です.例えば,意図しない値(マイナスの攻撃力など)への置き換えが容易になります.そのため,getやsetといった強い権限は必要最低限に抑える必要があります.
また,この必要最低限に抑えるという考え方は,オブジェクト指向やC++に限った話ではないです.システムの管理には,PoLP(Principle of Least Privilege)と呼ばれる最小権限の原則があります.これは,不要な権限を与えないことで,リスクを最小化,被害を最小限に抑える方針です.今回も似たような考え方で,乱用を控える方がよいです.エンジニアを目指していれば様々な場面で意識しないといけないので,ぜひ併せて覚えておいてください.
長くなりましたが,よい例としては次の通りです.
// 良い例:外から直接データをいじらせず,専用の処理を用意する
class Weapon {
private:
int power;
public:
Weapon(int p) { power = p; }
// 武器を強化するメソッドを用意する
void power_up(int amount) {
power += amount; // クラスの内部で安全に計算する
}
};
Weapon sword(10);
sword.power_up(5); //強化幅を入れるだけ
基本的にgetやsetの使用は控え,データを触る際は明確な意図を持ったメッソドを書くことを意識してください.
おわりに
今回はC++のクラスを理解し,安全なクラスの書き方を学ぶため,オブジェクト指向からおさらいしてきました.
オブジェクト指向では,読みづらく,変更しづらいコードを読みやすくするため,繰り返し出現する処理をクラスにまとめインスタンスとして使いまわせるようにします.この処理を実現するために,カプセル化,継承,多態性という3つの機能が備えられていました.
また,C++では,クラスを使った書き方で再定義が不要なこととインスタンス化することで異なる変数を使いまわせること,そしてコンストラクタによって初期値を設定させることで,C言語やC++特有のゴミ値へのアクセスからコードを守る方法について学んできました.
もし,これからオブジェクト指向を学んで行かれたい方は,DRY原則に従って,コードを作り直したり(リファクタリング),「カプセル化」,「継承」,「多態性」といったキーワードでオブジェクト指向を深堀っていくとよいかもしれません.
参考文献
一週間で身につくC++言語の基本/ 第2日目:クラス
「オブジェクト指向」とは?プログラミングに必要不可欠な要素を解説!
C++言語の基礎 第3弾:クラス
Microsoft Build 2026 class (C++)
オブジェクト指向って何だよ! #AdventCalendar2021
もうGetter/Setterを使うのはやめよう(feat. cleanCode)
C++の基礎:カプセル化について学ぶ