GAIO CLUB

2023年02月09日

【第4回】理解しづらいコーディングガイドラインとは? ~ポインタ~

静的解析/コンパイラ技術
いまさら聞けない 静的解析/コンパイラ技術
今回は ポインタのルールに関する話です。

一般的にポインタは解りにくいと言われますが、ポインタに関するルールも多いようです。
複数のガイドラインに似ているルールがあり、このようなルールが目立ちます。
・三段階以上のポインタ宣言は禁止
・ ポインタをより厳密にアラインされるポインタ型に変換しない
・ 異なる型へのポインタ変換は禁止
・ポインタとvoidポインタとのキャストも禁止
・constやvolatileを取り除かない
・ ポインタ算術は配列要素を扱うポインタのみ使用可能


宣言に関すること

三段階ポインタについて、ガイドラインが挙げている例は、*を連続3つ書いたものしかないようです。
しかし、複雑になることが原因と書いてありますから、たとえば構造体メンバーに含まれるポインタの接続もルール違反になりそうです。
char ***cp;				/* ルール違反 */
  struct T_ST1 {
    char  st1_c;
  };
  struct T_ST2 {
    int  st2_i;
    struct T_ST1 *st2_st1p;
  };
  struct T_ST3 {
    int  st3_i;
    struct T_ST2 *st3_st2p;
  };
  struct T_ST3  *st3p;			/* ルール違反? */
  
  void test()
  {
    st3p->st3_st2p->st2_st1p->st1_c = ***cp;
  }

キャストに関すること

キャスト関連のルールは多いですね。
とりあえずvoidポインタにしておこうという、安易なコーディングもダメそうです。

ポインタ関連ルールの多くは、CPUの挙動の違いを考慮したものと考えられます。
メモリアクセスにアラインメント制限のあるCPUでは、異なる型のポインタ間の変換による問題が発生します。

たとえば、2バイトデータは偶数アドレス、4バイトデータは4の整数倍アドレスに置かなければならない、などのアドレス制限をもつCPUは少なくありません。このような制限のあるCPUで以下のようなポインタ変換を行うと、この制限に反する可能性があります。
char	C;
short	*SP = (short *)&C;	/* SP が奇数になる可能性あり */
コンパイラはCPUの仕様を守ってコード生成しているので、通常はコーディング時にアラインメントを気にする必要はありません。しかし、上のポインタ代入例では、SPの値が奇数アドレスになってしまう可能性があり、CPUのアラインメント制限に該当してしまいます。

下の例では、アラインメント制限が同じ型どうしなので問題ありませんが、ルール上は違反かもしれません。
unsigned short	US;
short	*SP = (short *)&US;	/* shortサイズどうしなので問題なし(符号違い) */
アラインメント制限のあるCPUで制限違反をしてしまった際の動作も、CPUにより異なります。例外が発生するCPUもありますし、かまわず異なるアドレスをアクセスするCPUもあります。

たとえば、SPの値が0x1001だとしても、*SPによるメモリアクセスをした際には、0x1000番地をアクセスするということが起こります。例外も発生せず異なるアドレスをアクセスしてしまうと、そのことに気付かないかもしれません。

アラインメント制限のないCPUでは、shortやlong変数を奇数アドレスでアクセスしても読み書きできますが、ガイドラインのルール違反であることは同じです。そして、アクセス可能であっても、不整合アドレスのメモリアクセス速度は、アラインメント整合されたアドレスに比べて少し遅くなる場合があります。

malloc()が心配になった方もおられるかもしれませんが、コンパイラが提供するmalloc()であれば安全で切りの良いアドレスを返してくれるので、心配はいりません。もし、自作のmalloc()を使用しているのであれば、アラインメントに気を付けてください。


const や volatile は、型の一部を表すキーワードで、型修飾子とも呼ばれます。constはその変数値が変更不可能であることを示し、初期化以外の代入は文法上できません。

