C/C++ / const結合性

C/C++で変数宣言するときに「変更されることがない」という宣言として、const修飾子を使用します。


const int MAX_COUNTER = 100;

文字列リテラルを宣言する場合もよく使います。


const char* MY_MESSAGE = "Hello, World!\n";

この場合、変更されることがないと宣言されているのはchar型に対してになるので、 次のように再代入が可能です。


MY_MESSAGE = "Howdy World!\n";

これを防ぐには、宣言の時点で、


const char* const MY_MESSAGE = "Hello, World!\n";

このように、const char*がconstであることをコンパイラに指示します。

さて。これがネストしたり、順序が入れ替わったりしていくと大変分かりにくく混乱を引き起こします。


const char * FOO;...(1) //文字列リテラル

char const * FOO;...(2) // ???

char * const FOO;...(3) // ??? ???

const char * const FOO;...(4) // ??? ??? ???

char const * const FOO;...(5) // ??? ??? ??? ???

理解する肝は「constは*(実体化)より結合性が強い」と覚えることです。

結合性が強いということは、コンパイラが先に処理するように決まっているということです。四則演算でいうと積と商が和、差より結合性が強いと習ったやつと同じことです。


1 + 2 * 3 = 1 + (2 * 3) = 1 + 6 = 7

結合性に関係なく括弧でくくればそちらを先に処理します。 上記ではあえて、積を括弧で括って結合性が強いことを強調しています。

ちょっと脱線

昔、塾講師をしていて数学を担当していたことがあります。 ある日、英語が専任ながら、たまたま中学生に算数を教える羽目になっていた別の先生から、

「生徒さんから、『なんで括弧の中を先に計算しなくちゃいけないんですか?』って聞かれたんだけど……。なんで?」

と、いう質問を受けました。 考えてみれば……確かに。

数学(算数)というと厳密に何か絶対的な"正解"というものがあるという印象です。 ですから、件の生徒さんは、何かしら『数学的な正解』があって、『括弧を先に計算する』という必要があるように考えられたのでしょう。

この辺の感覚はプログラミング言語が数学的な圏論なんかの話に波及する分野であることに繋がるので、なかなか面白いと思います。

とにかく、このときはこう答えました。

「基本的に前から計算するんですけど、括弧の中を先に計算するようにしようと、決めたから……ですかね」

「誰が?」

「……おそらく、みんなで。というか、誰かが使い始めて、便利だから広まったんじゃないでしょうか。その辺は英語が出来ていく過程と同じようなものだと思いますよ」

「ああー、なるほど」

これが、真実かどうかは確証はありません。 生徒さんと英語の先生には申し訳ありませんが、なんとなく言いくるめている様な気もします。

しかし、この例は単純ながら"数学"と"言語"と"プログラミング言語"の境界にあるような話なんだなぁと思っています。

つまり、数学の式も単純に言えば言語です。計算手順を示すための言語。プログラミング言語みたいなものです。

どれを先に計算するか?

出てきた順番に計算しましょう。

先に計算して欲しい場合はどうするか?

括弧をつけるようにしましょう。

分かっている人にとっては何をいまさらと思われるのでしょう。ただ、数学を『解を求めるもの』、『問題を解けばよいもの』として教え続けてしまうと、教えている側ですら、本質的な部分を見失ってしまうのかもしれません。 「そう決めたから」というのが答えでは曖昧模糊で数学的ではないように感じられるのも仕方がないです。そんなものなんですが。

話は戻って

const修飾子の話でした。

「constは*(実体化)より結合性が強い」ので括弧でくくります。


const 型名 * == (const 型名) * // constが先

* 型名 const == * (型名 const) // constが先

さらに、基本として


const 型名 == 型名 const // どっちで書いても同じこと

ということを踏まえると……


(const char) * FOO;...(1)
// 文字列リテラルを指す変更出来る変数

(char const) * FOO;...(2)
// (1)と同じこと

(char *) const FOO;...(3)
// (変更してもよい文字列=配列等)を指す変更出来ない変数

((const char) *) const FOO;...(4)
// 文字列リテラルを指す変更出来ない変数

((char const) *) const FOO; ...(5)
// (4)と同じこと

と、いった具合です。

(1)と(2)は同じ、(4)と(5)も同じ。そして、最初の"Hello, World!"の例にでてきたものです。

(3)は危険です。直感に反することですが、"変更してもよい文字列"の中には"変更出来ない文字列"を入れることができるからです。


const char* FOO = "Hello, World!"; // OK

char* VAR = "Hello, World!"; // OK 文字列リテラルは変更して欲しくないのに……。

手元のgcc 4.8.3での"-std=c++11"で試したところ


警告: deprecated conversion from string constant to ‘char*’ [-Wwrite-strings]

と、警告してくれましたが、コンパイルが通りました。続けて、


VAR[0] = 'J';

としたところ、これもコンパイルが通り、実行した結果……。


Segmentation fault

撃沈です。 Clang3.3でも同様で


warning: conversion from string literal to 'char *' is deprecated [-Wdeprecated-writable-strings]

警告してくれるものの、Segmentation faultを起こしました。

この事象については、下記に詳しい記述がありましたので、説明を譲ろうと思います。

https://docs.oracle.com/cd/E19957-01/805-7888/z4000220f2c4c/index.html

歴史的な変遷の影響のようですね。

さて、長くなりましたが本記事はconst修飾子についての話でした。

「constは*(実体化)より結合性が強い」

理解のお役に立てれば幸いです。

参考文献

C Magazine 2004年8月号 P.93 「開発力アップ プログラミングの基礎テクニック」結城浩

0 件のコメント:

コメントを投稿