2014年5月31日土曜日

C++ 例外




コンパイル時のエラーなら、実行できないので

修正箇所も明確です。

しかし、コンパイルしたファイルを実行すると

エラーになることもあります。

この実行時エラーが、例外(Exception)です。

例外は、ゼロ除算やメモリの2重解放、配列の範囲外アクセスなどによって

起こります。実務では、どこで起こったのかすぐにわからないこともよくあります。

このような例外に対応することを例外処理といいます。(^^;)

C++には、例外処理のための仕組みが用意されています。

try, catch文
例外が起こりそうな箇所にtry、例外処理を行う箇所にcatch文を記述します。

構文はこのような形式になります。

try {
    // 例外が起こりそうな、ちょっと自信がないプログラム
}
catch (int &e) {
    // 例外処理
  // 例えば、エラーが起きたというメッセージの表示
}

実行時は、まずtry { } の中を処理し、例外が起こらなかった場合は、

catch { } の中は実行せずに処理を継続します。

もし、try { } の中で例外が起こった場合は、catch { } の中を

実行して、処理継続となります。

ここで、catchの部分に着目すると、catch (int &e) となっています。

カッコの中で、例外の型(クラス名)、変数名(オブジェクト)を指定して

どのような例外を捕捉するのかを決めることができるのです。

それはつまり、指定を間違うと例外が発生しても catch 文を実行せず

そのままスルーなんて事にもなる、ということです...(^^;)

ちなみにすべての例外を受け取るには

catch ( ... ) {
    // 何かの例外処理
}

このように、「...」 ピリオドを3つ記述します。


throw文
処理に応じて、意図的に例外を通知させたいこともあると思います。

その場合は、次のようにthrow文を使って例外の内容を通知できます。

try {
    int Number;
   
    cout << "Please Input Number : ";
    cin >> Number;
    if (Number < 0) { throw -1; }
    cout << "Input Number : " << Number << endl;
}
catch (int error) {
    cout << "負の数はエラーです。エラーコード: " << error << endl;
}


では、今回はメモリをひたすら確保し続けるサンプルコードを使い

try, catch してみるという実験を以下に示します。

100MBメモリ確保して、文字 'a' で初期化を繰り返すプログラム
#include <iostream>
#include <new>        // bad_allocのcatchに必要です
#include <string.h>

using namespace std;

int main(void)
{
    try {
        // 100MB確保を繰り返す
        for (int i = 1; ; i++) {
            char *pStr = new char[104857600];
            cout << i * 100 << "MB" << endl;
            memset(pStr, 'a', 104857600);
        }
    }
    catch (bad_alloc &) {
        cout << "'new' threw an exception..." << endl;
        return -1;
    }

    return 0;
}

実行結果
# ./test
100MB
200MB
'new' threw an exception...
#

ただメモリを確保しただけだと、OSには実際は使っていない

ということが分かっているので仮想メモリを駆使して実メモリ以上を

確保しているように見えます。手元の環境では、3000MBまで到達(^^;)

そのため、memsetで初期化することで、使用していることをアピールします。

非力なマシンなので、すぐに力尽きてしまいました...


結局、何が言いたいのかというと、例外処理はバグの早期発見に役立つ、

品質の良いプログラムを作るための一機能であるということです。(^^)


今日の名言
時の与えるあらゆる利益を迅速に利用せよ。
                               ウィリアム・シェイクスピア

過去の過ちから役に立つ教訓を引き出すためと、高価な代償を払って得た利益を
得るためでない限り、決して過去を顧みるな。
                               ジョージ・ワシントン

憎しみは、他のどんなものよりもエネルギーを消耗する。その消耗のひどさは、
重労働よりも病気よりも、ちゃんと理由があって心配する場合よりも、はるかに
はなはだしい。だから憎しみの炎が自分の体の中へ入ってきたら、すぐさま
消すことだ。その代わりに美しい考えを入れてやろう。我々の精力は神から
授かった貴重なものだから、価値のある者だけに費やさねばならぬ。
                               デール・カーネギー

2014年5月17日土曜日

C++ ヘッダファイルの記述方法




今日は、ヘッダファイルの書き方について

の話になります。

今までは、短いサンプルということもあり、

一つのソースファイル(.cpp)の中に

クラスの定義を記述していました。

しかし、普通はクラスの定義はヘッダファイル(.h)に

記述し、メンバ関数の定義をソースファイル(.cpp)に記述します。

つまり、画像の例でいうと、Peach.h にこれを書いて、

class PeachBox {
public:
    PeachBox();
    ~PeachBox();
    int GetTotal();
private:
    int m_total;
};

Peach.cpp にはこれを書きます。

PeachBox::PeachBox():
m_total(0)
{
}

PeachBox::~PeachBox()
{
    std::cout << "桃の化粧箱オブジェクトが終了します。" << std::endl;
}

