はじめに
こんにちは、42Tokyoの一般生徒ことsamatsumです。
C言語経験者に向けたC++入門シリーズ、第2弾です。
最初の一歩だけど第2弾です。
前回の記事では「Hello World」を通じて、ストリームやら名前空間やらを学びました。
今回は学んでいくのはクラス(class)。
C言語の構造体(struct)と似てるように見えるアレです。
オブジェクト指向と呼ばれる、C++で超重要な考え方の基本ですので、張り切っていきましょう。
C++が大規模開発に使用される理由がよく分かりますよ。
この記事を読み終える頃には、以下のコードが何をしているか完全に理解できるようになるでしょう。
#include <iostream>
#include <string>
class Dog {
private:
std::string _name;
unsigned int _age;
public:
void set_name(std::string new_name) {
_name = new_name;
}
std::string get_name() {
return _name;
}
void set_age(unsigned int new_age) {
if (new_age > 31)
return;
_age = new_age;
}
unsigned int get_age() {
return _age;
}
};
int main() {
Dog dog;
dog.set_name("ポチ");
dog.set_age(3);
std::cout << dog.get_name() << "(" << dog.get_age() << "歳)" << std::endl;
}
C言語の構造体で書くとこう↓です。
#include <stdio.h>
#include <string.h>
typedef struct s_dog {
char name[50];
unsigned int age;
} t_dog;
int main() {
t_dog dog;
strcpy(dog.name, "ポチ");
dog.age = 3;
printf("%s(%u歳)\n", dog.name, dog.age);
}
C言語は13行。C++版は30行以上。
「なんでわざわざ回りくどい書き方するの?」って思いましたね?
安心してください、解説しますよ(とにかく解説する安村)。
完璧なコードではありません!
この記事のコードは、クラスの仕組みを理解しやすくするために、実務や課題で必須となる重要な要素をあえて省略しています。
・コンストラクタ(変数の初期化を自動化する)
・const修飾(データを書き換えないことを保証する)
・参照渡し(無駄なコピーを防いで高速化する)
・ヘッダファイル(.hpp) とソースファイル(.cpp)の分割
『Lv.1の書き方』と思って読み進めてください!
目次
- C言語の構造体、何が問題だった?
- C++のクラス:構造体の進化形
- アクセス指定子:publicとprivate
- カプセル化:なぜデータを隠すのか?
- structとclassの違い(実は1つだけ)
- 冒頭のコードを読み解く
- まとめ
C言語の構造体、何が問題だった?
構造体はデータをまとめるだけ
C言語の構造体、便利でしたよね。
関連するデータをひとまとめにしちゃえるヤツ。
typedef struct s_dog {
char name[50];
unsigned int age;
} t_dog;
「おっ、年齢をunsigned intにしてるやん。マイナスの年齢は防げるね」と思ったあなた、鋭いね~。
でも、使う側のコードを見てみると…
t_dog dog;
strcpy(dog.name, "ポチ"); // C言語の配列には直接代入できないのでstrcpy
dog.age = 300; // 300歳の仙犬が爆誕
年齢に300って入れられても、構造体は何も文句を言いません。てか、言えません。
unsigned intを使えばマイナスは防げますが、「犬が300歳」とか「人間が999歳」みたいな意味的にありえない値までは型だけでは防ぎようがないんです。
変数の型は"箱の形"を決めるだけで、"中身の意味"までは面倒を見てくれないという事でさ。
もちろんC言語でも、検証付きの関数を「使えば」問題は有りません。
void set_dog_age(t_dog *dog, unsigned int age) {
if (age > 31) // 犬の寿命的に30歳超えはありえない
return; // 不正な値は弾く
dog->age = age;
}
しかし、問題は関数を「使わなくても」代入出来ちゃうこと。
set_dog_age(&dog, 3); // 検証関数を使っててえらい!
dog.age = 300; // これも通っちゃう!誰も止められない止まらない300歳の仙犬が爆誕
C言語では構造体のメンバが常に構造体外から丸見えand編集可能なので、検証関数を迂回して直接に変数を書き換えることを防ぐ手段が言語レベルで㋐りません。
C言語は、変数を書き換える際に、関数の使用を強制できないのです。
C++風に言うと、C言語の構造体は全てのメンバが常にpublic(公開)な状態ということ。
構造体では、外から好き勝手に触れられる設計しか選べなかったんです。(publicの詳しい意味は後ほど~)
もう1つの問題:データと操作がバラバラ
C言語では、構造体に関連する操作は別の関数として書く必要がありました。
// 構造体の定義
typedef struct s_dog {
char name[50];
unsigned int age;
} t_dog;
// 操作する関数は別の場所に...
void print_dog(t_dog *dog);
void init_dog(t_dog *dog, const char *name, unsigned int age);
t_dogとprint_dogは密接に関係しているのに、コード上はバラバラ。
プロジェクトが大きくなると「この構造体、どの関数で操作するんだっけ?」と迷子になりがちです。
C言語の構造体の問題点をまとめると:
- データが外から丸見え
― 関数を使用せずに変数を編集できちゃうよ~。- データと操作がバラバラ
― 関数を探し回る必要がある。「あの関数どこ置いたっけ?」
C++のクラスは、この2つの問題をまるっと解決します。さっそく見ていきましょう。
C++のクラス:構造体の進化形
クラスとは?
C++のクラスは、ざっくり言うと「データと、そのデータに対する操作をセットにした構造体」です。
前回の記事で「オブジェクト=データ+操作」と言いましたよね。coutは「標準出力への接続情報+<<でデータを送る機能」がセットになったオブジェクトだと。
あの「オブジェクト」を自分で設計するための仕組みが、クラスです。
クラスの中に関数を入れる
C++では、クラスの中に関数を定義できます。
クラスの中にある関数を**メンバ関数**と呼びます。
ここで出てくるstd::stringは、C++の文字列型です。C言語のchar[]やchar *に代わるもので、strcpyやstrlenを使わなくても代入(=)や比較(==)がそのまま書けます。バッファサイズも自動で管理してくれる便利なやつ。
class Dog {
std::string name;
unsigned int age;
// メンバ関数:このクラスに属する関数
void print() {
// name と age に直接アクセスできる!
std::cout << name << "(" << age << "歳)" << std::endl;
}
};
C言語では print_dog(&dog) と書いていたところが、C++では dog.print() と書くようになりました。
「dogクラスに所属する、printメンバ関数」ということですね。
「構造体と関数の紐づけ」をC言語で頑張ろうとすると関数ポインタを構造体のメンバに持たせる…なんて手もありましたが、かなり面倒でしたよね。
// C言語で無理やり「構造体に関数を持たせる」例
typedef struct s_dog {
char name[50];
int age;
void (*print)(struct s_dog *self); // 関数ポインタを構造体メンバに…面倒!
} t_dog;
void dog_print(t_dog *self) {
printf("%s(%d歳)\n", self->name, self->age);
}
int main() {
t_dog dog;
dog.print = dog_print; // 関数ポインタを手動でセット…面倒!!
dog.print(&dog); // 自分自身を引数で渡す必要がある…面倒!!!
}
C++のメンバ関数なら、そんな苦労なしに「この関数はこのデータに属する」という関係を自然に表現できます。
| C言語 | C++ | |
|---|---|---|
| 呼び出し方 | print_dog(&dog) |
dog.print() |
| データの渡し方 | ポインタで渡す | 自動的にアクセスできる |
| 関係性 | 関数と構造体は別々 | 関数はクラスの一部 |
ポイント:メンバ関数は、そのクラスのデータ(メンバ変数)に直接アクセスできます。C言語のように構造体のポインタを引数で渡す必要がありません。
データと操作が1つにまとまりました!
これだけでも十分嬉しいですが…クラスの真骨頂はここからだ!
アクセス指定子:publicとprivate
「見せるもの」と「隠すもの」を分ける
クラスの最大の特徴は、データへのアクセスを制御できること。
これを実現するのがアクセス指定子です。
class Dog {
private: // ← 外から触れない領域
std::string _name;
unsigned int _age;
public: // ← 外から使える領域
void print() {
std::cout << _name << "(" << _age << "歳)" << std::endl;
}
};
| アクセス指定子 | 意味 | C言語で例えると |
|---|---|---|
public |
クラスの外からアクセスできる | いつも通り |
private |
クラスの中からしかアクセスできない | C言語にはこの概念がない! |
命名の慣習:privateメンバ変数には先頭に_(アンダースコア)を付ける慣習が有ります。_name、_ageのように書くことで、コードを読む時に「これはprivateなデータだな」とひと目でわかるようにするためです。42の課題でもこの慣習に従うのがおすすめです。
レストランを思い浮かべてください。
-
public= 客席(お客さんが自由に利用できる) -
private= 厨房(お客さんは入れない。料理はスタッフを通じて注文する)
お客さん(外部のコード)は、厨房(private領域)に直接入ることはできません。
料理(privateなデータ)がほしければ、ウェイター(publicな関数)を通じてお願いするしかないのです。
アクセス指定子には public / private の他に、protected というものも。
これは「継承(Inheritance)」という機能を使う時に真価を発揮する、家族限定公開みたいなやつです。
詳細はまた別の記事で。
試しにprivateなメンバにアクセスしてみると…
int main() {
Dog dog;
dog._name = "ポチ"; // ❌ コンパイルエラー!privateだから
dog._age = 3; // ❌ これもコンパイルエラー!
dog.print(); // ✅ OK!publicだから
}
privateなメンバに外からアクセスしようとすると、コンパイルエラーになります。
実行する前にコンパイラが「ダメだって言ってるでしょ!」と怒ってくれるので安心ですね。
C言語では 300歳の犬が実行時まで(あるいは永遠に)発覚しなかったのに、C++ではコンパイル時点で不正なアクセスをブロックしてくれます。
じゃあ、privateなデータにはどうやってアクセスするの?
ここで登場するのが、getter(ゲッター) と setter(セッター) です。
名前の通り、get(取得する) と set(設定する) ための public な関数です。
この2つをまとめてアクセサ(accessor)と呼ぶこともあります。「privateなデータにアクセスするための窓口」というイメージですね。
class Dog {
private:
std::string _name;
unsigned int _age;
public:
// getter:データを読み取る
std::string get_name() {
return _name;
}
unsigned int get_age() {
return _age;
}
// setter:データを書き込む(ここにチェックを入れられる!)
void set_name(std::string new_name) {
_name = new_name;
}
void set_age(unsigned int new_age) {
if (new_age > 31) { // ← 犬の年齢として現実的な範囲かチェック
std::cout << "エラー:犬が30歳超えはさすがに…" << std::endl;
return;
}
_age = new_age;
}
};
ここがポイントです。unsigned intを使っているのでマイナスの値は型レベルで防げています。しかし「300歳の犬」のような意味的にありえない値は、型だけでは防げません。
setterの中にバリデーション(検証ロジック)を書くことで、型では守れない範囲まで守れるわけです。
int main() {
Dog dog;
// ❌ これはできない(privateだから)
dog._name = "ポチ";
dog._age = 300;
// ✅ setter経由でデータを設定
dog.set_name("ポチ");
dog.set_age(3); // OK
dog.set_age(300); // エラー:犬が30歳超えはさすがに…(セットされない)
// ✅ getter経由でデータを取得
std::cout << dog.get_name() << "は"
<< dog.get_age() << "歳です" << std::endl;
// 出力: ポチは3歳です
}
図にまとめるとこんな感じ。
ここまで読んだあなたは、もう冒頭のコードで「なんでわざわざ回りくどい書き方するの?」とは思わないはず。
privateで守って、アクセサ経由で操作する。回りくどいんじゃなくて、正しく守ってるんです。
堅牢性が~UP!
さて、カプセル化のメリットをもう少し深掘りしていきましょう。
カプセル化:なぜデータを隠すのか?
理由① 不正な値を防げる(型だけでは守りきれない)
振り返り学習です。
C言語は、「関数を使ってね」というお願いでしかありませんでした。
C++のprivate は「メンバ関数使わないと許さないよ」という強制です。この差はデカーい!
理由② バグの原因を特定しやすい
データがpublicだと、プロジェクト内のどこからでも値を変更できてしまいます。
例えば、連絡先の名前が突然空文字になるバグが発生したとします。
publicなら、プロジェクト中の全てのファイルでクラス名._name = ...を探し回る羽目になります。
privateなら、set_name()の呼び出し箇所だけ調べればOK。犯人捜しの範囲が劇的に狭まります。
理由③ 内部の構造を安全に変えられる
例えば、最初は犬の名前を1つの変数で持っていたけど、後から分けたくなったとします。
// 最初のバージョン
class Dog {
private:
std::string _name; // "ポチ太郎"
public:
std::string get_name() { return _name; }
};
// 改良バージョン:名前を分けたくなった
class Dog {
private:
std::string _first_name; // "ポチ"
std::string _last_name; // "太郎"
public:
std::string get_name() {
return _first_name + _last_name; // つなげて返す
}
};
get_name() を使っていた外部のコードは 一切変更不要。
privateのおかげで、内部構造の変更が外部に影響しないんです。
C言語だとこうはいきません。dog.name を直接使っていた箇所が全部壊れるので、プロジェクト中のdog.nameを一つ残らず書き換える羽目になります。
「やっぱりデータの持ち方変えたい」ってなった時に、getter/setterの中身だけ直せば済むのは…便利!
このように、データを隠して(privateにして)、 public な関数を通じてのみアクセスさせる設計を カプセル化(encapsulation) と呼びます。
薬のカプセルと同じイメージです。中身(private)は見えないけど、飲む(public関数を呼ぶ)だけで効く。
ここまでで、クラスの核心的な部分は押さえました。最後に、ちょっとした豆知識を。
C++におけるstructとclassの違い
さて、いかにも構造体(struct)はC言語にしかない…みたいな書き方をしてきましたが…実はC++でもstructは使えます。
メンバ関数もアクセス指定子も書けクラスとます。
クラスとほぼ同じ存在です。
唯一の違いは、**「何も書かなかった時(デフォルト)の状態」**です。
struct: デフォルトで public(C言語互換のため)
class: デフォルトで private(カプセル化を強制するため)
つまり、アクセス指定子を省略した場合にpublic扱いになるかprivate扱いになるかというだけ。
明示的にアクセス指定子を書けば、両者は全く同じ動作になります。
つまるところ、classをお薦めします。
classでもstructでも、アクセス指定子は省略せず、privateとpublicを明示的に書きましょう!
冒頭のコードを読み解く
記事の最初に出したコード、もう一度見てみましょう。
#include <iostream>
#include <string>
class Dog {
private:
std::string _name;
unsigned int _age;
public:
void set_name(std::string new_name) {
_name = new_name;
}
std::string get_name() {
return _name;
}
void set_age(unsigned int new_age) {
if (new_age > 31)
return;
_age = new_age;
}
unsigned int get_age() {
return _age;
}
};
int main() {
Dog dog;
dog.set_name("ポチ");
dog.set_age(3);
std::cout << dog.get_name() << "(" << dog.get_age() << "歳)" << std::endl;
}
もうバッチリ読めますよね?
privateでデータを隠し、アクセサ(getter/setter)を通じてのみ操作させる。
set_ageにはバリデーションが入っているから、300歳の仙犬は二度と爆誕しません。
「名前と年齢をセットして表示するだけの冗長なコード」に見えたのは、「カプセル化を使った堅牢性の高いコード」だったわけですね~。
まとめ
| C言語の構造体 | C++のクラス | ポイント |
|---|---|---|
| データのみ | データ+操作 | 関連するコードが1つにまとまる |
| 全メンバ公開 | public / private | アクセスを制御できる |
| 関数は外部 | メンバ関数 |
dog.print() のように呼べる |
| ― | カプセル化 | アクセサ(getter/setter)でデータを守る |
C言語からC++に移行する第2のステップは、「データの塊」から「データ+操作+アクセス制御」への考え方の切り替えでした。
最初は面倒に感じても、プロジェクトが大きくなるほど、カプセル化の恩恵を実感するはずですよ!
📖 この記事で出てきた用語をもっと詳しく知りたい方へ
「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典さんの記事が分かりやすいのでおすすめです。
この記事は 42Tokyo でのC++学習をもとに作成しました。
気づけば550行Overですよ。