組み込みC言語における定数の作り方について(前半)

C言語の定数の作り方

組み込みC言語を書いていると、定数をどう作ろうかと迷うことがあります。

最新のC言語では、定数の作り方は3つある*1。えっ?2つじゃないの?僕もそう思っていましたが、以下のサイトでは3つと紹介されています。言われてみれば、まぁ確かに。


  1. #define
  2. const
  3. enum

具体的には以下のように使います。

/* #defineを用いる場合 */
#define VAR 10

/* constを用いる場合 */
const int var = 10;

/* enumを用いる場合 */
enum{var = 10};

で、上記のサイトでは、プログラミング質問サイト「スタックオーバーフロー」のリンク先の書き込みを紹介しています。
c - "static const" vs "#define" vs "enum" - Stack Overflow

結論から言えば、

  • 多くの場合でenumによる定義が良い
  • アドレスを得たいのならconstしかない
  • プリプロセッサ(#ifなど)では#defineしか使えない

ということですが、
それでも先に紹介したサイトでは、これを知ってても、そのようにenumを使うことは無いだろうと締めくくっています。

僕の意見も全く同じで、
個人でコーディングしているならまだよいが、チームでコーディングしている時に、

enum{var = 10};

なんてコードを見たら、
「なんだこれ?作りかけの列挙型かな?
 でもタグ名が無いから他で使用できないし、タグ名も考え中ってこと?
 それとも変数名を考え中?」
とか考えちゃうでしょう。余りにも標準的な書き方ではありません。
チームでのコーディングは、チームメンバーにわかってもらえないといけないので、
「動けばいい」ではダメなのです。

それぞれの特徴

さて、まだタイトルの「組み込み」に触れられていませんが、もう少しご辛抱を。
まずは、そもそもこの3つの定義について、その特徴を挙げていきます。
具体的には、それぞれがコンパイルの過程でどのように処理されるのかを挙げ、
そこから、各々が出来ることと出来ないことを挙げていきます。

#define

#defineはプリプロセッサで処理されるので、コンパイルの時点では既に具体的な値に置換されています。

#define VAR 10

main(){
    int var = VAR;
}

というコードは、コンパイルの時点では

main(){
    int var = 10;
}

となって、VARはコード上から消えています。
コンパイルの時点で消えているので、

  • シンボルが生成されず、デバッグの時に追いかけることが出来ない。
  • メモリ上に配置されないので、容量を食わない
  • メモリ上に配置しないので、勿論アドレスはない

という特徴があります。
また、プリプロセッサでは単純に置換が行われるだけなので、

  • C言語として正しいか否かはチェックされない
  • 演算順序が予期したものと異なるものになることがある

という弱点もあります。
前者でエラーが発生しても、先の通りデバッグで追いかけることが出来ないので、
エラーの解決に時間がかかることがあります(慣れてくるとエラーメッセージで何となく察することができますが)し、
後者の場合はエラーも吐いてくれないので、挙動がおかしい時にその原因を突き止めるのに苦労することになります。
また、

  • スコープの概念がない

ので、一度#defineで定義された定数は、関数を越えて有効です。
万一同名の定数が異なる関数で定義されていると、エラーになります。

そんな理由もあり、プリプロセッサに対する処理(#ifの条件式に使用するなど)以外では、
#defineは使わないようにしましょう
というのが、半ばC言語業界の常識になっていると言っても過言ではないと僕は思っています。

const

(無用な混乱を避けるため、ここではポインタに対するconstのお話はしません。)
さて、一方でこのconstですが、
C言語においてconstは、
「この値は書き換えられない前提で、この修飾子が付いた変数は定義と同時に値を代入しなければいけないもの」
という意味を、コンパイラに(そして、コードを読む他人にも)伝える意味があります。
なので、その意味に反するコーディングされていると、コンパイラは「親切に」エラーを吐いてくれます。
「親切に」と言ったのは、コンパイラとしては、別にそのコーディングで困っているわけではないからです。

int i;
for(i=0;i<10){
    printf("hello!\n");
}

というコードをコンパイラが見たら、
「for文の式が足りないけど、どうするの?困るんだけど?」
となりエラーを吐きます。
一方、

const int var = 5;
var = 10;

というコードをコンパイラが見ると、
「この変数constがついているんだけど、定義の後で代入してるよ。間違ってない?」
となりエラーを吐いてくれます。
実はC言語では、ポインタ経由で操作することでconstの値を定義後に変えることが出来ます。

const int var = 5;
int *p;
printf("%d\n", var);
p = &var;
*p = 10;
printf("%d\n", var);

とすると、最初の出力では5が出てきますが、2回目の出力では10が出てきます。
このように、constはあくまでも「これは定数(のつもり)ですよ」ということをコンパイラに知らせているだけなので、

const int var1 = 5;
int var2 = var1;

int var1 = 5;
int var2 = var1;

コンパイル結果としては同じなのです。

つまり、const intで定義された変数は、intで定義された変数と(原則的には)同じように扱われます。

  • コンパイル時にシンボルが生成されるので、デバッグ時に定数名が使用できる
  • メモリ上に配置されるので、定数のアドレスが存在する
  • メモリ上に配置されるので、勿論メモリの容量を食う
  • C言語の修飾子なので、文法チェックや型のチェックもしてくれる

但し、constで定義された定数がコード上で一切アドレスを参照されていない場合は、
コンパイラが気を利かせて、#defineで書いたときのように定数を具体値に置換してくれることもあります。
(これはコンパイラや最適化レベルの設定次第なので、必ず置換してくれるわけではないことに注意して下さい)

ところで、const intはいわば「定数のふりをしたint」なので、昔は

  • 配列定義時に要素数として使用できない

という弱点がありました。ですので、配列定義時は必ず#defineを使う必要がありました。

#define LEN 5
int array[LEN];

C99規格で「配列の要素数に変数を使用することができる」と変更されたので、最新のC言語では

const int len = 5;
int array[len];

と書くことが出来ますし、

#include <stdio.h>
 
size_t fsize3(int n) {
  char b[n + 3];
  // ……
  return sizeof b;
}
 
int main(int argc, char **argv) {
  printf("%ld\n", fsize3(10));
}    

ということも出来るようになりました。
(コード出典:C言語の最新事情を知る: C99の仕様 - Build Insider

enum

列挙型、所謂enum型は、文字通り「項目を列挙するため」の型です。
列挙型は初期のC言語規格には存在せず、C89で初めて規格化されました。
それまでは#defineを使って列挙型を代用していました。

#define ALICE 0
#define BOB 1

void main(){
    int name = ALICE;
    if(name==ALICE){
        printf("My name is Alice!");
    }else{
        printf("My name is Bob!");
    }
}

前回の記事で#defineについて書いたとおり、このコードはプリプロセッサによって以下のように変換されます。

void main(){
    int name = 0;
    if(name==0){
        printf("My name is Alice!");
    }else{
        printf("My name is Bob!");
    }
}

一方、enumを用いるとコードはこうなります。

void main(){
    enum{Alice, Bob} name = Alice;
    if(name==Alice){
        printf("My name is Alice!");
    }else{
        printf("My name is Bob!");

    }
}

enumは#defineとは異なり、プリプロセッサで置き換えられる事はありません。
プリプロセッサでは置き換えられませんが、コンパイラで置き換えられることになります。
なので、enumの特徴は#defineとconstを足して2で割ったような感じになります。

  • コンパイル時にシンボルが生成されるので、デバッグ時に定数名が使用できる
  • メモリ上には配置されないので、定数のアドレスは存在しない
  • メモリ上に配置されないので、メモリの容量は食わない
  • スコープの概念がある
  • C言語の修飾子なので、文法チェックや型のチェックもしてくれる

但し、最後の項目については、コンパイラによってはエラーを吐いてくれないことがあります。

また、const intは「定数のふりをした変数」でしたが、enumは「れっきとした定数」なので、
switch-case文に使用することができます。

void main(){
    enum {east, west, south, north} direction;
    direction = east;
    switch( direction ){
        case east:
            printf("East!\n");
            break;
        case west:
            printf("West!\n");
            break;
        case south:
            printf("South!\n");
            break;
        case north:
            printf("North!\n");
            break;
    }
}

更に、enumを使うと連想配列みたいなものを作ることも出来ます。

void main(){
    enum {east, west, south, north} direction;    /* 方角 */
    int wind_power[4];    /* 風の強さ */
   wind_power[east] = 3;     /* 東風の強さ */
   wind_power[west] = 2;     /* 西風の強さ */
   wind_power[south] = 4;    /* 南風の強さ */
   wind_power[north] = 1;    /* 北風の強さ */
}

配列のインデックスは整数で指定しますが、
enum型はint型と互換性があるので、上記のように使うことも出来るのです。

(記事が長くなってしまったので、続きはまた今度)

*1:規格としては、1978年のK&Rでは#defineしかなかったが、1989年のC89でconst修飾子とenum型が導入されました。