C++のクラスとは:基礎から応用まで
クラスとオブジェクトの基本概念
クラスは、C++におけるオブジェクト指向プログラミングの中心的な概念です。クラスは、データ(メンバ変数)とその操作方法(メンバ関数)を1つのユニットとしてまとめた設計図のようなものです。オブジェクトは、そのクラスから生成された実体です。
例えば、以下のような単純なCar
クラスを考えてみましょう:
class Car { private: string brand; // 車のブランド string model; // モデル名 int year; // 製造年 public: // コンストラクタ Car(string b, string m, int y) : brand(b), model(m), year(y) {} // メンバ関数 void displayInfo() { cout << year << " " << brand << " " << model << endl; } }; // オブジェクトの生成と使用 Car myCar("Toyota", "Corolla", 2024); myCar.displayInfo(); // 出力: 2024 Toyota Corolla
なぜC++でクラスを使うのか
クラスを使用する主な利点は以下の通りです:
- カプセル化: データと操作を1つのユニットにまとめることで、コードの管理が容易になります。
- データ隠蔽: privateメンバを使用することで、クラスの内部データを外部から保護できます。
- 再利用性: 一度定義したクラスは、異なる場所で何度でも使用できます。
- 保守性: クラスの実装を変更しても、そのインターフェースを使用している他のコードに影響を与えません。
構造体との違いと使い分け
C++における構造体(struct)とクラス(class)の主な違いは、デフォルトのアクセス指定子にあります:
struct Point { int x; // デフォルトでpublic int y; // デフォルトでpublic }; class Rectangle { int width; // デフォルトでprivate int height; // デフォルトでprivate public: void setDimensions(int w, int h) { width = w; height = h; } };
使い分けの一般的なガイドライン:
特徴 | struct | class |
---|---|---|
主な用途 | データの単純なグループ化 | データと振る舞いの複雑な組み合わせ |
カプセル化 | 最小限 | 完全なカプセル化 |
継承 | 通常は使用しない | 頻繁に使用 |
メンバ関数 | 少ないまたはなし | 多数の場合が多い |
実際の開発では、単純なデータ構造を表現する場合はstructを、複雑な振る舞いを持つオブジェクトを表現する場合はclassを使用することが推奨されます。ただし、技術的にはstructでもclassでも同じことが実現可能です。
クラスの作成と実装:7つの基本ステップ
ステップ1:クラスの宣言と定義
クラスの宣言は、通常ヘッダーファイル(.h)で行い、定義は実装ファイル(.cpp)で行います。この分離により、コードの管理が容易になります。
// Person.h class Person { private: string name; int age; public: Person(string name, int age); // コンストラクタの宣言 void displayInfo(); // メンバ関数の宣言 }; // Person.cpp #include "Person.h" Person::Person(string name, int age) : name(name), age(age) {} void Person::displayInfo() { cout << "Name: " << name << ", Age: " << age << endl; }
ステップ2:メンバ変数の設計
メンバ変数の設計では、以下の点に注意が必要です:
- 適切なデータ型の選択
class BankAccount { private: string accountNumber; // 口座番号 double balance; // 残高(浮動小数点数) unsigned int userId; // ユーザーID(負の値を取らない) vector<Transaction> transactions; // トランザクション履歴 };
- アクセス修飾子の適切な使用
- private: クラス内部からのみアクセス可能
- protected: 派生クラスからもアクセス可能
- public: どこからでもアクセス可能
- const修飾子の活用
class Configuration { private: const string VERSION = "1.0.0"; // 変更不可能な定数 static const int MAX_USERS = 100; // クラス全体で共有される定数 };
ステップ3:コンストラクタの実装
コンストラクタは、オブジェクトの初期化を担当します:
class Student { private: string name; int grade; vector<int> scores; public: // デフォルトコンストラクタ Student() : name(""), grade(1), scores() {} // パラメータ付きコンストラクタ Student(string n, int g) : name(n), grade(g) {} // コピーコンストラクタ Student(const Student& other) : name(other.name), grade(other.grade), scores(other.scores) {} };
ステップ4:デストラクタの実装
デストラクタは、オブジェクトが破棄される際のクリーンアップ処理を担当します:
class FileHandler { private: FILE* file; char* buffer; public: FileHandler(const char* filename) { file = fopen(filename, "r"); buffer = new char[1024]; } ~FileHandler() { if (file) { fclose(file); } delete[] buffer; // メモリリークを防ぐ } };
ステップ5:メンバ関数の追加
メンバ関数は、クラスの振る舞いを定義します:
class Rectangle { private: double width; double height; public: // ゲッター double getWidth() const { return width; } double getHeight() const { return height; } // セッター void setWidth(double w) { if (w > 0) width = w; } void setHeight(double h) { if (h > 0) height = h; } // ユーティリティ関数 double getArea() const { return width * height; } double getPerimeter() const { return 2 * (width + height); } // 状態チェック関数 bool isSquare() const { return width == height; } };
ステップ6:アクセス指定子の適切な使用
アクセス指定子を使って、カプセル化を実現します:
class Employee { private: // 内部データは隠蔽 string name; double salary; int performanceScore; protected: // 派生クラスからアクセス可能 void updatePerformanceScore(int score) { performanceScore = score; } public: // 外部からアクセス可能なインターフェース Employee(string n, double s) : name(n), salary(s), performanceScore(0) {} void giveRaise(double percentage) { if (performanceScore >= 8) { salary *= (1 + percentage/100); } } };
ステップ7:インスタンス化とメモリ管理
オブジェクトの生成と管理には、以下の方法があります:
// スタック上のオブジェクト Rectangle rect1; // デフォルトコンストラクタ Rectangle rect2(10.0, 20.0); // パラメータ付きコンストラクタ // ヒープ上のオブジェクト Rectangle* rect3 = new Rectangle(); // 動的メモリ割り当て delete rect3; // メモリの解放 // スマートポインタの使用(推奨) #include <memory> unique_ptr<Rectangle> rect4 = make_unique<Rectangle>(15.0, 25.0); // 自動的にメモリ解放されるため、delete不要
メモリ管理のベストプラクティス:
- 可能な限りスマートポインタを使用する
- RAIIパターンを採用する
- メモリリークを防ぐため、デストラクタで適切なクリーンアップを行う
- コピーとムーブの意味論を理解し、必要に応じて実装する
クラスの高度な機能と活用方法
継承とポリモーフィズムの実践的な使い方
継承とポリモーフィズムは、コードの再利用性と拡張性を高める強力な機能です:
// 基底クラス class Shape { protected: string color; public: Shape(string c) : color(c) {} // 純粋仮想関数 virtual double getArea() const = 0; // 仮想デストラクタ(重要) virtual ~Shape() {} // 共通の機能 virtual void draw() const { cout << "Drawing a " << color << " shape" << endl; } }; // 派生クラス class Circle : public Shape { private: double radius; public: Circle(string c, double r) : Shape(c), radius(r) {} // 純粋仮想関数のオーバーライド double getArea() const override { return M_PI * radius * radius; } // draw関数のオーバーライド void draw() const override { cout << "Drawing a " << color << " circle with radius " << radius << endl; } }; // ポリモーフィズムの活用例 void processShape(const Shape& shape) { cout << "Area: " << shape.getArea() << endl; shape.draw(); }
フレンド関数とフレンドクラスの活用
フレンド機能は、カプセル化を維持しながら特定のクラスや関数にアクセス権を与えます:
class Complex { private: double real; double imag; public: Complex(double r, double i) : real(r), imag(i) {} // フレンド関数の宣言 friend Complex operator+(const Complex& a, const Complex& b); // フレンドクラスの宣言 friend class ComplexCalculator; }; // フレンド関数の定義 Complex operator+(const Complex& a, const Complex& b) { return Complex(a.real + b.real, a.imag + b.imag); } // フレンドクラスの定義 class ComplexCalculator { public: static double getMagnitude(const Complex& c) { // privateメンバに直接アクセス可能 return sqrt(c.real * c.real + c.imag * c.imag); } };
静的メンバの効果的な使用方法
静的メンバは、クラス全体で共有される要素を実現します:
class Database { private: static Database* instance; // シングルトンインスタンス static int connectionCount; // 接続数のカウンタ string connectionString; // プライベートコンストラクタ(シングルトンパターン) Database() : connectionString("default") {} public: // 静的メンバ関数 static Database* getInstance() { if (!instance) { instance = new Database(); } return instance; } static int getConnectionCount() { return connectionCount; } void connect() { connectionCount++; cout << "Connected. Total connections: " << connectionCount << endl; } void disconnect() { if (connectionCount > 0) connectionCount--; cout << "Disconnected. Total connections: " << connectionCount << endl; } }; // 静的メンバの初期化 Database* Database::instance = nullptr; int Database::connectionCount = 0;
静的メンバの主な用途:
用途 | 例 |
---|---|
グローバルな状態管理 | インスタンス数のカウント、設定値の共有 |
シングルトンパターン | データベース接続、ロギングシステム |
ユーティリティ関数 | 数学計算、文字列操作 |
定数の定義 | バージョン番号、設定値 |
使用上の注意点:
- 静的メンバは全インスタンスで共有されるため、スレッドセーフティに注意が必要
- 静的メンバの初期化順序は保証されないため、初期化の依存関係に注意
- 過度な使用は状態管理を複雑にする可能性がある
実践的なクラス設計のベストプラクティス
カプセル化による堅牢なクラス設計
カプセル化は、クラスの内部実装を隠蔽し、安全で保守性の高いコードを実現します。
class BankAccount { private: string accountNumber; double balance; vector<string> transactionHistory; // 内部検証メソッド bool isValidTransaction(double amount) const { return amount > 0 && (amount <= balance || amount < 1000000); } public: BankAccount(string accNum, double initialBalance) : accountNumber(accNum), balance(initialBalance) {} // 安全な公開インターフェース bool deposit(double amount) { if (!isValidTransaction(amount)) return false; balance += amount; transactionHistory.push_back("Deposit: " + to_string(amount)); return true; } bool withdraw(double amount) { if (!isValidTransaction(amount)) return false; balance -= amount; transactionHistory.push_back("Withdrawal: " + to_string(amount)); return true; } // イミュータブルな情報の提供 double getBalance() const { return balance; } vector<string> getTransactionHistory() const { return transactionHistory; } };
コピーコンストラクタとムーブコンストラクタの実装
リソース管理を適切に行うための実装例:
class ResourceManager { private: int* data; size_t size; public: // 通常のコンストラクタ ResourceManager(size_t n) : size(n) { data = new int[size]; } // コピーコンストラクタ(ディープコピー) ResourceManager(const ResourceManager& other) : size(other.size) { data = new int[size]; std::copy(other.data, other.data + size, data); } // ムーブコンストラクタ ResourceManager(ResourceManager&& other) noexcept : data(other.data), size(other.size) { other.data = nullptr; // 元のオブジェクトからリソースを移動 other.size = 0; } // コピー代入演算子 ResourceManager& operator=(const ResourceManager& other) { if (this != &other) { delete[] data; // 既存のリソースを解放 size = other.size; data = new int[size]; std::copy(other.data, other.data + size, data); } return *this; } // ムーブ代入演算子 ResourceManager& operator=(ResourceManager&& other) noexcept { if (this != &other) { delete[] data; // 既存のリソースを解放 data = other.data; size = other.size; other.data = nullptr; other.size = 0; } return *this; } // デストラクタ ~ResourceManager() { delete[] data; } };
メモリリークを防ぐための設計パターン
- RAIIパターンの活用
class FileWrapper { private: std::unique_ptr<FILE, decltype(&fclose)> file; public: FileWrapper(const char* filename) : file(fopen(filename, "r"), fclose) { if (!file) { throw runtime_error("Failed to open file"); } } // ファイルの自動クローズ(デストラクタで実行) };
- スマートポインタの活用
class Component { private: // 一意の所有権 unique_ptr<Resource> uniqueResource; // 共有所有権 shared_ptr<Cache> sharedCache; // 循環参照防止 weak_ptr<Parent> parent; public: Component() : uniqueResource(make_unique<Resource>()) , sharedCache(make_shared<Cache>()) {} void setParent(shared_ptr<Parent> p) { parent = p; // 弱参照として保持 } };
メモリ管理のベストプラクティス:
原則 | 実装方法 |
---|---|
リソースの所有権明確化 | スマートポインタの使用 |
例外安全性の確保 | RAIIパターンの採用 |
メモリリークの防止 | デストラクタでの適切な解放 |
循環参照の回避 | weak_ptrの使用 |
これらの設計パターンを適切に組み合わせることで、メモリリークのない堅牢なクラス設計が可能になります。
よくあるクラスの実装ミスと解決方法
メモリ管理に関する一般的な問題
- メモリリーク
問題のあるコード:
class ResourceHolder { private: int* data; public: ResourceHolder() { data = new int[100]; // メモリ確保 } // デストラクタの実装忘れ }; // メモリリークが発生
正しい実装:
class ResourceHolder { private: unique_ptr<int[]> data; // スマートポインタを使用 public: ResourceHolder() : data(make_unique<int[]>(100)) {} // デストラクタは自動的に処理される };
- 浅いコピーの問題
問題のあるコード:
class Buffer { private: char* data; public: Buffer(size_t size) { data = new char[size]; } // コピーコンストラクタの未実装 // → デフォルトの浅いコピーで問題発生 };
正しい実装:
class Buffer { private: unique_ptr<char[]> data; size_t size; public: Buffer(size_t s) : data(make_unique<char[]>(s)), size(s) {} // ディープコピーの実装 Buffer(const Buffer& other) : size(other.size) { data = make_unique<char[]>(size); memcpy(data.get(), other.data.get(), size); } };
継承設計での注意点
- 仮想デストラクタの欠如
問題のあるコード:
class Base { public: ~Base() {} // 非仮想デストラクタ }; class Derived : public Base { private: int* resource; public: Derived() : resource(new int[10]) {} ~Derived() { delete[] resource; } };
正しい実装:
class Base { public: virtual ~Base() {} // 仮想デストラクタ }; class Derived : public Base { private: unique_ptr<int[]> resource; public: Derived() : resource(make_unique<int[]>(10)) {} };
- オーバーライドの誤り
class Shape { public: virtual double getArea() const { return 0; } }; class Circle : public Shape { private: double radius; public: // constの欠如により、オーバーライドではなく新しいメソッドになる double getArea() { return M_PI * radius * radius; } };
正しい実装:
class Shape { public: virtual double getArea() const = 0; // 純粋仮想関数 }; class Circle : public Shape { private: double radius; public: double getArea() const override { // override指定子で誤りを防ぐ return M_PI * radius * radius; } };
パフォーマンス最適化のテクニック
- 不必要なコピーの回避
// パフォーマンス低下の例 void processData(vector<int> data) { // 値渡し // 処理 } // 最適化された実装 void processData(const vector<int>& data) { // const参照渡し // 処理 }
- メモリ割り当ての最適化
class OptimizedContainer { private: vector<int> data; public: OptimizedContainer(size_t expectedSize) { data.reserve(expectedSize); // メモリの事前確保 } void addItems(const vector<int>& newItems) { data.insert( data.end(), newItems.begin(), newItems.end() ); // 効率的な一括挿入 } };
主な最適化のポイント:
項目 | 推奨される方法 |
---|---|
大きなオブジェクトの受け渡し | const参照を使用 |
メモリ割り当て | 事前確保と再利用 |
コピーの削減 | ムーブセマンティクスの活用 |
キャッシュ効率 | データの局所性を考慮 |
実践演習:シンプルなクラス実装から始める
基本的な図形クラスの実装例
シンプルな図形クラス階層を実装して、C++クラスの基本概念を学びましょう:
#include <iostream> #include <cmath> #include <memory> #include <vector> // 抽象基底クラス class Shape { protected: std::string color; public: Shape(const std::string& c) : color(c) {} virtual ~Shape() = default; virtual double getArea() const = 0; virtual double getPerimeter() const = 0; virtual void draw() const { std::cout << "Drawing a " << color << " shape" << std::endl; } }; // 円クラス class Circle : public Shape { private: double radius; public: Circle(const std::string& color, double r) : Shape(color), radius(r) {} double getArea() const override { return M_PI * radius * radius; } double getPerimeter() const override { return 2 * M_PI * radius; } void draw() const override { std::cout << "Drawing a " << color << " circle with radius " << radius << std::endl; } }; // 長方形クラス class Rectangle : public Shape { private: double width; double height; public: Rectangle(const std::string& color, double w, double h) : Shape(color), width(w), height(h) {} double getArea() const override { return width * height; } double getPerimeter() const override { return 2 * (width + height); } void draw() const override { std::cout << "Drawing a " << color << " rectangle " << width << "x" << height << std::endl; } }; // 使用例 void demonstrateShapes() { std::vector<std::unique_ptr<Shape>> shapes; shapes.push_back(std::make_unique<Circle>("red", 5)); shapes.push_back(std::make_unique<Rectangle>("blue", 4, 6)); for (const auto& shape : shapes) { shape->draw(); std::cout << "Area: " << shape->getArea() << std::endl; std::cout << "Perimeter: " << shape->getPerimeter() << std::endl; } }
銀行口座クラスの実装例
実践的な銀行口座管理システムの基本実装:
#include <iostream> #include <string> #include <vector> #include <stdexcept> #include <chrono> class Transaction { private: std::string type; double amount; std::chrono::system_clock::time_point timestamp; public: Transaction(const std::string& t, double amt) : type(t), amount(amt), timestamp(std::chrono::system_clock::now()) {} std::string getType() const { return type; } double getAmount() const { return amount; } }; class BankAccount { private: std::string accountNumber; std::string ownerName; double balance; std::vector<Transaction> transactions; // 入力検証 bool isValidAmount(double amount) const { return amount > 0; } public: BankAccount(const std::string& number, const std::string& owner, double initialBalance = 0.0) : accountNumber(number), ownerName(owner), balance(initialBalance) { if (initialBalance < 0) { throw std::invalid_argument("Initial balance cannot be negative"); } } // 預金処理 void deposit(double amount) { if (!isValidAmount(amount)) { throw std::invalid_argument("Invalid deposit amount"); } balance += amount; transactions.emplace_back("Deposit", amount); } // 引き出し処理 bool withdraw(double amount) { if (!isValidAmount(amount) || amount > balance) { return false; } balance -= amount; transactions.emplace_back("Withdrawal", amount); return true; } // 残高照会 double getBalance() const { return balance; } // 取引履歴の表示 void printStatement() const { std::cout << "Account Statement for " << ownerName << std::endl; std::cout << "Account Number: " << accountNumber << std::endl; std::cout << "Current Balance: " << balance << std::endl; std::cout << "\nTransaction History:" << std::endl; for (const auto& transaction : transactions) { std::cout << transaction.getType() << ": " << transaction.getAmount() << std::endl; } } }; // 使用例 void demonstrateBankAccount() { try { BankAccount account("1234-5678", "John Doe", 1000.0); account.deposit(500.0); account.withdraw(200.0); account.deposit(1000.0); account.withdraw(1500.0); account.printStatement(); } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; } }
ゲームキャラクタークラスの実装例
シンプルなRPGキャラクターシステムの実装:
#include <iostream> #include <string> #include <vector> #include <memory> #include <algorithm> // 装備アイテムの基底クラス class Equipment { protected: std::string name; int power; public: Equipment(const std::string& n, int p) : name(n), power(p) {} virtual ~Equipment() = default; virtual int getPower() const { return power; } std::string getName() const { return name; } }; // キャラクタークラス class Character { private: std::string name; int level; int health; int maxHealth; std::vector<std::unique_ptr<Equipment>> equipment; public: Character(const std::string& n) : name(n), level(1), health(100), maxHealth(100) {} // 装備の追加 void addEquipment(std::unique_ptr<Equipment> item) { equipment.push_back(std::move(item)); } // 戦闘力の計算 int getPower() const { int totalPower = level * 10; // 基本戦闘力 for (const auto& item : equipment) { totalPower += item->getPower(); } return totalPower; } // レベルアップ void levelUp() { level++; maxHealth += 20; health = maxHealth; std::cout << name << " leveled up to " << level << "!" << std::endl; } // ステータス表示 void showStatus() const { std::cout << "\nCharacter Status:" << std::endl; std::cout << "Name: " << name << std::endl; std::cout << "Level: " << level << std::endl; std::cout << "Health: " << health << "/" << maxHealth << std::endl; std::cout << "Power: " << getPower() << std::endl; std::cout << "\nEquipment:" << std::endl; for (const auto& item : equipment) { std::cout << "- " << item->getName() << " (Power: " << item->getPower() << ")" << std::endl; } } }; // 使用例 void demonstrateCharacter() { Character hero("Hero"); hero.addEquipment(std::make_unique<Equipment>("Sword", 15)); hero.addEquipment(std::make_unique<Equipment>("Shield", 10)); hero.showStatus(); hero.levelUp(); hero.showStatus(); }
これらの実装例は、以下の重要な概念を実践的に示しています:
概念 | 実装例での使用 |
---|---|
カプセル化 | privateメンバ変数とpublicインターフェース |
継承 | Shape基底クラスと派生クラス |
ポリモーフィズム | 仮想関数とオーバーライド |
メモリ管理 | スマートポインタの使用 |
例外処理 | 入力検証と例外スロー |
コレクション | vector を使用したデータ管理 |