【C言語】constの使い方・メリットを解説

constの解説ページアイキャッチ

このページでは、C言語における const について解説していきたいと思います。

const はC言語に用意された修飾子で、const 指定された変数は “変更不可” になります。変更不可な変数って言葉的に矛盾してる気もしますし、不便な気もしますね…。

ただ、const を利用することで得られるメリットは大きいです。このメリットを知っておくと、きっとあなたも const を利用したくなると思います(たぶん…)!

このページでは、まず const の使い方とその効果について解説し、続いて const を利用するメリット(ここが一番重要)を、最後に const 利用時の注意点について解説していきたいと思います。 

const の基本的な使い方と効果

const の一番基本的な使い方は「変数宣言時に const 指定して、その変数を変更不可にする」です。

まずはこの「変数宣言時の const の使い方」を説明し、続いてこの const の効果について解説します。

const の基本的な使い方

この変数宣言時の const 指定は、型名の前側(or 型名の後ろ側)に “const” と追加で記述することで行うことができます。

変数宣言時のconst指定
const 型名 変数名;

例えば、int 型の変数 x に対して const 指定する場合は、下記のように記述します。

変数宣言時のconst指定例1
const int x = 100;

上記では const 指定は型名の前側で行っていますが、次のように型名の後ろ側で行うこともできます。どちらでも同様の効果が得られます。

変数宣言時のconst指定例2
int const x = 100;

スポンサーリンク

const の効果

続いて、上記のように const 指定することで、どのような効果があるかについて解説していきます。

const 指定により変数を変更不可にできる

この const 指定を行えば、変数の初期化後、その変数の値を変更不可にすることができます。

const指定の効果を示す図

もう少し具体的にいうと、変数の初期化後にその変数の値を変更しようとすると、コンパイル時にエラーが出力されるようになります。

つまり const は、コンパイラによって変数を “変更不可” にする修飾子です。他の言い方をすると、その変数を “読み取り専用にする”・”定数化する” とも言えます。

ですので、const初期化後に “値を変更されたくないような変数” に対して指定します。

これにより、”値を変更されたくないような変数” を直接変更する処理を記述した場合、その変更をコンパイル時に検出できるようになります(エラーが出力されるようになるので)。

例えば下記のような処理を記述すると、コンパイル実行時にエラーが発生します。

const指定の変数の値を変更
const int x = 100;

x = 1000; /* この行に対してコンパイルエラー */

const 指定した変数を変更することで出力されるエラーは、下記のようなものになります(下記は私の環境で出力されたエラーです。環境によってはエラーの文言は異なりますので注意してください)。

error: cannot assign to variable 'x' with const-qualified type 'const int'

上記でエラーが発生するのは x = 1000 の部分です。あくまでも変数が変更不可になるのは “初期化した後” なので、初期化は通常通りに行うことができます。

逆に初期化を行わないと、変数に格納されている値がずっと不定値のままになる可能性もあり、使い道がなくなります。

const 指定する場合は、必ず変数の初期化を行うようにしましょう。

また、const はあくまでも変数を “変更不可” にするための修飾子ですので、const 指定された変数であっても通常の変数のように “値の取得” は可能です。

例えば下記のように、const 指定した変数 x から値を取得するような処理を記述しても、コンパイルでエラーは発生しませんし、正常に動作させることができます。

const指定の変数から値を取得
const int x = 100;
int y;

/* 値を取得して他の変数に代入 */
y = x;

/* 値を取得して標準出力に出力 */
printf("%d\n", x);

また、この const 指定は配列に対しても有効です。これにより、変更不可な配列を作成することができます。

この場合、配列の各要素を直接変更する処理を記述した際にコンパイルエラーが発生するようになります。

例えば下記の場合、配列 arrayconst 指定されているため、array の第 0 要素を直接変更する array[0] = 10 の行でコンパイルエラーが発生します。

配列へのconst指定
const int array[3] = {1, 2, 3};

array[0] = 10; /* コンパイルエラー */

同様に、多次元配列に対しても const 指定を行うことができます。

例えば下記の場合、配列 number が const 指定されているため、number の第 5 要素の第 0 要素('F' の文字)を直接変更する number[5][0] = 'G' の行でコンパイルエラーが発生します。

多次元配列へのconst指定
const char number[10][16] = {
    "ZERO", "ONE", "TWO", "THREE", "FOUR",
    "FIVE","SIX", "SEVEN", "EIGHT", "NINE"
};

