【C++】例外処理の全知識を5つの具体例でマスター!

C++での例外処理を学ぶイメージC++
この記事は約16分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

プログラミングではエラーが避けられない事象であり、特にC++のような高度なプログラミング言語では効率的なエラー処理が不可欠です。

この記事では、C++における例外処理の基本から応用方法までを初心者にも理解しやすく詳細に解説します。

例外処理を理解し適用することで、より堅牢で信頼性の高いプログラムを作成することが可能になります。

●C++における例外処理の基本

C++での例外処理は、プログラム実行中に発生する予期しないエラーに対処する方法です。

例外が発生するとプログラムの通常の流れを中断し、エラーを処理する専用のコードブロックに制御を移します。

このメカニズムにより、プログラムの可読性と保守性が向上し、エラーによる不意の動作やシステムクラッシュを防ぐことができます。

○例外処理とは

C++における例外処理では、「try」「catch」「throw」の3つのキーワードを中心に構成されます。

これらのキーワードを使って例外を投げ、捕捉し、例外が発生する可能性のあるコードを囲むことができます。

例外処理を適用することで、エラーが発生した際にプログラムが安全に終了するよう制御が可能になります。

○try、catch、throwの基本的な使い方

tryブロックでは、例外が発生する可能性があるコードを囲みます。

このブロック内で例外が発生すると、対応するcatchブロックで捕捉されます。

throwステートメントを使用して例外を発生させることができ、throwに続けて例外オブジェクトを指定することが可能です。

catchブロックでは、特定の型の例外を捕捉し、エラー処理を行うコードを記述します。

catchブロックは、tryブロックに続けて記述されます。

○例外クラスの概要

C++では例外を表すためにクラスが使用されます。

標準ライブラリには多くの例外クラスが含まれており、これらを直接使用するか、カスタムの例外クラスを作成することもできます。

例外クラスはエラーの原因や情報を格納するために使用され、throwステートメントで投げられます。

カスタムの例外クラスを作成することで、より詳細なエラー情報の提供や特定のエラー条件に特化した処理の実装が可能になります。

●例外処理の詳細な使い方

C++における例外処理の詳細な使い方を理解するためには、実際のサンプルコードを通してその概念と流れを学ぶことが効果的です。

ここでは、基本的なtry-catchブロックから始め、より複雑なネストされたtry-catchブロック、さらにはカスタム例外クラスの作成と使用まで、段階的に例外処理の様々な側面を探っていきます。

○サンプルコード1:基本的なtry-catchブロック

まず最も基本的な例外処理の形式であるtry-catchブロックの使用方法を見ていきます。

下記のサンプルコードでは、例外が発生しうるコードをtryブロック内に配置し、その例外をcatchブロックで捕捉しています。

#include <iostream>
using namespace std;

int main() {
    try {
        // 例外が発生する可能性があるコード
        throw "エラーが発生しました";
    } catch (const char* msg) {
        cerr << "例外が捕捉されました: " << msg << endl;
    }
    return 0;
}

このコードでは、throwステートメントを使って文字列の例外を発生させ、catchブロックでその例外を捕捉し、エラーメッセージを出力しています。

○サンプルコード2:ネストされたtry-catchブロック

次に、ネストされたtry-catchブロックの例を見てみましょう。

これは、一つのtryブロック内に別のtryブロックを含めることができる構造です。

このような構造は、複数のエラー源に対応する必要がある場合に有用です。

#include <iostream>
using namespace std;

void nestedFunction() {
    try {
        throw "内部のエラー";
    } catch (const char* msg) {
        cerr << "内部の例外: " << msg << endl;
        throw; // 再度例外を投げる
    }
}

int main() {
    try {
        nestedFunction();
    } catch (const char* msg) {
        cerr << "外部の例外: " << msg << endl;
    }
    return 0;
}

このサンプルコードでは、nestedFunction関数内で発生した例外を一度捕捉してエラーメッセージを出力した後、再度外側のtryブロックに例外を投げています。

○サンプルコード3:カスタム例外クラスの作成と使用

最後に、独自の例外クラスを定義し、それを使用する方法を見ていきます。

カスタム例外クラスを作成することで、特定の種類のエラー情報をより詳細に表現できるようになります。

#include <iostream>
#include <exception>
using namespace std;

// 独自の例外クラスを定義
class MyException : public exception {
    virtual const char* what() const throw() {
        return "独自の例外が発生しました";
    }
};

int main() {
    try {
        throw MyException();
    } catch (MyException& e) {
        cerr << "例外が捕捉されました: " << e.what() << endl;
    }
    return 0;
}

