7.15 型修飾子
7.15.1 定数化
これまで、宣言とは記憶クラス指定子(static など)、型指定子(int , char など)、宣言子(変数名、関数名など)、初期化子(変数の初期値)を組み合わせて行われるということを説明してきました。型修飾子は、これらに加えて宣言対象の性質を決定するキーワードを提供します。これを宣言に加えて考えると、C 言語の宣言構文とは常に次のような形であると考えることができます。
記憶クラス指定子 型指定子 型修飾子 宣言子 初期化子 ...;
記憶クラス指定子、型指定子、型修飾子はどのような順番でもかまいません。また、記憶クラス指定子、型修飾子、初期化子は省略することができます。組み合わせによっては、型指定子を省略することもできます。宣言子は変数の名前などを指定する、これまで「識別子」と呼んできたものです。
型修飾子で最も使われるのは const キーワードです。これは、初期化以降、この変数が変更されないということを表します。
const int ciValue = n;
この場合、整数型の変数 ciValue は未来永劫、保存している値 n が変更されることはないことを表します。もし、プログラムの中でこの変数の内容を書き換えるようなことを行った場合はコンパイルエラーとなります。因みに const 型修飾子を用いた宣言で型指定子を省略した場合は int 型であると解釈されます。
例えば "Kitty" という文字列をプログラムの多くの場所で用いる場合、そのたびにリテラル文字列を指定するのは極めて効率の悪いコードとなります。ある日、プログラムを改良するために文字列を "Kitten" に変更しなければならなくなった時、全てのリテラル文字列を変更しなければなりません。もし、1ヶ所でも書き換えるのを忘れてしまうと、バグとなる可能性があります。つまり、整合性が低いプログラムとなってしまうのです。これは、計算用の整定数を記述する場合も同様です。
そこで、あなたは頻繁に利用される情報は、ひとつのグローバル変数などに初期化して、これを使い回そうと考えるでしょう。確かにそうすれば、プログラムの仕様変更時に書き換えるソースの場所は限られ、整合性が保たれます。しかし、そうなると新しい不安が沸き起こります。変数は、誰かが(何らかの関数が)不正に書き換えてしまう可能性があるのです。そこで、const を使って変数を守るのです。
多くの位置で参照され、変更されることを望まない静的な情報といえば、アプリケーションの名前やバージョン情報、会社名などでしょう。これらの情報をアプリケーションが保有するのは極めて一般的です。
#include <stdio.h> typedef unsigned char BYTE; const char APPNAME[] = "Kitty on your lap"; const VERSION = (15 << 8) | 7; int main() { /* APPNAME[1] = 'X'; /*エラー*/ /* VERSION = 0; /*エラー*/ printf("APPNAME = %s\n" , APPNAME); printf( "VERSION = %d.%d\n" , (BYTE)VERSION , (BYTE)(VERSION >> 8) ); return 0; }
コード1は、文字配列の変数 APPNAME と、整数型の変数 VERSION を外部レベルで宣言しています。これらの変数には const 型修飾子が指定されているため、定数のようにその値は常に同じです。これらの変数は、式内において参照は通常の変数と同じように行うことができますが、代入することはできません。コメント化されている代入式をコンパイルすれば、エラーが出ることを確認することができるでしょう。
APPNAME 変数のように、配列に対して const を指定した場合は、配列の各要素に対して const がかかります。また VERSION 変数のように const 型修飾子のみの宣言の場合、const int 型であると解釈されます。VERSION は、下位 8 ビットにメジャーバージョンを、次の8ビットにマイナーバージョンを格納した16ビットの整数情報として扱っています。
これは、const を利用した一例です。このほかにも、多くの場で const は利用されます。例えば、関数が与えられたポインタを変更しないのであれば、それを開発者に明示的に伝える手段として const 型の引数を宣言する方法があります。ポインタ型に const 型修飾子が指定されている場合は、そのポインタではなく、ポインタが指す値を変更しないことを表します。
リテラル文字列へのポインタから間接参照を行い、リテラル文字列の値を変更しようとした場合の結果は不定です。これは、どの開発者にとっても好ましいことではありませんが、構文上は間違いではないのでバグの原因となる可能性があります。そこで、リテラル文字列へのポインタを作成する場合、経験あるプログラマは const を習慣的に指定します。
#include <stdio.h> void Function(const char *str) { /* str[0] = 'X'; /*エラー*/ /* str = "あなたのひざの上の猫"; /* OK */ printf("%s\n" , str); } int main() { Function("Kitty on your lap"); return 0; }
コード2の Function() 関数は const 型修飾子を持つ文字列へのポインタを受け取ります。ポインタは間接参照を使ってポインタが指すアドレスの値を変更することができません。ただし、const ポインタが保有するアドレス自体は変更することができるので、str パラメータに他のアドレスを代入することは可能です。
これとは逆に、ポインタを定数化したい場合があるでしょう。つまり、ポインタが参照する変数の値は変更してもよいが、ポインタそのものは変更してはならないというポインタです。コード2のように const char *str と宣言した場合、定数データへのポインタであると解釈されるため、ポインタそのものは固定されません。つまり、次のような性質を持ちます。
const char *str = buf1; /*定数へのポインタ*/ *str = 'a'; /*エラー*/ str = buf2; /*正しい*/
定数へのポインタは、間接参照によって参照先の値を変更することができないポインタであり、ポインタそのものが定数というわけではありません。ポインタそのものを定数として宣言したい場合は const キーワードの位置を * の後に持ってきます。つまり char *const str というように宣言するのです。
char *const str = buf1; /*定数ポインタ*/ *str = 'a'; /*正しい*/ str = buf2; /*エラー*/
定数へのポインタと、定数ポインタは宣言が似ていますが異質のものなので、その違いを認識できるようになりましょう。
このように const は様々な場面で利用されます。特に、値を変更することがない変数に関しては積極的に const を利用するべきだと考えられています。そうすることによって、システムは変数を読み出し専用のメモリ領域に配置させ、より高速な動作を実現することができる可能性があるからです。もちろん、こうした最適化処理はコンパイラやシステムに依存する問題なので、開発者が意図するものではありません。
7.15.2 最適化拒否
const に続くもうひとつの型修飾子に volatile キーワードが存在します。この型修飾子は、よほど特殊なソフトウェアやシステムの開発を除いて用いることはないでしょう。volatile は、値が常に可変であり、システムが最適化処理をしてはいけないことを明示的に表します。次の文は、volatile を指定した数値型変数の宣言です。
volatile int viValue = n;
volatile 型修飾子を指定した場合は const 同様に型指定子を省略することができます。型指定子が省略されている場合は volatile int として解釈されます。
volatile は、これを実行しているプログラム以外の未知のプロセス(OSや特殊なハードウェアなど)が変数を利用する場合に、コンパイラが勝手に意図しない形に最適化することを防ぎます。もちろん、多くの場合は外部プロセスが値を変更することを望まないため、独立したソフトウェアは volatile を指定することはありません。
#include <stdio.h> volatile viVariable = 0xFF; const volatile char *str = "Kitty on your lap"; int main() { printf("viVariable = %d\nstr = %s\n" , viVariable , str); return 0; }
このプログラムでは、volatile 型修飾子を指定している整数型変数 viVariable と文字列へのポインタ str を宣言しています。これらの変数は、他の権限のあるプログラムが変更する可能性を示していますが、当然、システムやハードウェアと通信を行わなければ変更されることがあるはずもなく、このプログラムの volatile 型修飾子は、実質的な意味を持ちません。
因みに str 変数の宣言を見て分かるように、const と volatile 型修飾子は同時に指定することができます。この場合 const が指定されているため、このプログラムからは変数の内容を変更できないことを表しています。しかし、システムやハードウェアからの変更は受け付けることを意味しています。