number[5][0] = 'G'; /* コンパイルエラー */

この配列 number は、添字に 09 の整数を指定すれば、その整数を英語の文字列に変換するテーブルです。例えば number[7] により 7 を英語の文字列に変換した "SEVEN" を取得することができます。

こういった変換テーブルは、初期化時に変換後のデータ(上記の例だと英語の文字列)を格納し、その後は変換後データの取得のみに利用するだけで変更は行わないようなことが多いです。むしろ変更されてしまうと、取得できる英語の文字列がおかしくなることになるのでバグになります。

こういった、変更して欲しくない配列を “変更不可” にしておくことを目的に const を利用するケースは結構あります。

例えば、単なる int 型や double 型などの変数に関しては、わざわざ const 指定して “変更不可” な変数を用意しなくても、#define を利用して定数を定義してやれば事足りることがあります。

一方で、#define では配列の定義ができないので、変更不可な配列を用意したい場合は、const 指定が必須になります。なので、この配列に対する const 指定の利用価値は結構高いと思います。

さまざまな const の使い方

const 指定は下記に対しても行うことができます。

  • ポインタ変数
  • 関数の引数
  • 関数の戻り値

const 指定の中でも、特にこの3つに対する const 指定が重要なものになります。ということで、次はこの3つに対する const 指定について解説していきたいと思います。

ポインタ変数への const 指定

ここまでの説明は主に “ポインタ変数” 以外の変数に対する const 指定についてのものになります。

ポインタ変数への const 指定はもう少し違った意味合いになることがあるので、ここでポインタ変数への const 指定について解説していきます。const を利用する上で一番重要なのは、このポインタ変数への const 指定になると思います。

ポインタ変数の場合、今までの解説の通り const 指定により変数に格納されている値を “変更不可” にするだけでなく、そのポインタが指すデータ(変数やメモリ)も “変更不可” にすることができます。

簡単にポインタについておさらいしておくと、まずポインタ変数に格納されるのはアドレスです。ポインタ変数を変更するということは、そのポインタに格納されるアドレスを変更することになります(イメージとしてはポインタが指す先が変わることになります)。

ポインタの指す先を変更する様子

さらにポインタ変数では、間接演算子 * や添字演算子 [] により、変数に格納されているアドレスのデータの値を取得したり変更したりすることが可能です(イメージとしてはポインタが指すデータの値を取得したり変更したりすることが可能です)。

ポインタの指す先のデータを変更する様子

つまり、ポインタ変数からは、”ポインタが指す先” と “ポインタが指すデータ” の2つが変更可能です。

ポインタから変更できる2つのデータを示す図

そして、ポインタ変数に対して const 指定することで、そのポインタ変数からの変更を、上記の2つのどちらか一方、もしくは両方を不可にすることができます。

どれに対して変更不可にするかは、変数宣言時の const の記述場所によって制御することができます。

ポインタへの const 指定①:”ポインタが指す先” を変更不可にする

通常のポインタ変数を変数宣言する場合は、例えば下記のように * を用いて記述すると思います。

ポインタの変数宣言
int *x;

 *後ろ側const を指定することで、“ポインタが指す先” を変更不可にすることができます。

ポインタの指す先を変更不可にした様子

これにより、ポインタ変数への値の代入、つまり “ポインタが指す先” の変更を行うとコンパイルエラーが発生するようになります。

ポインタが指す先に対するconst指定
int a = 10, b = 20;
int * const p = &a;

p = &b; /* コンパイルエラー */

ただし、”ポインタが指す先” の変更はできませんが、そのポインタの指す先のデータに関しては変更可能です。

ポインタが指す先に対するconst指定2
int a = 10, b = 20;
int * const p = &a;

*p = 100; /* aの値が100になる */

ポインタへの const 指定②:”ポインタが指すデータ” を変更不可にする

*前側const 指定することで、“ポインタが指すデータ” を変更不可にすることができます。型名の前側・後ろ側のどちらに const を記述しても問題ありません。

ポインタが指すデータを変更不可にする様子

これにより、間接演算子や添字演算子を利用したポインタ変数への値の代入、つまり “ポインタが指す先のデータ” の変更を行うとコンパイルエラーが発生するようになります。

指すデータに対するconst指定
int a = 10, b = 20;
const int *p = &a;

*p = 100; /* コンパイルエラー */

この場合は、ポインタが指す先自体は変更可能です。

指すデータに対するconst指定2
int a = 10, b = 20;
const int *p = &a;