このコードでは、std::exceptionクラスを継承したMyExceptionクラスを定義し、whatメソッドをオーバーライドしています。

main関数内でMyExceptionを投げ、catchブロックでその例外を捕捉しています。

●例外処理の応用例

C++における例外処理は、さまざまな応用が可能です。

特に、ファイル操作やネットワークプログラミングにおいては、予期せぬエラーが発生しやすいため、効果的な例外処理が重要になります。

ここでは、これらの具体的なシナリオにおける例外処理の応用例をサンプルコードと共に見ていきます。

○サンプルコード4:ファイル操作における例外処理

ファイル操作では、ファイルが存在しない、アクセス権限がないなど、様々な理由でエラーが発生する可能性があります。

下記のサンプルコードでは、ファイルを開く際の例外処理を表しています。

#include <iostream>
#include <fstream>
#include <exception>
using namespace std;

int main() {
    ifstream file;
    file.exceptions(ifstream::failbit | ifstream::badbit);

    try {
        file.open("example.txt");
        // ファイル操作
    } catch (const ifstream::failure& e) {
        cerr << "ファイル操作で例外が発生しました: " << e.what() << endl;
    }

    file.close();
    return 0;
}

このコードでは、ifstreamオブジェクトの例外フラグを設定し、ファイルを開く際にエラーが発生すると自動的に例外が投げられるようにしています。

例外が捕捉されると、エラーメッセージが出力されます。

○サンプルコード5:ネットワークプログラミングにおける例外処理

ネットワークプログラミングでは、接続失敗やタイムアウトなど、多くの例外状況が考えられます。

下記のサンプルコードでは、ネットワーク接続時の例外処理を表しています。

#include <iostream>
#include <exception>
// ネットワーク関連のヘッダー(例: <asio.hpp>)

using namespace std;

int main() {
    try {
        // ネットワーク接続の初期化
        // 接続処理
    } catch (const std::exception& e) {
        cerr << "ネットワークエラー: " << e.what() << endl;
    }

    return 0;
}

このコードでは、ネットワーク接続に関する処理をtryブロック内に記述し、何らかの例外が発生した場合にはcatchブロックでエラーメッセージを出力しています。

●例外処理の詳細な対処法

例外処理は、C++プログラミングにおいて避けて通れない要素です。

エラーが発生したとき、それを適切に処理することで、プログラムの安定性と信頼性を高めることができます。

ここでは、エラーメッセージの適切な取り扱い方法と、例外の伝播とその処理方法について詳しく解説します。

○エラーメッセージの適切な取り扱い

プログラム内でエラーが発生した場合、ユーザーにわかりやすいエラーメッセージを提供することが重要です。

エラーメッセージは、エラーの原因を特定し、問題を解決する手がかりを提供します。

下記のサンプルコードは、エラーメッセージの取り扱いを表しています。

#include <iostream>
#include <stdexcept>

int main() {
    try {
        // エラーを発生させる処理
        throw std::runtime_error("エラー発生");
    } catch (const std::exception& e) {
        std::cerr << "エラー捕捉: " << e.what() << std::endl;
    }
    return 0;
}

このコードでは、std::runtime_errorを使用してエラーを発生させ、catchブロックでそのエラーを捕捉しています。

e.what()は、エラーの詳細な説明を提供します。

○例外の伝播と処理

例外処理では、捕捉された例外を再度投げる(伝播させる)ことが可能です。

これにより、エラーを処理する複数のレイヤーを実装することができます

下記のサンプルコードは、例外の伝播と処理の一例を表しています。

#include <iostream>
#include <stdexcept>

void functionA() {
    throw std::runtime_error("functionAでエラー発生");
}

void functionB() {
    try {
        functionA();
    } catch (const std::exception&) {
        std::cerr << "functionBでエラー捕捉" << std::endl;
        throw; // 例外を再投げる
    }
}

int main() {
    try {
        functionB();
    } catch (const std::exception& e) {
        std::cerr << "mainでエラー捕捉: " << e.what() << std::endl;
    }
    return 0;
}

このコードでは、functionAで発生した例外をfunctionBで捕捉し、その後main関数で再び捕捉しています。

このように例外を伝播させることで、エラーの処理を階層化し、より柔軟なエラー処理戦略を実装することが可能になります。

●例外処理における注意点

例外処理を実装する際には、特に注意が必要な点がいくつかあります。

これらの注意点を理解し、適切に対処することで、プログラムの安定性と効率性を高めることができます。

ここでは、リソースリークの防止とパフォーマンスへの影響に焦点を当てて解説します。

○リソースリークの防止