int PeachBox::GetTotal()
{
    return m_total;
}

int main(void)
{
    PeachBox myPeachBox;
    std::cout << "化粧箱の中の桃の数は" << myPeachBox.GetTotal() << "個です。" << std::endl;
    return 0;
}


さて、このようにヘッダファイルとソースファイルを分割したことによって、

例えばFood.hといった他のヘッダファイルでPeach.hをインクルードする、

あるいは Peach.cpp から Food.h をインクルードするといった、

少し規模が大きいプログラムでも便利に使えるようになります。

しかし、色々なファイルで #include を使っていると、結果的には同じヘッダファイルを

2度インクルードしてしまうこともあります。

この場合は、クラスを2度定義することになり、コンパイルエラーになります(-_-;)

試しにPeach.h では Food.h をインクルードしてみます。

#include "Food.h"

class PeachBox {
public:
    PeachBox();
    ~PeachBox();
    int GetTotal();

そして、Peach.cppを、こんな感じに変更します。

#include <iostream>
#include "Food.h"
#include "Peach.h"

PeachBox::PeachBox():
m_total(0)
{
}

実際やってみると、Food.hを2回インクルードしているので、もちろんエラーです(^^;)

ちなみに、Peach.h Peach.cpp は同じディレクトリにあるので

これでコンパイルできています。

# g++ -g -Wall -o Peach Peach.cpp
In file included from Peach.h:1:0,
                 from Peach.cpp:3:
Food.h:4:7: error: redefinition of ‘class Food’
Food.h:4:7: error: previous definition of ‘class Food’

こういう時のために、重複インクルードを防ぐための

#ifndef マクロ というプリプロセッサ命令があります(^^)

プリプロセッサとは、コンパイラに対してコンパイルする前に、

事前処理を行わせるためのプログラムのことです。

言葉だけでは、伝わらないので実際に使ってみます。

Food.h
ifndef __FOOD_H  // もし、__FOOD_Hが未定義なら
#define __FOOD_H  // 最初のインクルードで__FOOD_Hを定義します
             // こうすることで、2回目以降は既に__FOOD_Hが
            // 定義されているので、この内容は
            // インクルードされなくなります(*^^)v
#include <iostream>
#include <string.h>

class Food
{
public:
    static double sm_tax;
    void SetPrice(int price);
    int GetPrice(void);
private:
    int m_price;
};

#endif // __FOOD_H  ←通常、目印としてコメントを記述しておきます

Peach.h
#ifndef __PEACH_H
#define __PEACH_H

#include "Food.h"

class PeachBox {
public:
    PeachBox();
    ~PeachBox();
    int GetTotal();
private:
    int m_total;
};

#endif // __PEACH_H

Peach.cpp
#include <iostream>
#include "Food.h"
#include "Peach.h"

PeachBox::PeachBox():
m_total(12)
{
}

PeachBox::~PeachBox()
{
    std::cout << "桃の化粧箱オブジェクトが終了します。" << std::endl;
}

int PeachBox::GetTotal()
{
    return m_total;
}

int main(void)
{
    PeachBox myPeachBox;
    std::cout << "化粧箱の中の桃の数は" << myPeachBox.GetTotal() << "個です。" << std::endl;
    return 0;
}

コンパイルと実行結果
# g++ -g -Wall -o Peach Peach.cpp
# ./Peach
化粧箱の中の桃の数は12個です。
桃の化粧箱オブジェクトが終了します。

2重インクルードしているにもかかわらず、エラーにならず実行できました。

ただ Food.cpp を作成してメンバ関数を定義していないし、

Peach.cpp 内で使ってもいないので、

Food.h をインクルードする必要のない、

例によって全くおもしろみもないプログラムです...(^^;)

ここでは、重複インクルード防止のための方法として、

#ifndef, #define, #endif という構文を見てきました。

プリプロセッサを使いこなして、品質の良いプログラムを造っていきたいものです。


今日の名言
子供には過去も未来もない、だから現在を楽しむ - 大人はとてもそうはいかない。
                               ジャン・ド・ラ・ブリュイエール

明日をはじめる前に今日をすっかり終える。今日と明日との間に眠りの壁を置く。
これをやり遂げるには、節制の心がけが必要だ。
                               ラルフ・ワルド・エマーソン

ある者は過去の記憶を蒸し返して、我と我が身を苛み続ける。
ある者はまだ見ぬ罪におびえて、我と我が身を傷つける。
どちらも愚かきわまることだ。 - 過去はもはや関係がなく、未来はまだ来ぬ・・・。
                               セネカ

時の真の価値を知れ。引っ掴め。押さえつけろ。そしてその瞬間瞬間を楽しめ。
怠けず、だらけず、ぐずぐずするな。今日できることを明日まで伸ばすな。
                               チェスターフィールド卿