p = &b; /*pはbを指す */

ポインタへの const 指定③:両方を変更不可にする

さらに、*前後両方に対して const を指定することで、“ポインタが指す先” と “ポインタが指すデータ” の両方を変更不可にすることができます。

ポインタの指す先とポインタが指すデータ両方を変更不可にする様子

これにより、ポインタ変数への値の代入 or 間接演算子や添字演算子を利用したポインタ変数への値の代入が行われるとコンパイルエラーが発生するようになります。

両方に対するconst指定
int a = 10, b = 20;
const int * const p = &a;

p = &b; /* コンパイルエラー */
*p = 100; /* コンパイルエラー */

使用用途が多いのは “ポインタへの const 指定②”

ここまで3種類のポインタ変数への const 指定を紹介してきましたが、一番よく利用するのは、2番目に紹介した「”ポインタが指すデータ” を変更不可にする」だと思います。

ポインタが指すデータを変更不可にする様子

単純に “ポインタが指すデータ” を変更不可にしたい場合にも利用しますが、そもそもポインタが “変更不可” なデータを指しているときにも const が有効です。

例えばポインタではリテラル(ソースコード上に記述された文字列)を指すことも可能です。ただし、リテラル自体は変更不可能で、リテラルを変更しようとすると、おそらくプログラムは異常終了すると思います。

例えば下記ではリテラル "AIUEO" の第 0 文字の 'A' を小文字の 'a' に変更しようとしています。

リテラルの変更
char *str = "AIUEO";

str[0] = 'a'; /* プログラム落ちる */

前述の通り、リテラルは変更不可なので、上記を実行するとプログラムが異常終了します。

なので、そもそも変更不可能なデータをポインタで指しておくと、誤ってそのポインタが指すデータを変更してしまってプログラムが異常終了してしまうという可能性があります。

ですので、こういった変更不可能なデータをポインタで指す場合は、ポインタが指すデータの変更を防ぐために const を利用した方が良いです。const 指定をしておけば、コンパイルエラーが発生しますので、リテラルの変更をプログラム実行前に検知することができます。

リテラルの変更をconstで防ぐ
const char *str = "AIUEO";

str[0] = 'a'; /* コンパイルエラー */

また、このポインタ変数への const 指定、特に2番目に紹介した「”ポインタが指すデータ” を変更不可にする」に関しては、関数の引数や関数の戻り値に利用することで、大きなメリットを得ることができます。

ということで、ここからは関数の引数や関数の戻り値への const 指定について解説していきたいと思います。

スポンサーリンク

関数の引数に対する const 指定

まずは関数の引数に対する const 指定について解説していきます。

ここまで解説してきた const は、変数宣言時だけでなく、関数の引数に対して指定することも可能です。

関数の引数への const 指定はポインタ引数に対してのみ行う 

引数への const 指定は、基本的にはポインタに対してのみ行います。

さらに、”ポインタが指すデータ” を変更不可にする目的でのみ const 指定を行います。つまり、引数の型の * よりも前側に const を記述して const 指定を行います(これは、ポインタへの const 指定②:ポインタが指すデータの値を変更不可にするで紹介した const 指定になります)。

関数呼び出し側と関数側とでは、引数で値を渡すことはできますが、変数そのものは別のものなので、引数がポインタ以外の場合は関数呼び出し側の変数が変更されることはありません。

なので、引数がポインタ以外の場合、その引数を関数内でどれだけ変更しようが関数呼び出し側には影響ありません。

その一方で、引数がポインタの場合、関数呼び出し側で使用している変数やデータのアドレスがそのポインタ引数に渡されるので、関数呼び出し側で使用している変数やデータが関数内で変更されてしまう可能性があります。

この「ポインタ引数の指すデータが変更されてしまうこと」を防ぐのを目的に、ポインタ引数に対して const が利用されます。

さらに、関数の引数に const 指定することで、その関数内ではポインタ引数の指すデータが変更されないことを関数利用者に対して示すこともできます。

つまり、関数の引数に対して const を指定することで、下記の2つの効果が得られることになります。

  • 関数内でポインタ引数の指すデータが変更不可になる
  • ポインタ引数が指すデータを変更しないことを示すことができる

続いては、この2つの効果について解説していきます。

関数内でポインタ引数の指すデータが変更不可になる

関数のポインタ引数に const 指定することで、そのポインタ引数が指すデータを変更不可にすることができます。

この効果に関しては、ポインタへの const 指定②:ポインタが指すデータの値を変更不可にするで解説した内容と同様のものになります。

