nptclのブログ

Common Lisp処理系nptの開発メモです。https://github.com/nptcl/npt

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;

abは同じ意味となります。
ではポインタのポインタの……ポインタの場合はどうなるでしょうか。
例えば、

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';

みたいに書けるわけです。
このcharconstとして定数と宣言したい場合は、 一番左側にconstを付けます。 一番左と言っても書き方は二通りあるため、 例えば次のどちらかとなります。

const char ******a;
char const ******a;

一方、式でアスタリスクを一つもつけない場合は全く逆となります。
つまり、aの型はchar ******であり、 constを指定したい場合は一番右側にconstを付けます。

char ******const a;

*aconstにしたい場合は、

char *****const *a;

**aconstにしたい場合は、

char ****const **a;

と順番にずれていくわけです。

constを2つ以上宣言することも可能であり、**a***aconstにしたい場合は、

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番目以降のconstgcc, 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番目以降のconstVisual Studio 2017)

コンパイル間で挙動が変わったので、Visual Studio 2017編です。
こちらは単純に、右から2番目と同じです。
つまり、非constconstへの値の変更は許されます。

なので下記の例

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と同じように記載できるのは、volatilerestrictだそうです。
なんですかrestrictって。
c99から出てきたようですが、あまりよく知らない人なので今回は無視。

それで、これらを複合すると、一見してよくわからないことになったりします。
例えばchar *constvolatileを合わせたい場合はどうしたらいいでしょうか。
volatileの記載する位置は、constと変わりません。
そして、constvolatileは、同じ位置に順番は関係なく記載できます。
例えば下記の通り。

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と同じ

ではconstvolatileが合わさって宣言された場合はどうなるのか。
ただconstvolatileを分けて考えればいいだけです。
例えば次の通り。

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+cconstなのでエラー。

引き続き、次の例を考えます。

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)

つまり、代入は問題なしです。