見出し画像

C言語教室 第34回 - 型修飾子あれこれ

さて、昔のC言語には無かったのと、ブラウザ環境などで使えない処理系もあることから、int や char などを修飾する const などについての説明を端折って来ましたが、そろそろ触れておきます。


const

const というのは「変更できない」「読み出し専用」という意味があり、これを int につけて、

const int constant = 10;

のように使います。これで constant は初期化で値を設定するのみで、値を代入することができなくなり、もし代入すればコンパイルエラーとなります。

[cc.c]

void main() {
  const int constant = 10;
 
  constant = 20;
}

と、const をつけた変数に代入するコードを書くと、以下のエラーメッセージが表示されコンパイルできません(gccの例)。

cc.c: In function 'main':
cc.c:3:12: error: assignment of read-only variable 'constant'
3 |   constant = 20;
  |            ^

ちょっと恐ろしいのは const の付いた変数は初期化の際に初期値を書く必要があるはずなのですが、これを省略しても何らかのエラーにならないことがあります(gcc では何も起きない)。もちろんどこかで値を代入していれば、そこでエラーにはなるのですが、必ず未定義の値を持つ変数が出来上がってしまったりします。const をポインタ変数に対して使うときは、少しばかりややこしいことになるので、これはあらためて説明します。

volatile

const の逆という言い方も妙かもしれませんが、代入もしていないのに、値が変わる(かもしれない)変数というのもあります。普通にプログラムを書いているときにはまずお目にかからないのですが、ハードウェアを相手にしているときや共有メモリであるとかで、プログラムがまったく操作をしなくても、その値が変わる恐れがあるときに、そのメモリを指す変数に volatile という修飾子をつけて、コンパイラが最適化などを行う時に、値が変わらないことを想定しないでくれと指示します。これについては余程のことがなければ知らなくても良さそうです。

auto と register

それから、今は殆ど使われなくなったものに auto と register があります。ローカル変数を宣言する時に、特に何も断らずに

int a, b;

などと書きますが、このようにスタック上に確保されるローカル変数のことを auto 変数と呼び、本来であれば

auto int a, b

などと書くことになっているのですが、auto は省略可能なので普通はいちいち書きません(BASIC の LET 命令みたいなもの)。

またローカル変数に対して register を付けると、処理系はこの変数をスタックではなく CPU の持つレジスタを使うように「努力」します。実際には人間よりもコンパイラのほうが賢いことが多く(コンパイラのほうが変数がどう処理されるのかよくわかっている)、殆どの場合は、この指定は無視され、より効率の良い変数がレジスタに割り当てられることになります。ただし、ひとつだけ有用な使い方があって、registerに変数が割り当てられると、当然ですが、この変数のアドレスを取ることができません。そこで、どうもどこからか参照経由でアクセスされているような気配があるときに、その変数にregister宣言をすることにより、参照をとっているところがエラーになるので、見つけることができたりします。こんな使い方はとてもオススメできるようなものではないのですが、そんなこともあるんだよとだけ知っていただければ幸い。

[reg.c]

void main() { 
 register int i = -1;
 int *p = &i;
}

というコードをコンパイルすると、以下のエラーメッセージが出ます。

.\reg.c: In function 'main':
.\reg.c:3:3: error: address of register variable 'i' requested
3 |   int *p = &i;
  |   ^~~

restrict

変数の型はローカルであるとかグローバルな変数での宣言だけではなく、関数の戻り値や引き数の宣言でも登場します。呼び出し側の型と呼び出される側の型が一致しなければ、暗黙の型変換が行われますが、値渡しの場合は引き数で渡された変数の値が関数内で変更されてもされなくても、呼び出し側の変数の値が変わる訳ではないので、あくまで関数の中でどう扱われるかだけなのですが、ポインタ型の場合には注意が必要です。

ポインタ型固有の話ですが、異なるポインタ変数で同じ領域を指すと、先の volatile では無いですが、そのポインタを使って参照先を書き換えることをしなくても、他のポインタを経由して参照先が書き換えられることがあります。このためコンパイラが充分な最適化が出来ないことがあるので、「そんなことはしていない」ということを明示するために restrict という修飾子を付けることがあります。ピンと来ないかもしれませんが、この修飾子はライブラリ関数のプロトタイプ宣言などでよく見かけます。

具体例をあげると

void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
void *memmove(void *s1, const void *s2, size_t n);

この違いは、これら2つの関数は、どちらもs2をs1にn文字コピーしますが、memcpyは領域の重なり合わないオブジェクト間でコピーする必要があり、memmoveにはそんな制限はなく、重なり合った領域をコピーすることが出来ます。memcpyの方が制約がありますが、よりコストの低い処理が期待できます。

inline

これは関数版の register のようなものと言っても良いかもしれませんが、関数の宣言及び定義に inline があると、コンパイラはこの関数を呼び出した場所に「埋め込んだ」コードを生成し、関数を「呼び出す」事をしなくなります。ただし実際に埋め込むかどうかはコンパイラが判断し、その判断は最適化レベルの指定で変わってきます。ですからこの指定があっても実際に関数が埋め込まれるか、普通の関数呼び出しとして扱われるかはケースバイケースでコンパイラ任せです。

古いC言語には inline 指定はなく、もっぱらプリプロセッサによるマクロを使うことで、処理を埋め込んでいました。マクロを使うとC言語としての型チェックなどが使えないので、inline を使うほうが安全なのですが、一般的には関数を埋め込むためには宣言だけではなく、その中身も予めわかっている必要があるので、ソースコード的に呼び出す前に関数全体が書かれている必要があります。これはマクロでも同じなのですが、ヘッダファイルに関数の中身を置く必要があり、そうすると必ず1度だけしか登場してはいけないので、インクルードガードを使ってしっかりと保護してあげないといけません。

また、関数が埋め込まれてしまうと、その関数の名前はどこにも残らないので、デバッガで、その関数を探してもどこにも存在しません。ブレークポイントを設定して、なんていうことも出来なくなります。もっともデバッグモードでコンパイルするようなときは最適化しない設定になることが多く、関数が埋め込まれないので大丈夫なのですが、今度はヘッダファイル内に関数があることにより、複数の同じ名前の関数が出来上がってしまうので、リンク時のエラーにならないように、通常のインクルードガードの書き方だけでは足りず、もうひと工夫が必要になることもあります(static inline な関数にするとか)。もうひとつ、コンパイラのオプションによる指定だけではなく、プリプロセッサ命令 #pragma inline オプションによって、インライン展開を個別に指定することも可能な処理系もあります。

このように、とても便利で強力な機能なのですが、なかなか癖があるので、初心者はあえて使わずに、ありものを使う時に注意するだけにしておいたほうが無難かもしれません。もし使いたいときには、使う処理系のドキュメントを良く読んで、使い方を確かめてから使ってください。


課題

さて、次回はconstが付いたポインタ変数の説明をする予定です。その前に予習も兼ねた課題を出しておきます。

課題

char s[] = "abcdefg";
const char *p = s;
char * const q = s;

このとき、pとqで何が違うのかを説明しなさい。

ヘッダ画像は、いらすとや さんより
https://www.irasutoya.com/2019/11/blog-post_67.html

#C言語 #プログラミング講座 #const #volatile #auto #register #restrict #inline


この記事が気に入ったらサポートをしてみませんか?