つまり、ポインタ引数に const 指定することで、そのポインタ引数の指すデータを変更しようとした場合にコンパイルエラーが発生するようになります。

例えば下記では str2 の指すデータが変更不可になるように const 指定していますので、コンパイル時に str2[0] = 'A' の行でエラーが出力されます。 

引数の型に対するconst指定
char *func(char *str1, const char *str2) {
    str2[0] = 'A'; /* コンパイルエラー */
}

ポインタ引数が指すデータを変更しないことを示す

さらに関数のポインタ引数に const を指定することで、関数内ではそのポインタ引数の指すデータを “変更しない” ことを関数利用者に示すことができます。

const指定により、関数がポインタ引数の指すデータを変更しないことを示す様子

前述の通りC言語では、関数の引数がポインタである場合、関数内から関数呼び出し側のデータを変更することができてしまいます。

例えば下記の場合、関数呼び出し側で指定した p_str が指すデータと、関数の引数 p が指すデータは同じもの(両方が配列 str を指す)になります。ですので、関数内から関数呼び出し側で使用している配列 str の値が変更される可能性があります。

strが変更される可能性がある
#include <stdio.h>
#include <string.h>

void func(char *);

void func(char *p) {
    /* pが指すデータが変更される可能性あり! */
}

int main(void) {
    char str[] = "AIUEO";
    char *p_str;

    p_str = str;

    func(p_str);

    /* AIUEOと表示されるとは限らない */
    printf("str = %s\n", str);

    return 0;
}

ですので、関数 func 内で “絶対に配列 str が変更されたくない場合” は、下記のように配列 str を他の配列等に一旦コピーして退避してから func を実行する必要があります。

strのデータを退避してから関数実行
#include <stdio.h>
#include <string.h>

void func(char *);

void func(char *p) {
    /* pの指すデータが変更される可能性あり! */
}

int main(void) {
    char str[] = "AIUEO";
    char tmp[256];
    char *p_str;

    /* 一旦strをtmpに退避 */
    strcpy(tmp, str);

    p_str = str;

    func(p_str);

    /* 退避したデータをstrに戻す */
    strcpy(str, tmp);

    /* 必ずAIUEOと表示される */
    printf("str = %s\n", str);

    return 0;
}

ですが、func の引数の型が const char * であった場合はどうでしょう?

funcの引数にconst指定
void func(const char *p)

この場合、func の引数 p が指すデータ、つまり配列 str を変更しようとするとコンパイルエラーが発生することになります。したがって、func 内では配列 str は変更されないと判断することができます。

であれば、関数実行前にわざわざデータの退避をする必要はないですね!

何が言いたいかというと、関数利用者は、関数内で引数に指定したアドレスのデータが “変更される可能性の有無の情報” が無いとデータの退避を行うべきかどうかの判断ができません。

で、その情報を関数作成者が関数利用者に伝える手段の1つが “関数の引数の型に対する const 指定” になります。

関数の引数がポインタの場合、その型に const を指定することで、そのポインタが指すデータを “変更しない” ことを関数利用者に示すことができます。

逆に const を指定しないことで、そのポインタの指すデータを “変更する可能性がある” ことを関数利用者に示すことができます。

こういった情報を示すだけで、関数利用者はその関数を利用しやすくなるので、関数作成時はポインタ引数には積極的に “適切に” const を付ける方が良いとと思います。

逆に関数を利用する時は、ポインタ引数に const 指定されているかどうかを確認して、事前にデータの退避を行うかどうかを判断するようにしましょう!

関数の戻り値に対する const 指定

const は関数の戻り値の型に対して指定することも可能です。

戻り値の型に対するconst
const char *func(char *str1, const char *str2) {
    return "AIUEO";
}

戻り値の型に対する const 指定により、戻り値のアドレスのデータが変更できないことを関数利用者に対して示すことができます。

const指定により、戻り値のアドレスのデータが変更できないことを示す様子

例えば上記ではリテラル "AIUEO" のアドレスを返却しており、関数呼び出し側はそのアドレスを戻り値として受け取ることになります。

アドレスのデータがリテラルなので、もし関数呼び出し側でこのデータを変更してしまうと異常終了してしまうことになります。

こういった変更できないデータのアドレスを返却するような場合に、戻り値の型に const 指定することで、関数利用者に “この関数の戻り値のアドレスのデータは変更できない” ことを伝えることができます。