例外処理を誤って行うと、リソースリークが発生する可能性があります。

リソースリークは、プログラムが使用していたリソース(メモリ、ファイルハンドル、ネットワーク接続など)が適切に解放されずに残ってしまう状態を指します。

特に、例外が発生した際にリソースが解放されない場合、メモリリークなどの問題が生じる可能性があります。

下記のサンプルコードは、リソースリークを防ぐための一例です。

#include <iostream>
#include <memory>

void processFile(const std::string& fileName) {
    std::unique_ptr<std::FILE, decltype(&std::fclose)> filePtr(std::fopen(fileName.c_str(), "r"), &std::fclose);
    if (!filePtr) {
        throw std::runtime_error("ファイルを開けませんでした");
    }
    // ファイル処理
}

int main() {
    try {
        processFile("example.txt");
    } catch (const std::exception& e) {
        std::cerr << "エラー: " << e.what() << std::endl;
    }
    return 0;
}

このコードでは、std::unique_ptrを使用してファイルハンドルを管理しています。

例外が発生した場合でも、unique_ptrのデストラクタによってファイルハンドルが自動的に閉じられ、リソースリークを防ぐことができます。

○パフォーマンスへの影響

例外処理は便利ですが、パフォーマンスに影響を与えることもあります。

例外を投げる(throw)操作は、通常の関数呼び出しよりもコストが高くなり得るため、不必要に頻繁に使用することは避けるべきです。

例外は本当に例外的な状況、つまり回復が困難なエラーが発生した場合にのみ使用するのが適切です。

例外処理の使用を過度に避けることなく、しかし必要な場合にのみ利用するバランスを取ることが重要です。

●例外処理のカスタマイズ方法

C++プログラミングにおいて、例外処理をカスタマイズすることは、より洗練されたエラー管理を実現する上で非常に有効です。

カスタマイズされた例外処理は、特定のエラー状況に応じたより詳細な情報を提供し、エラーの原因を特定しやすくすることができます。

ここでは、カスタム例外クラスの作成と、例外処理ロジックの再利用について詳しく見ていきます。

○カスタム例外クラスの拡張

C++では、標準の例外クラスを継承して、独自の例外クラスを作成することができます。

これにより、特定のエラーに特化した情報を持たせることが可能になります。

下記のサンプルコードは、カスタム例外クラスの作成方法を表しています。

#include <iostream>
#include <exception>

// 独自の例外クラスを定義
class MyCustomException : public std::exception {
    const char* what() const noexcept override {
        return "カスタム例外が発生しました";
    }
};

void someFunction() {
    throw MyCustomException();
}

int main() {
    try {
        someFunction();
    } catch (const MyCustomException& e) {
        std::cerr << "エラー発生: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "標準例外: " << e.what() << std::endl;
    }
    return 0;
}

このコードではMyCustomExceptionクラスを定義し、what()メソッドをオーバーライドしています。

これにより、この例外が投げられたときには、カスタマイズされたエラーメッセージが表示されます。

○例外処理ロジックの再利用

例外処理のロジックは、適切に設計されていれば、プログラムの異なる部分で再利用することができます。

これにより、コードの重複を避け、メンテナンス性を高めることができます。例外処理の再利用は、関数やクラスメソッドなどを通じて実現されます。

下記のサンプルコードは、例外処理の再利用の一例を表しています。

#include <iostream>
#include <exception>

void handleError() {
    try {
        throw;
    } catch (const std::runtime_error& e) {
        std::cerr << "ランタイムエラー: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "一般的なエラー: " << e.what() << std::endl;
    }
}

void functionA() {
    try {
        // エラーを発生させる処理
        throw std::runtime_error("functionAのエラー");
    } catch (...) {
        handleError();
    }
}

int main() {
    try {
        functionA();
    } catch (...) {
        handleError();
    }
    return 0;
}

このコードでは、handleError関数を定義して、様々な種類のエラーを処理するロジックを集約しています。

これにより、functionAmain関数内で発生したエラーを共通の方法で処理することができます。

まとめ

この記事では、C++における例外処理の基本から応用、さらにはカスタマイズ方法に至るまでを詳細に解説しました。

基本的なtry-catchブロックから始まり、カスタム例外クラスの作成、例外処理ロジックの再利用に至るまで、各段階での注意点と効果的な対処法を紹介しました。

例外処理はC++プログラミングの不可欠な部分であり、この知識を身につけることで、より堅牢で信頼性の高いソフトウェア開発が可能になります。

効果的な例外処理の実装は、初心者から上級者までのC++プログラマーにとって重要なスキルです。