volatileは明示的な変更がなくても値が変化することを示します。たとえば、割り込み処理が変更するような変数で、プログラムの記述上変化するはずのないところでも変化する恐れのある変数に指定します。また、volatileはコンパイラの最適化を抑制するためにも使用することがあります。

ガイドラインではconstやvolatileを取り除くことを禁止しています。
const int	CI = 10;
int		*IP = &CI;		/* ルール違反 : const を取り除いている */
const int	*CIP = &CI;		/* const int を指すポインタ */
余談ですが、constとvolatileは型の後にも書けます。*の後にも書けますが、意味が変わってきます。
const int	CI1 = 1;
int const	CI2 = 2;				/* const int と同じ意味 */
volatile int	VI;
volatile int	*const VICP = &VI;			/* volatile int を指すconstポインタ */
volatile int	* const * volatile VICPVP = &VICP;	/* volatile int を指すconstポインタ を指すvolatileポインタ */
キャスト関連のルール

算術演算に関するルール

ポインタ変数に使える算術演算子は多くありません。
整数との演算で使えるのは加減算のみ、浮動小数点との演算はできません。ポインタどうしの演算は同じ型を指すポインタの減算だけです。

ガイドラインではなく文法上の決まりなので、これに違反するとコンパイルエラーになります。
int	I,*IP;
  short	*SP1,*SP2;
  
    IP += 1;
    IP -= 1;
    IP *= 1;		/* エラー : 乗算は使えない */
    IP /= 1;		/* エラー : 除算は使えない */
    IP += 0.5;		/* エラー : 浮動小数は使えない */
    I = SP1+SP2;	/* エラー : ポインタどうしの加算は使えない */
    I = SP2-SP1;
    I = SP2-IP;		/* エラー : ポインタの指す型が異なっている */
それでは、ガイドラインについて。

"ポインタ算術は配列要素を扱うポインタのみ使用可能"とは、どういう意味でしょうか。
ポインタに算術演算を使うのは、ポインタをずらす、またはポインタの差異を調べる操作をしていると考えられます。
    int		I1,I2;
    short	S1,S2;
    short	*SP1=&S1, *SP2=&S2;
    long	LA[10];
    long	*LP1=&(LA[0]), *LP2=&(LA[3]), *LP3=&(LA[4]), *LP4=&(LA[8]);
    
                ++LP1;
                LP2 -= 3;
                I1 = LP4 - LP3;
                I2 = SP1 - SP2;			/* ルール違反:配列を指していないポインタの算術演算 */
ポインタの初期値が指す配列要素
算術演算で使うポインタは、上図のように"配列の要素を指すポインタであること"というのがルールのようです。

この例のLP4−LP3の結果は、アドレス差ではなく、要素番号差の4になります(1要素4バイトとして、アドレス差は16あります)。++LP1の結果もポインタ値(アドレス)は4増えるので、それと同じ原理です。

配列要素を指していないポインタ、SP1とSP2の減算結果(SP1−SP2)も2つのアドレスの間にshortデータがいくつあるかを求めます。と言っても、どこに配置されるかコンパイラ任せになっているS1とS2変数のアドレス間のshortデータ数を求めることに何の意味もないので、ガイドラインは違反にしているのです。


配列ではなく、malloc()などで確保したエリアを指すポインタはどうでしょうか。

特定の大きさの連続メモリを確保するのですから、おそらく、配列と同様の扱いになると考えられます。
ただし、そもそも動的なメモリ確保を使用してはならないというルールを設定しているガイドラインもありますので、そのガイドラインに従う場合にはmalloc()を使用することはできません。

筆者紹介

浅野 昌尚(あさの まさなお)

ガイオ・テクノロジー株式会社

開発1部 QTXグループ

1980年代から30年以上にわたり汎用構造のCコンパイラ開発に従事し、その間に8ビットマイコンからRISC・VLIW・画像処理プロセッサまで、さまざまなCPU向けのクロスCコンパイラを開発。

人気のコラム

最新のコラム