また、変更して欲しくないデータのアドレスを返却するような場合も、戻り値の型に const 指定する方が良いです。

関数利用者側は、関数の戻り値が const 指定されているのであれば、その戻り値を格納するポインタ変数も const 指定しておいた方が良いと思います(ポインタ変数宣言時に * よりも前側に const を記述して const 指定)。

これにより、関数の戻り値として受け取ったアドレスのデータが変更できなくなるため、関数作成者の意図に沿った戻り値の使い方をすることができます。

const を利用するメリット

続いて const を利用するメリットについて解説していきたいと思います。これを知ると、あなたも const を積極的に使いたくなる(はず)!

スポンサーリンク

人為的ミスを防ぐ仕組みになる

この const 指定を行うことで得られる1番のメリットは、「変更して欲しくない変数が変更されてしまう」というミスを防ぐことができる点だと思います。

このメリットについて、具体例を用いて解説していきたいと思います(ちょっと極端な例ですが、その点はご容赦願います…)。

例えば下記のような配列がグローバル変数として変数宣言されているとします。添字に 09 の整数を指定すれば、その整数を英語の文字列に変換した結果が取得できる配列になります。const 指定は無しです。

変数宣言されている配列
char number[10][16] = {
    "ZERO", "ONE", "TWO", "THREE", "FOUR",
    "FIVE","SIX", "SEVEN", "EIGHT", "NINE"
};

さらに、この配列は、複数の開発者がいろんな関数から利用しているとしましょう。開発者には、この配列 number は変更しないように一応伝えています。

numberが利用されている様子
#include <stdio.h>
#include <string.h>

char number[10][16] = {
    "ZERO", "ONE", "TWO", "THREE", "FOUR",
    "FIVE","SIX", "SEVEN", "EIGHT", "NINE"
};

void func1(int x) {
    printf("%s\n", number[x]);
}

char *func2(int x) {
    return number[x];
}

void func3(void) {
    for (int i = 0; i < 10; i++) {
        printf("%s\n", number[i]);
    }
}

/* 略 */

int func100(char *num) {
    for (int i = 0; i < 10; i++) {
        if (strcmp(number[i], num) == 0) {
            return i;
        }
    }
    return -1;
}

で、ある程度開発が進んできた時にふと number[5] の文字列を出力してみると、下記のように表示されました。

FlVE

何がおかしいか分かりますか?"FIVE" の第 1 文字のアイの文字が小文字のエルに変わっています!

おそらく誰かがどこかの関数で間違って配列 number[5] の文字列を変更してしまったようです…。

ではここで1つ目の質問です。

この変更が行われた箇所をどうやって特定すれば良いでしょうか?

特定する方法はいくつもあります。例えばソースコードを読み返してみてもいいですし、要所要所に printfnumber[5] を表示してみたり、デバッガーを利用して number の値をウォッチしながら変更箇所を特定したりすることもできます。

でも、一番簡単な方法は numberconst 指定してやることです。

変数宣言されている配列
const char number[10][16] = {
    "ZERO", "ONE", "TWO", "THREE", "FOUR",
    "FIVE","SIX", "SEVEN", "EIGHT", "NINE"
};

もし上記のように const 指定してからコンパイルを実行すれば、number が変更されている行でコンパイルエラーが発生しますので、一瞬で変更箇所を特定することができます。

ではもう一つ質問。このようなミスを未然に防ぐことはできなかったのでしょうか?

できますね!そもそも number の変数宣言時に const 指定しておけば、number をうっかり変更してしまったときにコンパイルでエラーになるので、"FIVE""FlVE" に変更されるようなことは起こり得なかったはずです。

では最後の質問です。今回このようなミスが発生してしまった1番の原因はなんでしょう?

まぁ原因はたくさんあるかもしれませんが、私は1番の原因は、number を変更しないことを “人の頑張り” で達成しようとした点だと思います。

今回は極端な例かもしれませんが、こういったうっかりミスは起こり得るものですし、開発者が多くなると、そもそも number を変更してはいけないということを理解せずに開発している人がいる可能性もあります。

また、今回は "FIVE""FlVE" に変わったことを発見できたので良かったですが、最悪気づかずにアプリをリリースしたり、製品を発売したりしてしまう可能性もあり得ます。

基本的に人間は、どんなに頑張ってもミスや見落としをするものです。人の頑張りには限界があります。

