まずC++を使いこなすにはまずCを知っている必要があります。C++基本文法はCであり、それをオブジェクト指向の概念に対応できるように機能追加したのがC++だからです。もちろんC++になって不要となったCの機能も少しはありますが、微々たる物ですから、まずはCの理解が欠かせません。まずはCをある程度は理解してください。
そして次ぎに徹底的にCで培ったプログラミングスタイルを捨て去る必要があります。特に長年Cに親しんだ人ほどCに執着してしまいC++に移れない傾向があります(私がそうでした)。思いきって捨て去ってください。オブジェクト指向プログラミング(OOP)は今までとは全く違う角度からのアプローチです。思い切って頭を切り替える必要があります。
C++の話に入る前にCで巨大なプログラムを組むときのことをおさらいしておきましょう。
巨大なプログラムを組む場合、それを細かな単位に分割してそれぞれをブラックボックス化してインターフェースを決めて、個別に開発していくという方法を取りますよね。もっとも簡単なのは、ひとつのモジュールをひとつのファイルにしてインターフェースとなる関数や変数をグローバルにして、ヘッダファイルにプロトタイプ宣言やextern宣言し、モジュール内部で使う関数や変数を static 属性を付けてモジュール内で宣言しますね。
しかし、この方法にはさまざまな問題があります。たとえばスタックを実現するモジュールを作ったとしましょう(以下即興で打ちこんでいるのでそのままでは動かないかも(爆死))。
#ifndef __STACK_H_ #define __STACK_H_ void InitStack(int nSize); /* スタックの初期化 */ void EndStack(void); /* スタックの使用終了 */ void Push(int data); /* データの格納 */ int Pop(void); /* データの取り出し */ #endif /* __STACK_H_ */
#include <stdio.h> #include <stdlib.h> #include "stack.h" static int *pStack = NULL; /* スタック用データ */ static int nStackSize; /* スタックのサイズ */ static int nStackPtr; /* スタック用のポインタ */ static void TrapErr(void); /* エラーを処理 */ /* スタックの初期化 */ void InitStack(int nSize) { /* すでに使用中であればエラー */ if ( pStack != NULL ) ErrTrap(); /* 初期化 */ pStack = (int *)malloc(nSize * sizeof(int)); nStackSize = nSize; nStackPtr = 0; } /* スタックの使用終了 */ void EndStack(void) { /* メモリの開放 */ free(pStack); pStack = NULL; } /* スタックにデータを格納 */ void Push(int data) { /* エラーチェック */ if ( nStackPtr >= nSize ) TrapErr(); /* スタックにデータの格納する */ Stack[nStackPtr++] = data; } /* スタックからデータを取り出す */ int pop(void) { /* エラーチェック */ if ( nStackPtr <= 0 ) TrapErr(); /* スタックからデータを取り出す */ return Stack[--nStackPtr]; } /* エラーを処理 */ static void TrapErr(void) { fprintf(stderr, "Stack Err\n"); EndStack(); exit(1); }
まあ上記のプログラムではスタックを実現するためのインターフェースを stack.h で定義して、実体を stack.c で書いているわけです。インターフェースさえ変えなければ実体を書きなおしても良いわけで、この時点でこのスタックのモジュールは外部からはブラックボックス化されたわけです。外からは static な変数や関数にはアクセスできません。多くの場合はこれでも何とかなりました。がこれでは問題もあります。それは「スタックが2つ以上欲しくなったらどうするか?」です。関数や変数の名前だけ変えてもうひとつ同じものを作りますか? そんなことをすればコードサイズも増えますし、何のためにブラックボックス化したのか分からず愚の骨頂ですね。
ではどうするかというとすかさずハンドルを作ってしまいます。ハンドルとは操作の対象となるものを示すポインタのようなものです。運転手は同じでも、握らせるハンドルを変えることで軽自動車からトラック、果ては耕運機まで乗りこなします(本当か?)。fopen、fcloseなどで FILE* 型のポインタを用いて同じ関数でいろいろなファイルを操作しますよね。このときのファイルポインタも一種のハンドルです。ハンドルを使って先ほどのスタックを書いてみましょう。
#ifndef __STACK_H_ #define __STACK_H_ /* スタック用データ */ typedef struct tagStack { int *pStack; /* スタック用データ */ int nStackSize; /* スタックのサイズ */ int nStackPtr; /* スタック用のポインタ */ } Stack; Stack *InitStack(int nSize); /* スタックの初期化 */ void EndStack(Stack *stack); /* スタックの使用終了 */ void Push(Stack *stack, int data); /* データの格納 */ int Pop(Stack *stack); /* データの取り出し */ #endif /* __STACK_H_ */
#include <stdio.h> #include <stdlib.h> #include "stack.h" static void TrapErr(Stack *stack); /* エラーを処理 */ /* スタックの初期化 */ Stack *InitStack(int nSize) { Stack *stack; /* データ格納用メモリ確保 */ stack = (Stack *)malloc(sizeof(Stack)); /* メモリ不足の場合はエラー */ if ( stack == NULL ) ErrTrap(stack); /* 初期化 */ stack->pStack = (int *)malloc(nSize * sizeof(int)); stack->nStackSize = nSize; stack->nStackPtr = 0; return stack; } /* スタックの使用終了 */ void EndStack(Stack *stack) { /* メモリの開放 */ free(stack->pStack); free(stack); } /* スタックにデータを格納 */ void Push(Stack *stack, int data) { /* エラーチェック */ if ( stack->nStackPtr >= stack->nStackSize ) TrapErr(stack); /* スタックにデータの格納する */ stack->pStack[stack->nStackPtr++] = data; } /* スタックからデータを取り出す */ int pop(Stack *stack) { /* エラーチェック */ if ( stack->nStackPtr <= 0 ) TrapErr(stack); /* スタックからデータを取り出す */ return pStack[--stack->nStackPtr]; } /* エラーを処理 */ static void TrapErr(Stack *stack) { fprintf(stderr, "Stack Err\n"); EndStack(stack); exit(1); }
上のプログラムとは何をしたかというとスタックで利用する変数を構造体にしてそのポインタを第一引数にして渡すことにより処理を実現しています。利用するときは
Stack *st1, *st2; int data1, data2; /* スタックの作成 */ st1 = InitStack(128); st2 = InitStack(64); /* 使う */ push(st1, 1); push(st2, 2); data1 = pop(st1); data2 = pop(st2); /* 終了 */ EndStack(st1); EndStack(st2);
このように初期化部分で毎回 malloc で確保すればいくらでも同じものが作れます。fopen で FILE * 型が返され、fread や fwriteにそのハンドルを渡して操作し、最後に fclose で閉じるという流れとまったく同じです。これがC++への第一歩でして、プログラム中心の世界から、まずデータがあり、それに付随する形でそれを操作する関数があるという世界に突入していくのです。
基本は上記のプログラムなのですが、これでは書くのが大変面倒ですし、人為的なミスが起こりやすいです。問題点を挙げると
とまあ問題が多いわけです。結局これらのことをプログラマが気を使わなくても良いような機能を言語レベルで提供しようというのがC++のクラス(class)という機能です。最後にこのStackの機能をC++で書いてみましょう。
#ifndef __STACK_H_ #define __STACK_H_ /* スタック用データ */ class Stack { protected: int *m_pStack; /* スタック用データ */ int m_nStackSize; /* スタックのサイズ */ int m_nStackPtr; /* スタック用のポインタ */ void TrapErr(void); /* エラーを処理 */ public: Stack(int nSize); /* コンストラクタ */ ~Stack(); /* デストラクタ */ void Push(int data); /* データの格納 */ int Pop(void); /* データの取り出し */ }; #endif /* __STACK_H_ */
#include <iostream.h> #include "stack.h" /* コンストラクタ(構築子) */ void Stack::Stack(int nSize) { /* 初期化 */ m_pStack = new int[nSize]; m_nStackSize = nSize; m_nStackPtr = 0; } /* デストラクタ(消滅子) */ void Stack::~Stack() { /* メモリの開放 */ delete[] m_pStack; } /* スタックにデータを格納 */ void Stack::Push(int data) { /* エラーチェック */ if ( m_nStackPtr >= m_nStackSize ) TrapErr(); /* スタックにデータの格納する */ m_pStack[m_nStackPtr++] = data; } /* スタックからデータを取り出す */ int Stack::pop(void) { /* エラーチェック */ if ( m_nStackPtr <= 0 ) TrapErr(); /* スタックからデータを取り出す */ return m_pStack[--m_nStackPtr]; } /* エラーを処理 */ void Stack::TrapErr() { cerr << "Stack Err\n" << endl; exit(1); }
上記のようになります。使うときは
Stack *st1, *st2; int data1, data2; /* スタックの作成 */ st1 = new Stack(128); st2 = new Stack(64); /* 使う */ st1->push(1); st2->push(2); data1 = st1->pop(); data2 = st2->pop(); /* 終了 */ delete st1; delete st2;
のように利用すれば良いのです。
大雑把に動作を説明します。まず、C++では malloc/free関数に変わり new/delete演算子というものが使えます。new 演算子は生成したい型に合わせてメモリを確保し、必要であればコンストラクタという初期化関数を呼んでくれます。また、delete演算子はメモリを開放する前にデストラクタというオブジェクトを後処理するための関数を呼び出してからメモリを開放してくれます。詳しくはC++の本をお読みください。
とりあえずC++を使った際のメリットを書いておきますと
といったところです。つまりは class という構造体を拡張した機構と new/delete という型チェック機能のついたメモリ管理の機構により、安全かつ簡単にオブジェクトをブラックボックス化できるようになったわけです。クラスについての詳しい話や再利用についてはまた時間を見つけて書きたいと思います。今回は「Cの持つ問題点」と、「プログラム中心からデータ中心への移行の概略」について考えていただければ幸いです。今回は説明できませんでしたが、OOPの醍醐味はクラスの再利用にありますので、今後を期待してください。
一気に書いたんで疲れました。しかもhtml用に書いただけで、実行はおろか一度もコンパイラにかけていません。多分バグだらけですんで、あまり鵜呑みにしないようにお願いします。変なところあったら教えてください、直します。