代入演算子とは「=」のことである。C言語の構造体で以下のように代入演算子を用いると、
構造体のメンバーが全てコピーされた。
struct meibo{
char name[20];
int age;
};
struct meibo abe;
struct meibo tmp;
abe.name = "Abe Natsumi";
abe.age = 19;
tmp = abe;
|
クラスも同様に代入演算子を使用すると、メンバー変数が全てコピーされます。
「メンバー変数を全部コピーする」という代入演算子はコンパイラが自動的に作成してくれます。
実はこのことは便利なようで危険がいっぱいです。
スタッククラスを例にとって説明します。スタッククラスではコンストラクタで、
引数で渡された領域をnewしています。つまりメンバー変数はサイズと、割り当てた
メモリーが格納されています。不用意にアドレスがコピーされ、一方のインスタンスが
削除されればデストラクタで割り当てたメモリーが解放されてしまいます。つまり
コピーした方のスタックのpushなど関数を呼べば、すでに解放されたメモリーを
アクセスすることになり、実行時エラーになってしまいます。
void main(){
stack<int> stack1(10);
// スタッククラスのメンバー変数stackにint型配列10個分が確保される
// 例えば、1000番地に割り当てられたとすると、stack1.stack = 1000になる
{
stack<int> stack2(10);
// スタッククラスのメンバー変数stackにint型配列10個分が確保される
// 例えば、2000番地に割り当てられたとすると、stack1.stack = 2000になる
stack2 = stack1;
// 自動的にメンバー変数がコピーされる。つまり、
// stack2.stack = 1000になる
// 2000番地に割り当てられたメモリーは迷子になり、解放することができなくなる。
}
// stack2のインスタンスが削除される。つまりデストラクタが呼ばれる
// デストラクタ内で、1000番地のメモリーは解放されてしまう。
stack1.push(10);
// stack1.stack = 1000番地はすでに解放されてしまっている。
// この時点で開放された領域にアクセスしようとするので、エラーが出てしまう。
}
// stack1のインスタンスが削除される。つまりデストラクタが呼ばれる
// デストラクタ内で、すでに解放されている1000番地のメモリーは解放しようとし、エラー。
|
このように、メンバー変数として、ポインタを持ち、且つデストラクタでdeleteする
ようなクラスは勝手に代入されることを防がなければなりません。
どのように防げばよいのでしょう。C++では、演算子を作ることもできます。つまり
コンパイラに頼らずに自分で代入演算子を作ってしまえばよいのです。なお、演算子の
作り方は後述します。ここではスタッククラスの代入演算子の例を示します。変更点は
アンダーラインの部分です。
/////////////////////////////////////////////////////////////////////////////
//
// テンプレートスタッククラス(動的メモリ割当版)定義
//
// 2重取り込みを防止する
#ifndef STACK_H
#define STACK_H
#include<process.h>
#include<iostream>
using namespace std;
//////////////////////////////////////////////////////////////////////////////
// スタッククラス
template<class Type>
class Stack {
public:
// コンストラクタ:
// Stack オブジェクトが定義された時に自動的に呼び出される
Stack( int sz = 100 ){
// コンストラクタ引数を省略すると、スタックサイズは100となる
// スタックサイズを保存する
size = sz ;
// スタックの実体(size個のType配列)を自由記憶上に割り当てる
stack = new Type[size] ;
// スタックポインタを初期化する
sp = 0 ;
}
// デストラクタ:
// Stack オブジェクトが削除される時に自動的に呼び出される
~Stack(){
//スタック本体を削除する
delete [ ] stack ;
}
// プッシュ
void push( const Type& value ){
// オーバーフローのチェック
if( sp >= size ){
cerr << "stack overflow" << endl ;
exit( 1 ) ;
}
*( stack + sp ) = value ;
sp++ ;
}
// ポップ
void pop( Type* p_value ){
// アンダーフローのチェック
if( sp <= 0 ){
cerr << "stack underflow" << endl;
exit( 1 ) ;
}
sp--;
*p_value = *( stack + sp );
}
// 現在のスタック長を獲得する
int get_length(){
return sp;
}
// 指定位置(スタックトップからのオフセット)のスタック要素を覗き見る
void peek( Type* p_value, int offset = 0 ){
// オフセットのチェック
if( offset >= sp ){
cerr << "stack underflow" << endl ;
exit( 1 ) ;
}
// 指定位置の要素をコピーする
*p_value = *( stack + sp - offset - 1 );
}
// 代入演算子
Stack<Type>& operator=( const Stack<Type>& src ){
// 自己代入でなければ
if( &src != this ){
// 現在自分が保有しているスタックを削除する
delete [] stack;
// サイズをコピーする
size = src.size;
// スタックポインタをコピーする
sp = src.sp;
// サイズ分の配列を作る
stack = new Type[size];
// 配列の中身をコピーする
for( int i = 0 ; i < size ; i++ )
stack[i] = src.stack[i];
}
return *this;
}
private:
////////////////////////////////////////////////////
// 管理情報
// スタックポインタ:次にプッシュする位置
int sp;
// スタックサイズ
int size;
// スタック
Type *stack;
};
#endif // STACK_H
|
代入演算子では、現在の配列をdeleteして、コピー元の配列と同じ大きさの配列を
作る。そしてサイズと、配列の要素全てをコピーする。もし、コピー元とコピー先が
同じ場合(a=aのような場合)、これを自己代入という。自己代入の場合、自分の配列
(自己代入なので、コピー元の配列と同じ)を削除し、コピーもとの配列をコピーしようと
すると、コピー元の配列はすで削除されているため、コピーできない。したがって、
自己代入を行おうとしているのかをチェックし、自己代入ならば何もしないと言う
操作をする必要がある。このため、if文で自己代入かどうかのチェックを行う必要がある。
なお、その他の演算子については後述する。