なので、人の頑張りではなく、仕組みで解決することを考えた方が良いです。今回の場合は変更してはいけない変数を変更してしまうというミスが起こりましたが、このようなミスを検知する仕組みや、ミスが起こらないようにする仕組みがあれば、上記のようなミスは発生することはありませんでした。

で、この仕組みが const です。

const 指定しておくことで、上記のような人為的なミス(変数の変更)をコンパイルによりすぐに検知することができますし、コンパイルできなくなるので、変数が変更されることもありません。また、人間のように見落としもありません。

つまり、const は上記のような人為的なミスを防ぐための仕組みになってくれるのです。

特に開発者が多い・ソースコードの規模が大きいと人為的なミスが発生する可能性も高くなります。そういった時は、人の頑張りではなく、const のような仕組みで解決した方が確実かつ簡単です。

このように、人為的なミスを防ぐための仕組みになる点が const のメリットの1つです。

このようなメリットが得られるので、”変更されたくない” 変数に関しては const 指定はした方が良いです。

“使いやすい関数” を作ることができる

また、関数の引数に対する const 指定関数の戻り値に対する const 指定で解説した通り、const を利用することで関数利用者に関数の特性を示すことができます。

特にポインタ引数に const 指定することで、その関数がそのポインタ引数の指すデータを “変更しない” ことを関数利用者に示すことができます。

また、戻り値に const 指定することで、その関数が返却するアドレスのデータが “変更できない” ことを関数利用者に示すことができます。

このように、const 指定によって上記のような関数の特性を示すことで、関数利用者にとって “使いやすい関数” にすることができます。これは、const 指定の有無により、関数実行前にデータの退避を行う必要があるかや、関数の戻り値のアドレスのデータを直接変更しても良いかどうかの判断を簡単に行うことができるからです。

さらに、これらの引数や戻り値への const 指定はプロトタイプ宣言にも反映する必要があるため(const 指定の有無が関数定義とプロトタイプ宣言で異なるとコンパイルエラーになる)、関数利用者はプロトタイプ宣言のみから関数の特性を知ることができます。

特に他の人に使用してもらうような関数を作成するときは、必要がある際には引数や戻り値に対して const 指定を行い、使いやすい関数を作ることを心がけるようにしましょう!

また、const 指定の有無は、関数利用時に非常に重要な情報になりますので、関数利用者はプロトタイプ宣言で示された関数の特性をしっかり汲み取って関数を利用するようにしましょう!

const の注意点

最後に、const 利用時の注意点について解説していきます。

スポンサーリンク

キャストにより const が外されてしまう場合がある

特に const を使用している時にはキャストに注意が必要です。

キャストとは変数や値を他の型に変換することを言います。詳しくは下記ページで解説していますので、興味のある方はぜひ読んでみてください!これを意識するだけでC言語におけるバグの多くを防ぐことができるようになります。

キャスト解説ページのアイキャッチC言語のキャストについて解説!「符号あり」と「符号なし」の比較・計算は特に危険!

注意が必要なのは、キャストにより const 指定が外されてしまう可能性があるからです。具体的にいうと、const 指定された型の変数を const 指定されていない型にキャストすると、const 指定が外されてしまいます。

キャストによって const 指定が外されてしまうと、元々 const 指定されていた変数であっても変更可能になってしまいます。より具体的に言うと、変更してもコンパイル時にエラーが発生しなくなります。

例えば下記の場合、const 指定された c_str が指すデータを変更しているので、今までの解説の通り、コンパイル時にエラーが発生します。

const指定を付けたまま変更
const char *c_str = "AIUEO";

(c_str)[0] = 'a'; /* コンパイルエラーが発生 */

その一方で、下記の場合、const 指定されている c_str をキャスト演算子 (char *) によって const 指定を外してから c_str の指すデータを変更しているので、コンパイル時にエラーは発生しません。

const指定をキャストで外してから変更
const char *c_str = "AIUEO";

/* c_strからconstを外して第0文字を変更 */
((char *)c_str)[0] = 'a'; /* コンパイルエラーは発生しない */

こんな感じで、変更されたくないから変数に const 指定していたのにも関わらず、キャストにより簡単に変更を行うことができてしまいます。

変更されたくない変数が変更されてしまうことで、バグに繋がる可能性は高いです。ですので、const 利用時は、特に const 指定が外れてしまわないように注意が必要です。

暗黙的なキャストでは const 指定が外されたことが検出可能

ただし、暗黙的なキャストの場合はまだ救いがあります。

これは、const 指定が外されるような暗黙的なキャストが発生した場合、コンパイル時に警告が出力されるからです。

