Cの型システムではchar *
とconst char *
とconst char *const
は別の型として区別されます。今回の動作を厳密に理解するには、const
とは何か、lvalue(左辺値)とrvalue(右辺値)の違いを知る必要があります。また、C++だと少し異なるのでそれを交えて説明していきます。
const
は「変更不可」を表す修飾子です。const
が付いた型の部分へ代入等による破壊的な変更を行おうとした場合、コンパイラはエラーとし、変更されないことを保証します。また、変更されないことを前提にコンパイラは最適化を行い、より効率的に処理ができるようになります。
では、何が「変更不可」となるのかを見ましょう。const
はconst 型
または型 const
と型にかかる場合と、*const
とポインタにかかる場合があります。型にかかる場合はその型であるデータの部分が「変更不可」になり、ポインタの場合はポインタそのものが「変更不可」になります。
例として、const char *const a
を見ましょう。a
というconst char
へのポインタの値を変更できません。また、*a
とaが示す先のデータの値も同じく変更できません。コレとは違い、const char *b
とした場合は、b
というポインタ自体は変更できますが、*b
とbが示す先は変更できません。char *c
はどれもこれも変更可能になります。char *const d
は少し奇妙で、d
自体のポインタ値は変更できませんが、*b
という示す先のデータの値は変更できます。
C
1const char *const a = "abc";
2const char *b = "def";
3char *c = "ghi";
4char *const d = "jkl";
5a = "hoge"; // エラー
6*a = 'A'; // エラー
7b = "hoge"; // OK
8*b = 'A'; // エラー
9c = "hoge"; // OK
10*c = 'A'; // OK だが、別の理由で動作は未定義
11d = "hoge"; // エラー
12*d = 'A'; // OK だが、別の理由で動作は未定義
さて、最初に区別すると言いましたが、完全に別扱いでは不便です。そこで、const
には暗黙の型変換(キャスト)というのがあります。制限が緩い方(const
がない)からきつい方(const
がある)、つまり、const
を追加する場合は暗黙に型変換してくれます。
C
1char *a = "abc";
2const char *b = "def";
3b = a; // OK
4a = b; // 警告
5a = (char *)b; // 明示的にキャストすればOK
逆ができないのはint
とlong long
で大きい方にはできるが小さい方にはできないのと同じです。明示的にキャストすればコンパイルが通るのも同じです。(C++では専用のconst_cast
が用意されています)
ということで、char *
にconst char *
を入れようとしても警告(C++ではより厳密になるためエラー)になります。なので、const char *
にするのです。あれ、でも、関数の戻りの型はconst char *const
でしたよね?const
が一つ多いし、const char *
には入らないのでは思うことでしょう。いや、思ってください。
今から説明するのは、一度でうまく理解できるのは難しいと思います。私もうまく説明できるかは自信がありません。ただ、Cをよく理解し、C++へ繋げて行くには必要な知識だと思っています。
まず、lvalue(左辺値)とrvalue(右辺値)の説明をします。代入演算子(=
)の左辺に来る式の結果の値をlvalue、右辺に来る式の結果の値をrvalueと呼びます。左と右、単純ですね。ここで重要なのは、const
はlvalueにだけしか適用されず、rvalueでは「変更不可」の意味を失うと言うことです。つまり、rvalue自体にかかっているconst
は失われると言うルールです。
C
1const int x = 0;
2int y = 1;
3x = y; // エラー
4y = x; // OK
x = y
がエラーになる理由は単純です。x
は「変更不可」にしたので代入による変更はできません。では、y = x
はどうでしょう。y
はint
型、x
はconst int
型と型が違っています。const
を足す方への暗黙の型変換は問題ないけど、const
を外す方は明示しないと無理だと行ったはずなのに、警告もエラーもありません。その理由は何か?それは、y
はrvalueなのでconst
が失っているからです。
考えてみてください。代入は値のコピーです。x
の値はコピー元ですので、コピー先のy
がこの後どのような値に変わっても、x
の値そのものが変更されることは決してありません。ですので、rvalueのときは、値そのものに対するconst
を保持する必要がなくなるのです。
おおっと、ここでもう一つ注意点があります。あくまで失われるのは値そのものに対するconst
だけです。const char *const a;
と言う形だった場合はa
そのものにかかるconst
だけが外れてconst char *
型になります。これはコピー元のa
は示す先のデータは、コピー先をゴニョゴニョすることで書き換えることができるからです。示す先を「変更不可」として守るために、その部分に対する「変更不可」であるという情報は残しておかなくてはなりません。
これで、問題にあった関数の戻りの型も理解できましたね。const char *const
という戻り値の型でしたが、rvalueですのでconst char *
になったと言うことです。なので、同じ型であるconst char *a
には警告もエラーも無く代入できるようなったと言うのがこのコードの動きになります。
※ 気付いたと思いますが、関数はconst char *month_name(int n)
としても問題ありません。return
で指定しているmonths[n - 1]
はconst char *const
型ですが、これもrvalueであるため、この時点でconst char *
になっています。二番目のconst
はCでは全く意味が無いことになります。(C++は異なります)
※ C++だともう少し複雑です。C++では、lvalueとrvalueの定義が異なっており、また、rvalue reference(右辺値参照)というのがあり、関数の戻り値でもconst
付きは意味がある物に変わります。詳しくはconst rvalue referenceは何に使えばいいのか - ここは匣を見てください。すいませんが、私もよくわかっていません。
参考文献: const type qualifier - cppreference.com
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。