ホーム < ゲームつくろー! < C++踏み込み編 < constのあれこれ


その2 constのあれこれ


@ 一般変数に対する振る舞い

 「const」は「キーワード」という部類の1つだそうで、変数のアクセスに対して付加的な機能を提供してくれます。基本的な使い方は次のようです。

const int Val = 200;

宣言と同時に初期化をすると、Valはもう変更できません(緊急回避法はあります)。ですから、「Val = 100;」とするとコンパイルエラーとなります。C++の場合、こうして宣言した値は「定数」として扱われるので、例えば、

int Array[Val];

と配列の要素数として使うことが出来ます。constを付けないと、これはコンパイルエラーです。ちなみに、

const int Val = 200;
int const Val = 200;

どちらもOK。ここまでは簡単。


 constはポインタに対しても使えますが、これがとても面白い振る舞いになります。

int Val = 200;
const int *pVal = &Val;

こうすると、int型のpValを固定したように見えるので、

int *pDumy;
pVal = pDumy;

のようにポインタを代入したらエラーになりそうな気がしますよね。ところが、これはセーフなんです。このコンストの使い方だとポインタ変数の変更を許容します。「じゃ、何を固めているの?」と思いますよね。実は、「ポインタを通して何かすること」をブロックしています。よって、

*pVal = 10000;

これはコンパイルエラーになります。また、const宣言されていないポインタへの代入もエラーとなります。

int *pDumy;
pVal = pDumy;     // セーフ
pDumy = pVal;     // アウト!

間違ってはいけないのが、「ポインタの先にある変数の変更」に関しては保障しません。他のポインタが同じ変数を指している場合、そのポインタを通してあっさりと変更されます。こういうイメージですね。


 さて、同じようなポインタに対するconstに別の宣言方法があります。

int Val = 200;
int *const pVal = &Val;

先ほどと違いconstが型名の後に来ています。これは「定数ポインタ」と呼ばれていて、ポインタそのものを固定します。上の絵で言えば箱と矢印を接着剤でくっつけて、常にワンセットになっているとイメージできます。ですから、接着をはがそうとする行為、つまり、

int *pDumy;
pVal = pDumy;

というポインタの変更はコンパイルエラーとなります。ただ、くっつけているだけで通行止めにはしないので、ポインタを通した変数の変更はもちろん可能です。

 constの位置による名前の違いはこうです。

 const int *pVal     : 定数データへのポインタ (通行止め)
 int *const pVal     : 定数ポインタ        (接着剤)


 定数データへのポインタを関数の引数に使用した場合、そのポインタを通したデータの変更が禁止されます。例えば、

void Change(const int *pval)
{
   *pval = 200;
}

はコンパイルエラーです。つまり、この関数内では読み取り専用になり、「int val = *pval」のような代入だけが許されます。

 オブジェクトの場合はもっと厳しくて、定数オブジェクトへのポインタを引数にしてしまうと、ポインタを通して通常のメンバ関数を呼び出すことが一切出来なくなります。「じゃ、そんな引数は意味無いじゃん!」と思ってしまんですが、そうではありません。メンバ関数が「constメンバ関数」であれば呼び出し可能なのです。constメンバ関数については次で説明します。



A constメンバ関数をうまく使うべし

 constメンバ関数とは「読み込み専用」を保障したメンバ関数です。この関数を通してオブジェクトのメンバ変数は一切変更できません。まず、宣言方法から見てみましょう。

class Animal
{
private:
   string m_Name;

public:
   string GetName() const { return m_Name; }
   void SetName( stirng name){ m_Name = name; }
};

引数の後にconstを入れます。これでconstメンバ関数の出来上がりです。この関数の中で例えば「m_Name = "Animal";」とメンバ変数を変更したり、「SetName("Animal");」のようにメンバ関数を呼ぶとコンパイルエラーになります。ただし、constメンバ関数は呼び出せます

 constメンバ関数に入ると、メンバ変数はすべて定数として扱われるようになります。「定数」というところがポイントでして、メンバ変数がポインタの場合「定数ポインタ」になります。接着剤の方です。ということは、constメンバ関数内から定数ポインタを通して値の変更をするのは可能です。あくまでも、自分のメンバ変数をガードする。それがconstメンバ関数なんですね。

 「それなら、定数ポインタがメンバ変数のアドレスを保持していたら変更できるんでないの?」なんて考えた人は鋭いですねぇ!これやってみたら、できちゃうんです

string GetName() const
{
   *m_pName = "Animal";   // m_pNameは&m_Nameを保持しているとします
   return m_Name;
}

これはコンパイラを通ります。


 constメンバ関数の使い道ですが、値を取得するだけの関数、メンバ変数に付加的に演算して出力する関数はconstメンバ関数にするべきです。これには決定的な理由があります。すごく重要なことなんですが、「他のクラスのメンバ関数の引数がconstオブジェクトの時、唯一呼び出せるのはconstメンバ関数」なんです。例を挙げます。

class Zoo
{
private:
   string m_AnimalName;
public:
   void GetAnimalName(const Animal *animal)
   {
      m_AnimalName = animal->GetName();
   }
};

動物園クラスが動物の名前を格納しようとしている分かりやすい関数なのですが、引数はconstオブジェクトです。この場合、GetName関数が通常のメンバ関数だとしたら、これはコンパイルエラーになってしまいます。オブジェクトに対してはconstは厳しいのです。しかし、GetName関数がconstメンバ関数だったら、この呼び出しはセーフです。

皆さん上のような呼び出しをした時に、

エラー内容
error C2662: 'GetName' : 'const class Animal' から 'class Animal &' へ 'this' ポインタを変換できません。

というエラーが出たことはありませんか?このエラーはAnimalクラスのGetName関数をconstメンバ関数にしていないのが原因です。このエラーが出た時にconstを外す人がいますが、それはイタダケナイ。 引数にconstキーワードを使うと言うことは、そのオブジェクトを一切変更しませんよ、データだけ頂きますよと宣言しているわけで、それを外すと言うことは「変更もするぜ」と宣言しているようなものです。ですから、このエラーが出た時には、引数に指定したクラスのメンバ関数をconstメンバ関数にする変更をしましょう

これらをうまく使いこなして、厳格なアクセス権限を確保することは、保守性を保つ上でも重要ですね。



B 通行止めを強行突破!const_cast演算子

 constポインタを使うと変更に対して厳しい制御を行えますが、時に間違って使われいたりすると、「ここをせき止めるなよ!」という目に合ったりします。もしくは、どうしても変更したい、アクセスしたい時もあります。const_cast演算子は、変数宣言からconstを取り外して自由にアクセスできるよう変更してくれます。

 使い方はとても簡単。

const int *cnsVal = 200;   // 通行止め

// constポインタを変更したい!
int *freeway = const_cast<int*>(cnsVal);

*freeway = 1000;  // 変更

int Result = *cnsVal;   // 1000が入る

 const_castの後に変更したいポインタ型を宣言し、その後ろに変更元であるconstポインタを置きます。このキャストが成功すると、constが外れたポインタが返されるので、そのポインタを通して自由に値を変更できます。

 これがなんの役に立つかというと、やっぱり緊急回避なんだろうと思います。constはconstであるが故の理由があるので、それを破るのはプログラムの秩序を乱しかねません。


 constは使うべきところでビシッと使って、変更や緊急回避をしなくても良い設計にする事が大切ですよね。