MEMO

“暗黙的なキャスト” とは、キャスト演算子 (型名) を指定せずに行われる型変換、つまりコンパイラによって勝手に実行される型変換のことを言います

特に const 指定の場合は、下記の場合に暗黙的なキャストが行われることに注意が必要です

  • 代入時に、左辺の変数の型と右辺の変数の型が異なる場合
  • 関数実行時に、関数の引数の型と関数呼び出し時に指定する変数の型が異なる場合

逆にキャスト演算子を指定して行う型変換を “明示的なキャスト “と言います

この警告を確認することで、const 指定が外されたことを検出することができます。

例えば下記のようなソースコードの場合、p_str = c_strfunc(c_str) を実行する行で暗黙的なキャスト(const char * から char * への変換)が発生しますので、各々の行でコンパイル時に警告が出力されるはずです。

暗黙的なキャストでconst指定を外す
void func(char *p) {
    /* 略 */
}

int main(void) {
    const char *c_str = "AIUEO";
    char *p_str;

    p_str = c_str;

    func(c_str);

    return 0;
}

例えば私の環境では、p_str = c_str を実行する行では下記の警告が、

warning: assigning to 'char *' from 'const char *' discards qualifiers

func(c_str) を実行する行では下記の警告が表示されます(警告の文言は環境によって異なると思うので注意してください)。

warning: passing 'const char *' to parameter of type 'char *' discards qualifiers

こういった警告が出力されるようになりますので、const 指定が外されたことを検出することができます。また、どの行で const 指定が外されたのかも警告の出力内容から特定できるので、あとは const 指定が外されることがないように修正してやれば良いだけです。

明示的なキャストでは const 指定が外されたことを検出不可(コンパイラでは)

ただし、明示的なキャストを行なってしまうと const 指定が外された時にコンパイル時の警告が出力されなくなるので注意してください。

例えば先程紹介したソースコードも、下記のようにキャスト演算子 (char *) を用いて明示的にキャストしてやることで、コンパイル時に警告が出力されなくなります。

明示的なキャストでconst指定を外す
void func(char *p) {
    /* 略 */
}

int main(void) {
    const char *c_str = "AIUEO";
    char *p_str;

    p_str = (char *)c_str;

    func((char *)c_str);

    return 0;
}

もしかしたらコンパイル時のオプション指定で警告が出るようにできるかもしれませんが、少なくとも私の環境で -Wall-Wextra オプションを指定しても警告が出力されませんでした。

また、静的解析ソフトを利用したりすれば、const 指定が外されたことを検出することができるかもしれません。が、とにかく明示的キャストの利用により const 指定が外されたことが検出されにくくなることは間違いないと思います。

で、この明示的なキャストを行うようにして問題が解決されたのかというと、そんなことはありません。プログラムの動作としては、暗黙的なキャストを行なった時と全く同じです。単に警告が出力されなくなっただけです。もっというと、問題が隠されただけです。

暗黙的なキャストで警告が出力されたことの原因は、const が外されたことで “変更されたくない変数” が変更されるようになってしまったことです。

なので、警告を消したいのであれば明示的なキャストを行うのではなく、途中で const が外されないように修正を行うのが筋です。

逆に明示的なキャストを行なってしまうと、警告が出力されなくなるので問題点が検出できなくなり、”修正の必要性” も隠れてしまうことになります。

開発プロジェクトではコンパイル時の警告を0にすることが目標に掲げられてたりするので、とにかく警告を消そうと思って明示的なキャストを行ってしまうケースも多いです。

ですが、上記のように明示的なキャストを利用することで問題が隠れてしまうという危険性もありますので、あまり使用しない方が良いと思います(型を見直すなど、もっと別の解決方法があるはず)。

間接的な変更はできてしまう

ここまで const 指定された変数するとコンパイル時にエラーが発生すると説明してきましたが、実際にコンパイル時にエラーが出力されるのは、const 指定された変数を “直接” 変更した場合のみであることに注意してください。

つまり、他の変数から “間接的に” 変更された場合はコンパイル時にエラーが発生しません。

例えば下記のようなソースコードでは、const 指定された変数 c_str の値を他の変数 p_str から間接的に変更しているため、コンパイル時にエラーは発生しません。

間接的な変更
const int c_data = 100;
int *p_data;

p_data = &c_data;

*p_data = 200;

ただし、このような間接的な変更を行うためには、上記のように const 指定されていないポインタ変数に const 指定された変数のアドレスを代入し、そのポインタ変数から代入されたアドレスのデータを変更するような処理が必要になります。

