hiro99ma blog

何か技術的なこと

constが2つある場合のポインタ変数

2024/08/29

C言語を長いこと使っていないのでいろいろ忘れている。

前回の Exercise にこういう行があった。

static const struct device *const async_adapter;

constが 2つある場合、変数への代入ができない方と値の書き換えができない方があった気がする。

const char[]

char c[] = "abc";

これはchar型で初期値abc\0を持つ配列だ。
配列の場合、単にcだけにするとc[0]のアドレス値を指す。

cはポインタ変数ではないので、c[0]char型のデータを代入はできてもccharポインタ型の変数を代入することはできない。
同一型で同一サイズの変数なら代入で書くことによりmemcpy()になるかと思ったが、そうはならんようだ。

そういった事情?だから、cのアドレスはその場所から変わらないことになる。 &cも同じアドレスである。 ポインタ変数と間違えないようにコンパイルエラーにしてもいいんじゃないかという気はするが、

constをつけると、cのデータを書き換えるコードはコンパイルエラーになる。

const char c[] = "abc";

別のconstが付かない変数にcを代入してしまえば、コンパイラはconstが付いていないからエラーにしない。 せいぜい、constが付かない変数に代入する際に warning を出すくらいである。

たまに値を書き換えないのに仮引数でchar *をとる関数がある。 文字列はconst char*などが多いので引数で使うと warning が出る。 仕事でやっていると warning は全部除去するか、あるいは説明をしないといけないということがある。 そういうときはイライラするね(constをつけなかった関数に)。

この例だとc[]は書き換えないので、初期値付きで宣言しないとダメだろう。 かといってc[10]みたいにしても代入できないので同じだ。
だったらこの変数の値は書き換えられないので、c自体を RAM ではなく ROM に置いてしまってよいのではないかという気もする。 そうしてしまえば RAM は消費しないし、初期値のコピーも不要になるし。

const配列を RAM に置くのか ROM に置くのかはよく知らないが、ROM に置かれるものと考えるのが良いだろう。 悩ましいなら、const配列よりはconstポインタにした方がなんとなく無難な気がしているので、私はだいたいそう書くようにしている。

const char *

上に書いたが、書き換えない文字列なんかはconst char *で十分だと思う。

const char *c = "abc";

ダブルクォーテーションで囲んだ文字列は「文字列リテラル」になる。 なので "abc"は ROM に置かれるはず(個人の感想です)。

const char *のポインタ変数は代入が可能である。

#define HLO "HELLO"
#define WLD "WORLD"

static void test1(void)
{
    const char *c = HLO;
    printf("c=%p\n", c);
    c = WLD;
    printf("c=%p\n", c);
}

この関数をコピーして違う名前(test2)を割り当てて両方実行したが、それぞれHLOのアドレスもWLDのアドレスも同じところを指していた。

 test1 --------------
a=0x55b0f7829004
a=0x55b0f7829010

 test2 --------------
a=0x55b0f7829004
a=0x55b0f7829010

#defineは変数ではなくマクロなので、それぞれの場所に展開された(gcc)。 なので関数も違うし別のアドレスになるんじゃないかと思ったが、そうはならなかったのだ。

試しにtest2の方でHLOの代わりに直接"HELLO"を使ってみたが、やはり同じアドレスになった。 この辺はコンパイラの最適化が仕事をしたのだろう。

char * const

constが後ろに来ると、これは c への代入ができなくなる。 こちらはコンパイルエラーだ。

static void test3(void)
{
    char * const c = HLO;
    printf("c=%p\n", c);
    c = WLD; // エラー
}

その代わりと行ってはなんだが、中身への書き込みはできるようになる。

static void test3(void)
{
    char *const c = HLO;
    printf("c=%p\n", c);
    c[0] = 'b';
    printf("c=%p\n", c);
}

あくまでコンパイラ上ではできるというだけで、動くかどうかは別である。
上のコードはコンパイルエラーにはならないが、Linux 上で実行すると Segmentation Fault が発生する。 HLO が ROM に置かれているので書き換えができないからだ。

const char * const

こうすると、ポインタ変数への代入もできないし、中身の書き換えもできない。

static void test4(void)
{
    const char * const c = "HELLO";
    printf("c=%p\n", c);
    c = WLD; // エラー
    c[0] = 'b'; // エラー
}

const char *で十分と書いたが、cを変更するつもりがないなら こっちの方がよいのかな。

const char const *

これはエラーにはならないが、うざいって言われる。

static void test5(void)
{
    const char const * c = "HELLO";
    printf("c=%p\n", c);
}

warning: duplicate ‘const’ declaration specifier [-Wduplicate-decl-specifier]

duplicate なのは後ろの const である。

char const * const

これはconst char * constと同じ扱い。

    char const * const c = "HELLO";

なので無理に文字にするなら、const の直後にあるものが const の対象になるという感じか。 const * がポインタ変数が指した中身で、 const c がポインタ変数cそのもの。

ただ、私は const char * で済ませたい派で、スタックくらいだったらポインタ変数が可変になっていてもいいんじゃないのと思っている。 コンパイラの最適化任せ。

グローバル変数で変更したくないポインタ変数だったら仕方なくconstを2つ付けるが、それでも最初にconstを付けてしまいたい。 まあ、気分の問題と言ってしまえばそれまでだが。

const 以外の修飾子

constの話はもう終わるとして、同じジャンル(修飾子)として volatilerestrict があるそうだ。

volatile は覚えている。 これは最適化させないときに使うやつだ。メモリマップドI/Oでのレジスタアドレスが入ったポインタ変数などだ。 必要があって書き込むので最適化されると困る場合に使う。

restrict は C99 で採用されたらしい。
これはコンパイラへの指示と同時に実装者の制約になっていて、restrictなポインタ変数を使ってしか指している先のメモリにアクセスしないように実装しているので、コンパイラはそれを前提にしてギリギリまで最適化しても良いですよ、ということらしい。

memcpyの仮引数にも付いている。 memcpyしているアドレスにそれ以外からアクセスすることがあってはいけないのでrestrictを付けていても問題ないはず、みたいなところもあるのかな?

使いどころが難しいというか、restrictがあるのを思い出せない気がする。。。

ncs の C言語

ncs は C99 以降になっているのだろうか?

Language Standardsには C99 以降であればよいということが書かれていた。

CONFIG_CPPC++もサポートできるようだ。 STL は便利だけど、気軽に使っているとサイズが大きくなりそうだし、このくらいのマイコンであれば C言語の方がわかりやすいと思うので忘れよう。

< Top page