C言語のconstの使い方
今までconst
の書き方がよくわかっていませんでした。
で、調べたら予想以上に難しかったです。
本投稿ではconst
の使い方を記載していくわけですが、
規約を調査したわけではなく、Cコンパイラで実験した内容です。
もしかしたらおかしい所があるかもしれません。
実験に使用したコンパイラは、FreeBSD 11.1 clang, Gentoo Linux gcc, Windows 10 Visual Studio 2017付属のやつです。
const
の意味
const
とは定数を宣言するときに使います。
書き込み不可という意味が強いと思います。
簡単な例としては下記の通り。
const int a = 100;
別の書き方もあります。
int const a = 100;
意味は同じになります。
あるいは2つ書いても同じです。
const int const a = 100; /* 警告 */
しかしconst
を重複させるのはダメのようで、コンパイル時に警告が出ました。
あと、古いC言語だと、int
に限って省略できたはず。
つまり、
const int a = 100; は const a = 100; /* 警告 */
と記載できます。
でもこれは今のC言語だと規約レベルでダメだったような記憶があります。
clangとgccでは警告が出ました。
では、もし値を代入しようとした場合はどうなるでしょうか。
下記の例を示します。
const int a = 100; a = 200; /* エラー */
この場合は、コンパイルエラーとなりますので、実行できません。
なんとかして無理やり代入するとどうなるでしょうか。
#include <stdio.h> int main() { const int a = 100; *((int *)&a) = 200; /* 危険 */ printf("%d\n", a); return 0; }
実行結果
$ cc main.c $ ./a.out 200 $
やったね、うまく行きました。
でも確かこれはかなり危険だったはず。
上記の例はコンパイラとOSによって挙動が変わります。
const
の定数は、書き込み不可のメモリ領域に配置することが許されています。
実行例では書き込み可能な領域に配置されたようですが、
もし書き込み不可の領域を書き換えようとした場合は、
OSレベルにて不具合が生じるため、最悪Segmentation violationコースとなります。
上記の実行はGentoo Linux+gccによるものです。
FreeBSD+clangでは、なぜか100が返却されました。
const
ポインタの書き方
const
はポインタにも使用できます。
詳しく見ていく前に、まずは書き方から。
通常の変数の場合、const
は、重複と省略を考慮しないのであれば、
次の2通りの方法があると説明しました。
const char a; char const a;
ポインタの場合は、ポインタを表すアスタリスク*
が一つ増えるごとに、
const
の書ける位置が1つずつ増えていきます。
char
のポインタであるchar *
の場合は、次の3通りの位置に記載できます。
const char *a; char const *b; char *const c;
a
とb
は同じ意味となります。
ではポインタのポインタの……ポインタの場合はどうなるでしょうか。
例えば、
char ******a;
の全てにconst
をつけたものは、次のどちらかになります。
const char *const *const *const *const *const *const a; char const *const *const *const *const *const *const a;
アスタリスクが6個で、const
の書ける場所は8か所。
そのうち、上記の2例は同じ意味なので、
値を定数として指定できるのは7か所ということになります。
もうこの時点で簡単ではないです。
const
の記載する位置は、一見規則正しく並んでいるようなのですが、
左から一番目と二番目が同じ意味であり、かつ重複不可なので混乱するのです。
ではconst
の位置によって何が変わるのでしょうか。
引き続き、
char ******a;
をconst
にする場合を考えて行きます。
変数a
は、式で宣言したときと同じ数のアスタリスクを付けると、
指定した型そのものになります。
つまり、******a
の型はchar
なので、
******a = 'Z';
みたいに書けるわけです。
このchar
をconst
として定数と宣言したい場合は、
一番左側にconst
を付けます。
一番左と言っても書き方は二通りあるため、
例えば次のどちらかとなります。
const char ******a; char const ******a;
一方、式でアスタリスクを一つもつけない場合は全く逆となります。
つまり、a
の型はchar ******
であり、
const
を指定したい場合は一番右側にconst
を付けます。
char ******const a;
*a
をconst
にしたい場合は、
char *****const *a;
**a
をconst
にしたい場合は、
char ****const **a;
と順番にずれていくわけです。
const
を2つ以上宣言することも可能であり、**a
と***a
をconst
にしたい場合は、
char ***const *const **a;
となります。
初期化と代入
初期化とは、変数宣言時に値を設定することです。
例えばこんな感じ。
int a = 100;
代入とは、変数に値を格納することです。
例えばこんな感じ。
a = 100;
const
変数を初期化する、あるいは代入する場合は、
両辺の各const
がどうなっているのかを合わせて調査して行き、
問題がある場合はエラーか警告が出力されます。
このチェックは、次の3段階に分けて行われます。
- 右から1番目の
const
- 右から2番目の
const
- 右から3番目以降の
const
これらをひとつずつちゃんと説明していきます。
右から1番目のconst
右から1番目のconst
とは、例えば
int ****const a;
のような場合です。
これは変数そのもののconst
なので、
代入は禁止されますが初期化は禁止されません。
初期化とは
int ****const a = b;
みたいなものです。
初期値を与えられなければ定数にもできないので、
当然有効な宣言となります。
一方、const
指定されたということで、
a = b;
とするのは値を変更することになるのでエラーです。
当たり前のことですよね。
でも、右から1番目のconst
は、右から2番目、3番目とは違って、
ポインタとは一切関係がないと覚えておくといいと思います。
右から2番目のconst
例えば、
const int ****const *ptr;
のような場合です。
よく文字列を扱うときに、
const char *ptr; char const *ptr;
と宣言しますが、まさにこの場合が該当します。
右から2番目のconst
は、それ以外のconst
とは違っていて特別な判定がされます。
初期化と代入で、チェックの内容は変わりません。
例えば、下記の場合を考えます。
a = b;
もしb
よりもa
の方が制限がきつくなる場合はOKです。
しかし逆にb
よりもa
の方が制限が緩くなれば警告が発せられます。
つまり、せっかく値をconst
で保護をしていたにも関わらず、
それを解除するような代入をする場合は警告になるのです。
次の宣言があったとします。
const char *a; char *b;
このとき、
a = b; /* OK */ b = a; /* 警告 */
となります。
ちなみにこの右から2番目のチェックは、
違反していた場合はコンパイルエラーではなく警告が出力されます。
たぶんコンパイルは継続されるので実行ファイルができてしまいます。
しかし正しいと思わずにちゃんと原因を突き止めるべきであり、
もし問題ないならば明にキャストしましょう。
右から3番目以降のconst
(gcc, clang)
恐ろしいことにVisual Studio 2017と挙動が異なりました。
まずはgcc, clang編。
3番目以降は、初期化か代入を行う際には、
const
と非const
が全て同じでなければなりません。
右辺にconst
と指定されていたら、左辺もconst
です。
2番目みたいに、左辺const
で右辺非const
は許されません。
左辺が非const
なら、右辺も非const
でなければなりません。
こちらも違反した場合は、エラーではなく警告が出力されます。
それでは例をあげます。
char ****a = NULL; char const *const *const *const *const b = a; /* 警告 */
右から1番目、2番目はOKですが、3番目以降のconst
が
合っていないので違反です。
char ****a = NULL; char ***const *const b = a; /* OK */
3番目以降が全て非const
なのでOKです。
char const **const **const a = NULL; char const **const *const *b; b = a; /* OK */
このとき、
aは(const, なし, const, なし, const)です。 bは(const, なし, const, const, なし )です。
右から1番目は、const→なしなのでOK。
右から2番目は、なし→constなのでOK。
右から3番目以降は、全て一致するのでOK。
右から3番目以降のconst
(Visual Studio 2017)
コンパイル間で挙動が変わったので、Visual Studio 2017編です。
こちらは単純に、右から2番目と同じです。
つまり、非const
→const
への値の変更は許されます。
なので下記の例
char ****a = NULL; char const *const *const *const *const b = a;
は、gcc, clangではエラーでしたが、 Visual Studio 2017では問題なくコンパイルが通りました。
もし移植性を考慮するなら、こちらではなくより厳しいgcc, clangの方に 合わせればいいと思います。
修飾子の複合
c89時点でC言語の修飾子は6個あると記憶しています。
register auto extern const static volatile
今はもっとあるんでしょうか、知らないですけど。
const
と同じように記載できるのは、volatile
とrestrict
だそうです。
なんですかrestrict
って。
c99から出てきたようですが、あまりよく知らない人なので今回は無視。
それで、これらを複合すると、一見してよくわからないことになったりします。
例えばchar *
にconst
とvolatile
を合わせたい場合はどうしたらいいでしょうか。
volatile
の記載する位置は、const
と変わりません。
そして、const
とvolatile
は、同じ位置に順番は関係なく記載できます。
例えば下記の通り。
char *a; /* 通常のポインタ */ const volatile char *b; /* charにconstとvolatile */ char volatile const *c; /* bと同じ */ const char *volatile d; /* charがconstでポインタがvolatile */ volatile char const *const volatile e; /* charもポインタもconst volatile */
ではvolatile
の初期化と代入は、const
とはどう違っているのでしょうか。
簡単に説明すると次の通り。
- 右から1番目は、
volatile
では制約は無し - あとは
const
と同じ
ではconst
とvolatile
が合わさって宣言された場合はどうなるのか。
ただconst
とvolatile
を分けて考えればいいだけです。
例えば次の通り。
const char *a; const volatile char *b; b = a; /* 問題なし */ a = b; /* エラー */
続いて、次の例を考えます。
volatile char const *volatile const *volatile **const a = NULL; char const volatile *const *volatile *volatile *volatile b; b = a; /* エラー */
このとき、
aは(v+c, v+c, volatile, なし, const) bは(v+c, const, volatile, volatile, volatile)
右から1番目は、volatile
は制約なし、const
→なしとなるのでOK。
右から2番目は、なし→volatile
なのでOK。
右から3番目以降は、4番目がv+c
→const
なのでエラー。
引き続き、次の例を考えます。
volatile char const *const *volatile **const a = NULL; char const volatile *const *volatile *volatile *volatile b; b = a; /* OK */
このとき
aは(v+c, const, volatile, なし, const) bは(v+c, const, volatile, volatile, volatile)
つまり、代入は問題なしです。