この「const 指定されていないポインタ変数に const 指定された変数のアドレスを代入」を行う際には必ず const 指定を外すキャストが発生しますので、コンパイル時に const 指定が外された旨の警告が出力されます。

なので、キャストにより const 指定された時同様に、コンパイル時の警告を確認することで、const 指定された変数が間接的に変更されたことを検出することが可能です(もちろん明示的なキャストを行なっていなければの話です)。

(参考)const 指定された変数の値を変更するとどうなるのか?

ここまでの注意点で、const 指定された変数であっても、キャストや間接的な変更により値が変更できてしまうことを説明しました。

const 指定された変数は変更しない” が大原則ですが、実際に変更してみたらプログラムがどのように動作するのかも興味ありますよね?

ここでは参考として、const 指定された変数を変更した時のプログラムの動作を簡単に紹介したいと思います。

前置きしておきますが、ここから紹介するのは私の環境で試した時のプログラムの動作です。環境が変わると動作も変わる可能性もあります。が、環境によってはここで紹介するようなプログラムの動作になることを考えると、const 指定された変数の値は変更しない方が良いことが実感できると思います。

const 指定されたローカル変数の値を変更してみる

まず下記のようなソースコードをコンパイルして実行してみました。最後に printf で出力される result = c_data * 10 の結果はいくつになるでしょう?

const指定された変数の値の変更1
#include <stdio.h>

int main(void) {
    const int c_data = 100;
    int *p_data;
    int result;

    p_data = &c_data;

    *p_data = 200;

    result = c_data * 10;

    printf("%d\n", result);

    return 0;
}

c_data の値を p_data から間接的に 200 に変更しているのですから、本当であれば printf で出力される値は 2000 になるはずです。

ですが、私の環境では 1000 と表示されました。明らかに結果おかしいですよね!ちなみに c_dataconst 指定を外せば 2000 が表示されます。

おそらくですが、c_dataconst 指定されているので、result = c_data * 10 の時点で c_data の値は初期値の 100 から “変更されているはずがない” とコンパイラが解釈したのだと思います。

変更されているはずがないので、c_data は変更後の 200 ではなく、初期値のままの 100 を使って result = c_data * 10 が計算され、結果として 1000 が表示されたのだと考えると話が合います。

こんな感じで const 指定された変数の値を変更しても、その変更結果が反映されないことがあるようです。

const 指定された static 変数の値を変更してみる

次は下記のように、先ほど紹介したソースコードの const int c_data = 100 の部分を static const int c_data = 100 に変更してみました。要は static 指定を追加しています。

const指定された変数の値の変更2
#include <stdio.h>

int main(void) {
    static const int c_data = 100;
    int *p_data;
    int result;

    p_data = &c_data;

    *p_data = 200;

    result = c_data * 10;

    printf("%d\n", result);

    return 0;
}

変更後のソースコードをコンパイルして実行すると、*p_data = 200 の処理時にプログラムが落ちて異常終了しました。

変数宣言を行われた変数はメモリ上に配置されることになりますが、static 変数やグローバル変数を const 指定して変数宣言した場合は、そのメモリの “読み取り専用領域” に配置される(ことがある)ようです。リテラルなども、この “読み取り専用領域” に配置されます。

また、この “読み取り専用領域” に配置されたデータを変更してしまうと、プログラムが落ちて異常終了するようになっています。

なので、上記のように static const 指定された変数を変更すると、プログラムが落ちて異常終了してしまいます。

こんな感じで const 指定された変数の値を変更すると、プログラムが落ちて異常終了するような場合があります。

const 指定された変数を変更しても良いことがないことを理解していただけたのではないかと思います。

スポンサーリンク

まとめ

このページではC言語における const について解説しました!

変数に対して const 指定することで、その変数を “変更不可” にすることができます。より具体的には、その変数を変更しようとするとコンパイルエラーが発生するようになります。

変更されたくない変数があれば、積極的に const 指定しておくようにしましょう!これにより人為的なミスでその変数が変更されることを防ぐことができます。

また、関数の戻り値や引数に対して const 指定することで、より使いやすい関数に仕立てることもできます。

const を利用しなくてもプログラムを作成することができますが、特に複数人での開発をスムーズ進める時に有効な修飾子になります。

是非今回紹介したメリットを頭に入れておき、積極的に、かつ適切に const を利用するようにしましょう!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です