7.4 const

さて、ここまで文字列リテラルに対する変更操作(書き込み操作)は未定義であると述べてきました。よって以下のようなコードは正しく動きません。

int main()
{
    char* str="hoge";
    str[1]='a';
}

しかし、コンパイルは通ってしまう事があります。正しく動かない事が初めから分かりきっているのであれば、このようなコードはコンパイルエラーにしてしまって未然に防ぎたいものですよね。

7.4.1 constキーワード

これを防ぐためには、変数に対する変更操作を禁止する事で実現できそうです。C++には、そのためのキーワードが用意されています。

int main()
{
    const char* str="hoge"; 
    char const* str1="hoge"; // 同じ意味
    str[1]='a'; // エラー!
    str1[1]='a'; // エラー!
}

constキーワードです。これを識別子名の前に付与する事でその変数に対する書き込み操作を禁止する事ができます。上記のように二つの記述法がありますが、どちらも同じ意味です。

この場合、ポインター変数に対してconstを指定していますが、以下のような操作は許されます。

#include<iostream>
int main()
{
    const char* str="hoge";
    str="foo";
    std::cout<<str<<std::endl;
}

実行結果は以下となります。

foo

constを指定しているのにも関わらず変更できているじゃないか!と思うかもしれませんが、この場合禁止しているのはポインターが指している先の実態データに関する変更操作であって、ポインターが指し示す先の変更操作ではないからです。 ポインターが指し示す先も固定したい場合、以下のように記述します。

int main()
{
    const char* const str="hoge"; // ポインターが指し示している実態とポインターが指し示すものを固定
    char const* const str1="hoge"; // 同じ意味

    // 以下は全てエラー
    str="foo";
    str1="foo";
    str[0]='a';
    str1[0]='a';
}

この機能は、勿論ポインター型以外にも使う事ができます。

int main()
{
    const int a=10;
    const char b='a';
    a=42; // エラー!
    b='b'; // エラー!
}

また、関数の引数に付与する事もできます。

#include<iostream>
int func(const int l,const int r)
{
    l=10; // エラー!
    r=20; // エラー!
    return l+r;
}

int main()
{
    int x=10,y=20;
    std::cout<<func(x,y)<<std::endl;
}

このようにする事で、引数にされた変数に対する変更操作を禁止する事ができます。これは、ポインターを渡す時、また、まだ説明していませんがこれから取り扱う概念である参照と組み合わせる時に、より有用的な効果が発揮されます。ここでは、ポインターでの例を示します。

void f(int* ptr)
{
    // *ptr=42 などといった変更操作が行われればxの値は変わってしまうかもしれない...
}

int main()
{
    int x=10;
    f(&x);
}

このように、アドレスを渡したいが関数fxに対する変更操作は行わないでほしいとします。しかし、プログラマーが意識しないうちに何気なく変更操作をおこなってしまうコードを書いてしまうかもしれません。そういった場合に、constを利用します。

void f(const int* ptr)
{
    *ptr=42; // 間違えて変更操作を行ってしまったがconstのためエラーになってくれる。
}

int main()
{
    int x=10;
    f(&x);
}

また、例えば以下のように別の関数内でポインターの指し示す先を変えてしまうコードがかけてしまう事で、意図しない動きになってしまうかもしれません。

#include<iostream>

void f(int* ptr)
{
    int a;
    ptr=&a; // ptrはxとは違うアドレスを指している
    *ptr=20; // xと同じアドレスに対する操作のつもり
}

int main()
{
    int x=10;
    f(&x);
    std::cout<<x<<std::endl; // 20になってほしい
}

こういった場合にも、constを用いてポインターの指し示す先を固定する事で未然にエラーにする事ができます。

#include<iostream>
void f(int* const ptr)
{
    int a;
    ptr=&a; // エラーとなる
    *ptr=20; 
}

int main()
{
    int x=10;
    f(&x);
    std::cout<<x<<std::end
}

因みに、const指定された変数は初期化時になんらかの値を与えなければなりません。

const int a; // エラー!初期化値が指定されていない

constはその性質上、代入操作は許されていませんから、初期化時点で値を与えないと何の意味もなくなってしまいますので、この挙動も当然ですね。

7.4.2 const_cast

「2.1.9 キャスト」でキャストについて述べた際、const_castについて述べていませんでした。const_castは、const指定されたオブジェクトのconst性を排除するキャストです。

#include<iostream>

int main()
{
    const int x=0;
    const int* const a=&x; // ポイントする先も変更しないし、ポイント先の値も変更しない

    std::cout<<*a<<std::endl;
    int* const b=const_cast<int* const>(a); // ポイントする先は変更しないが、ポイントする先の値は変更する

    *b=30;
    std::cout<<*a<<std::endl;
}

実行結果は以下となります。

0
30

ポインター、aはポイント先も、ポイント先の値もconstと指定していますがconst_castによって変更できてしまいました。

このように、const_castは、constなオブジェクトという、変更がされないという決まりごとを強引に排除してしまうキャストです。よって、特別必要がない限り、const_castによってconstを排除するべきではありません。上記の場合であればconst_castを使うのではなく、初めからconstなオブジェクトとして定義しなければ良いのです。